新功能(#3): 抽奖 Stage 1 收官 — 用户 API + 后台 CRUD + 集成
Closes HIF-3 Stage 1 完整闭环 PR C:用户 API + 后台 CRUD + 抽奖事务服务 + 审计 + QA curl。合并后 Stage 1 可交测试。 架构师 review R1(rulecaps depth≤8/nodes≤64/bytes≤8KB)+ R2(InviteHook source_ref 加 order: 前缀)已全部落地。 - 迁移 02158_admin_action_log:后台写操作审计 - xerr 100xxx 段:抽奖错误码(NotEligible/NoChances/ActivityEnded/RateLimited/NotClaimable/InternalError/RuleTooDeep/RuleTooMany/RuleTooLarge) - feature flag config.Lottery.Enable 默认 false,合并后线上零副作用 - draw service:feature flag → rate limit → pre-tx reads → nonce dedupe → Consume → Pick → 乐观扣库存 → 双快照 → Dispatch → finalize - 用户 API 4 个:GET /config、POST /draw、GET /records、POST /claim(Stage 1 返回 100010) - 后台 CRUD:活动 / 奖品 / rules PUT(rulecaps gate)/ chances/grant - audit.WriteAdminAction:与调用方 tx 同生共死,SHA1 body 摘要 - QA 脚本:qa/lottery/stage1_curl.sh 全链路 curl 测试覆盖:model 76.5% / draw 69% / handler 68.3% / hook 91.9% / rulecaps 87.8% / audit 100% 100 并发抢库存 + 10000 次概率分布 e2e 推 QA 环境(sqlmock 无法忠实模拟 InnoDB 行锁) CI 全绿:构建/Vet/测试 + golangci-lint
This commit is contained in:
Executable
+136
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env bash
|
||||
# HIF-3 Stage 1 lottery — end-to-end curl script for QA.
|
||||
#
|
||||
# 环境准备(一次性):
|
||||
# 1. 在测试环境的 config.yaml 里打开 Lottery: { Enable: true }
|
||||
# 2. 用 admin token 运行 setup 部分(创建活动 + 奖品 + 上架 + 发次数)
|
||||
# 3. 用普通用户 token 运行 user 部分(config → draw → records → claim)
|
||||
#
|
||||
# 使用:
|
||||
# BASE_URL=http://127.0.0.1:8080 ADMIN_TOKEN=<jwt> USER_TOKEN=<jwt> USER_ID=<int> \
|
||||
# bash qa/lottery/stage1_curl.sh
|
||||
#
|
||||
# 输出:每步 request/response 打印到 stdout,非 2xx 或 code!=0 直接退出。
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BASE_URL="${BASE_URL:-http://127.0.0.1:8080}"
|
||||
ADMIN_TOKEN="${ADMIN_TOKEN:-CHANGE_ME}"
|
||||
USER_TOKEN="${USER_TOKEN:-CHANGE_ME}"
|
||||
USER_ID="${USER_ID:-1}"
|
||||
|
||||
hdr_admin=(-H "Authorization: Bearer ${ADMIN_TOKEN}" -H "Content-Type: application/json")
|
||||
hdr_user=(-H "Authorization: Bearer ${USER_TOKEN}" -H "Content-Type: application/json")
|
||||
|
||||
log() {
|
||||
printf '\n\033[1;34m▶ %s\033[0m\n' "$*"
|
||||
}
|
||||
|
||||
expect_ok() {
|
||||
local body="$1"
|
||||
local step="$2"
|
||||
local code
|
||||
code=$(printf '%s' "$body" | jq -r '.code // 0')
|
||||
if [[ "$code" != "0" && "$code" != "200" ]]; then
|
||||
printf '\033[1;31m✗ %s failed: %s\033[0m\n' "$step" "$body" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf '\033[1;32m✓ %s\033[0m\n' "$step"
|
||||
}
|
||||
|
||||
# ---- Admin setup ---------------------------------------------------------
|
||||
|
||||
log "Create activity (draft)"
|
||||
start_at=$(date -v +0S +%s 2>/dev/null || date +%s)
|
||||
end_at=$((start_at + 86400 * 7))
|
||||
create_body=$(cat <<JSON
|
||||
{"title":"QA Stage 1 抽奖","description":"e2e","start_at":${start_at},"end_at":${end_at},"grid_size":9,
|
||||
"eligibility":{},"chance_sources":[{"source":"manual_grant","amount":1}],"unmet_action":"block"}
|
||||
JSON
|
||||
)
|
||||
resp=$(curl -sS -X POST "${BASE_URL}/v1/admin/lottery/activities" "${hdr_admin[@]}" -d "${create_body}")
|
||||
expect_ok "$resp" "activity create"
|
||||
echo "$resp"
|
||||
ACTIVITY_ID=$(printf '%s' "$resp" | jq -r '.data.id')
|
||||
|
||||
log "Add prize (vpn_duration 3 天, unlimited)"
|
||||
prize_body=$(cat <<JSON
|
||||
{"activity_id":${ACTIVITY_ID},"slot":0,"type":"vpn_duration","name":"3 天","icon_url":"",
|
||||
"config":{"duration_days":3},"weight":50,"is_fallback":false}
|
||||
JSON
|
||||
)
|
||||
resp=$(curl -sS -X POST "${BASE_URL}/v1/admin/lottery/prizes" "${hdr_admin[@]}" -d "${prize_body}")
|
||||
expect_ok "$resp" "prize vpn_duration"
|
||||
echo "$resp"
|
||||
|
||||
log "Add fallback prize (none / 谢谢参与)"
|
||||
resp=$(curl -sS -X POST "${BASE_URL}/v1/admin/lottery/prizes" "${hdr_admin[@]}" -d "{\"activity_id\":${ACTIVITY_ID},\"slot\":1,\"type\":\"none\",\"name\":\"谢谢参与\",\"config\":{},\"weight\":50,\"is_fallback\":true}")
|
||||
expect_ok "$resp" "prize fallback"
|
||||
echo "$resp"
|
||||
|
||||
log "Publish activity"
|
||||
resp=$(curl -sS -X POST "${BASE_URL}/v1/admin/lottery/activities/publish" "${hdr_admin[@]}" -d "{\"id\":${ACTIVITY_ID}}")
|
||||
expect_ok "$resp" "publish"
|
||||
|
||||
log "Manually grant 2 chances to user ${USER_ID}"
|
||||
resp=$(curl -sS -X POST "${BASE_URL}/v1/admin/lottery/chances/grant" "${hdr_admin[@]}" -d "{\"activity_id\":${ACTIVITY_ID},\"user_id\":${USER_ID},\"amount\":2,\"source_ref\":\"qa-e2e-1\"}")
|
||||
expect_ok "$resp" "chances grant"
|
||||
|
||||
# ---- User flow -----------------------------------------------------------
|
||||
|
||||
log "GET /config"
|
||||
resp=$(curl -sS "${BASE_URL}/v1/lottery/config?activity_id=${ACTIVITY_ID}" "${hdr_user[@]}")
|
||||
expect_ok "$resp" "config query"
|
||||
echo "$resp"
|
||||
|
||||
nonce_a=$(uuidgen 2>/dev/null || python3 -c "import uuid;print(uuid.uuid4())")
|
||||
log "POST /draw (nonce=${nonce_a})"
|
||||
draw_body="{\"activity_id\":${ACTIVITY_ID},\"client_nonce\":\"${nonce_a}\"}"
|
||||
resp=$(curl -sS -X POST "${BASE_URL}/v1/lottery/draw" "${hdr_user[@]}" -d "${draw_body}")
|
||||
expect_ok "$resp" "draw #1"
|
||||
echo "$resp"
|
||||
|
||||
log "POST /draw same nonce (idempotent replay)"
|
||||
resp2=$(curl -sS -X POST "${BASE_URL}/v1/lottery/draw" "${hdr_user[@]}" -d "${draw_body}")
|
||||
expect_ok "$resp2" "draw replay"
|
||||
draw_id_1=$(printf '%s' "$resp" | jq -r '.data.draw_id')
|
||||
draw_id_2=$(printf '%s' "$resp2" | jq -r '.data.draw_id')
|
||||
if [[ "$draw_id_1" != "$draw_id_2" ]]; then
|
||||
echo "✗ nonce replay produced different draw_id ($draw_id_1 vs $draw_id_2)" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ nonce idempotent: draw_id=${draw_id_1}"
|
||||
|
||||
log "GET /records"
|
||||
resp=$(curl -sS "${BASE_URL}/v1/lottery/records?activity_id=${ACTIVITY_ID}&page=1&size=20" "${hdr_user[@]}")
|
||||
expect_ok "$resp" "records"
|
||||
echo "$resp"
|
||||
|
||||
log "POST /claim (Stage 1 always 4010)"
|
||||
resp=$(curl -sS -X POST "${BASE_URL}/v1/lottery/claim" "${hdr_user[@]}" -d "{\"draw_id\":${draw_id_1}}")
|
||||
code=$(printf '%s' "$resp" | jq -r '.code // 0')
|
||||
if [[ "$code" != "100010" ]]; then
|
||||
echo "✗ /claim should return 100010 not_claimable in Stage 1; got ${code}" >&2
|
||||
echo "$resp" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ claim returned 100010 as expected"
|
||||
|
||||
# ---- Rate limit verification ---------------------------------------------
|
||||
|
||||
log "Trigger rate limit (2 draws within 1 sec)"
|
||||
nonce_b=$(uuidgen 2>/dev/null || python3 -c "import uuid;print(uuid.uuid4())")
|
||||
curl -sS -X POST "${BASE_URL}/v1/lottery/draw" "${hdr_user[@]}" -d "{\"activity_id\":${ACTIVITY_ID},\"client_nonce\":\"${nonce_b}\"}" >/dev/null
|
||||
nonce_c=$(uuidgen 2>/dev/null || python3 -c "import uuid;print(uuid.uuid4())")
|
||||
resp=$(curl -sS -X POST "${BASE_URL}/v1/lottery/draw" "${hdr_user[@]}" -d "{\"activity_id\":${ACTIVITY_ID},\"client_nonce\":\"${nonce_c}\"}")
|
||||
code=$(printf '%s' "$resp" | jq -r '.code // 0')
|
||||
if [[ "$code" != "100004" ]]; then
|
||||
echo "⚠️ expected 100004 rate_limited, got ${code} — non-fatal (Redis may have residual budget)"
|
||||
else
|
||||
echo "✓ rate limit triggered as expected"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "──────────────────────────────────────────"
|
||||
echo " Stage 1 end-to-end curl script completed."
|
||||
echo "──────────────────────────────────────────"
|
||||
Reference in New Issue
Block a user