# 抽奖控制策略选择与执行

## 背景与约束

* 不改变算法逻辑（承诺→HMAC+拒绝采样→验算一致），仅通过“输入/配置/时机”达到运营控制。

* 验算用户能看到的是回执中池快照与承诺哈希一致性，不能证明你在抽后操纵，只能确认抽前配置。

## 方案A：权重/库存门控（推荐）

* 做法：在承诺前将目标奖品 `weight=0` 或 `quantity=0`，前 N 次（或直到达到阈值）不进入抽取集合；到期后恢复权重/库存并重新承诺。

* 基于代码：

  * 排除条件：`weight>0 && (quantity==-1 || quantity>0)` 才参与抽取（`internal/service/activity/draw_execute.go:31-35,56-58`）。

  * 承诺快照：包含每个奖励的 `{id,name,weight,quantity_before}`（`internal/service/activity/random_commit.go:53-61,67-73`）。

* 优点：简单直接、无需改算法；前 N 次绝不命中；验算完全通过。

* 缺点：恢复后需生成新承诺（`state_version` 增加），不同时间段 `items_root` 不同，运维需记录策略切换。

## 方案B：承诺版本切换

* 做法：用 `state_version` 管理期的承诺版本：

  * v1：不含目标奖品或其权重为 0 → 前 N 次抽使用 v1。

  * v2：目标奖品恢复权重/库存 → 达到 N 后切换到 v2。

* 基于代码：承诺生成与历史查询（`internal/service/activity/random_commit.go:74-97,121-146`）。

* 优点：语义清晰、审计友好；对不同用户批次可严格区分承诺。

* 缺点：运维复杂度稍高；用户若横向对比可能看到承诺变化，但单次验算仍通过。

## 方案C：直接发放替代抽奖

* 做法：对需要“必中/避中”的个体，使用管理端发放接口 `POST /api/admin/users/:user_id/rewards/grant`（`internal/router/router.go:127`）。

* 优点：精确可控，零风险。

* 缺点：不产生抽奖回执；不适合需要“抽奖体验”的场景。

## 方案对比与推荐

* 目标“前 N 次不出现”且保留抽奖体验：优先选 **方案A（权重/库存门控）**，用 `quantity=0` 或 `weight=0` 让奖品在 N 次前不参与集合；到期后恢复并重新承诺。

* 若需批次化与清晰审计边界：选 **方案B（承诺版本切换）**，以 `state_version` 驱动切换，N 次阈值以抽奖日志计数实现运维。

* 个体定向控制：用 **方案C（直接发放）** 替代抽奖。

## 验算与用户感知

* 验算会确认：回执中的 `server_seed_hash/items_root/weights_total/selected_index/rand_proof` 与承诺一致（`internal/api/admin/verify_draw.go:50-66,104-138`）。

* 用户能“看到”：当次承诺的奖池快照与权重（若回执包含快照，管理端/APP均有：`internal/api/admin/draw_receipt.go:55-73`，`internal/api/activity/draw_app.go:28-34`）。

* 用户“感知不到”：你通过前置配置与时机实现“前 N 次不出现”的意图；只要在承诺前已固化，抽后不会被判定为操纵。

## 执行建议（不改代码）

1. 选定期次与目标奖品，设置前置配置：`quantity=0` 或 `weight=0`。
2. 生成承诺（`commit_random`）并上线；开始计数抽奖日志，达到 N 次后恢复配置并生成新承诺。
3. 记录操作与版本切换，必要时在活动规则中说明奖池/期的切换策略。

## 参考位置

* 参与判定与选取：`internal/service/activity/draw_execute.go:31-35,50-66,131-145`

* 承诺生成与版本：`internal/service/activity/random_commit.go:67-85,74-97,121-146`

* 管理端验证：`internal/api/admin/verify_draw.go:50-66,104-138`

