新功能(#4): 抽奖 Stage 2 人工奖领奖工单(crypto / physical / manual_other)
Closes HIF-4 Stage 2 交付:人工奖领奖工单完整闭环。crypto / physical / manual_other 三类奖品从抽中到 mark-paid 的全流程可用。 - 迁移 02159_lottery_claim:UNIQUE(draw_id) + 3 支持索引,状态机 pending_claim→reviewing→paying→paid,rejected 可复活,超时 expired - 3 个 PrizeHandler:Dispatch→ErrDispatchNotSupported 兜底、ClaimSchema 各自形态、ValidateClaim 表驱动 - BuildCryptoClaimSchema:抽中时按奖品 config.networks 注入 enum,前端下拉直接可用 - Draw service dispatchOrEnqueueClaim:人工奖同 tx 插 pending_claim(回滚双清),nonce 重放回读 ExpiresAt + ClaimFormSchema - POST /claim 实装:ownership 校验 → prize 类型校验 → handler.ValidateClaim → crypto network 白名单二次校验 → tx CAS status IN (pending_claim, rejected) AND expires_at > now - Admin CRUD 5 接口:list(IN 批拉 snap + user,无 N+1)、summary(GROUP BY 一次拿计数 + overdue 单查)、approve/reject/mark-paid 全走 CAS + audit - Scheduler @every 1h 扫过期,级联 lottery_draw.dispatch_state → expired - 新增错误码 100005-100011(already_submitted / invalid_claim_data / draw_not_found / not_your_draw / claim_expired / claim_state_invalid) - Rebase 后 Stage 2 测试主动 reuse PR E 的 unmetReasonsNotEmpty + evaluatedAtNotZero matcher,人工奖分支若绕过守卫会立即挂 - Stage 1 全部 4 处 guardrail 后端 rebase 时自检过:UnmetReasons、EvaluatedAt、GrantLedger.Payload、AdminMetaMiddleware 全保留 CI 全绿;28 files, +2442/-126;覆盖率 handler 78.9% / model.lottery 74.3% / draw 68.7% / queue/lottery 76.9%
This commit is contained in:
Executable
+215
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env bash
|
||||
# HIF-4 Stage 2 lottery — end-to-end curl for manual-claim workflow (crypto / physical / manual_other).
|
||||
#
|
||||
# 环境准备:
|
||||
# 1. Lottery: { Enable: true } in config.yaml
|
||||
# 2. 已有可用的 admin token 和 user token
|
||||
# 3. 已跑过 Stage 1 且现有活动状态是 running(或者用本脚本重新建一个)
|
||||
#
|
||||
# 使用:
|
||||
# BASE_URL=http://127.0.0.1:8080 ADMIN_TOKEN=<jwt> USER_TOKEN=<jwt> USER_ID=<int> \
|
||||
# bash qa/lottery/stage2_curl.sh
|
||||
#
|
||||
# 覆盖:
|
||||
# ✓ 三种人工奖类型都能配置、抽中、领取
|
||||
# ✓ 状态机:pending_claim → reviewing → paying → paid
|
||||
# ✓ 状态机:reviewing → rejected → reviewing(用户重新提交)
|
||||
# ✓ 过期窗口读取(claim_ttl_hours 覆盖默认 7d)
|
||||
# ✓ 后台工单列表 + summary
|
||||
# ✓ mark-paid 前置校验(crypto 必填 tx_hash / physical 必填 delivery_ref)
|
||||
|
||||
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" step="$2" 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"
|
||||
}
|
||||
|
||||
expect_code() {
|
||||
local body="$1" step="$2" expected="$3" code
|
||||
code=$(printf '%s' "$body" | jq -r '.code // 0')
|
||||
if [[ "$code" != "$expected" ]]; then
|
||||
printf '\033[1;31m✗ %s: expected code=%s, got %s (body=%s)\033[0m\n' "$step" "$expected" "$code" "$body" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf '\033[1;32m✓ %s (code=%s)\033[0m\n' "$step" "$expected"
|
||||
}
|
||||
|
||||
# ---- Admin: build activity + three manual prizes -------------------------
|
||||
|
||||
log "Create Stage 2 activity"
|
||||
start_at=$(date +%s)
|
||||
end_at=$((start_at + 86400 * 30))
|
||||
resp=$(curl -sS -X POST "${BASE_URL}/v1/admin/lottery/activities" "${hdr_admin[@]}" -d "$(cat <<JSON
|
||||
{"title":"QA Stage 2 抽奖","description":"人工奖 e2e","start_at":${start_at},"end_at":${end_at},"grid_size":9,
|
||||
"eligibility":{},"chance_sources":[{"source":"manual_grant","amount":10}],"unmet_action":"block"}
|
||||
JSON
|
||||
)")
|
||||
expect_ok "$resp" "activity create"
|
||||
ACTIVITY_ID=$(printf '%s' "$resp" | jq -r '.data.id')
|
||||
|
||||
log "Add crypto prize (BTC, unlimited, 48h TTL)"
|
||||
resp=$(curl -sS -X POST "${BASE_URL}/v1/admin/lottery/prizes" "${hdr_admin[@]}" -d "$(cat <<JSON
|
||||
{"activity_id":${ACTIVITY_ID},"slot":0,"type":"crypto","name":"1 BTC","icon_url":"",
|
||||
"config":{"amount":"1","currency":"BTC","networks":["BTC","TRX"],"claim_ttl_hours":48},
|
||||
"weight":34,"is_fallback":false}
|
||||
JSON
|
||||
)")
|
||||
expect_ok "$resp" "prize crypto"
|
||||
CRYPTO_PRIZE_ID=$(printf '%s' "$resp" | jq -r '.data.id')
|
||||
|
||||
log "Add physical prize (T-shirt)"
|
||||
resp=$(curl -sS -X POST "${BASE_URL}/v1/admin/lottery/prizes" "${hdr_admin[@]}" -d "$(cat <<JSON
|
||||
{"activity_id":${ACTIVITY_ID},"slot":1,"type":"physical","name":"限量 T 恤","icon_url":"",
|
||||
"config":{"sku_id":"tee-01","sku_name":"限量 T 恤"},
|
||||
"weight":33,"is_fallback":false}
|
||||
JSON
|
||||
)")
|
||||
expect_ok "$resp" "prize physical"
|
||||
|
||||
log "Add manual_other prize"
|
||||
resp=$(curl -sS -X POST "${BASE_URL}/v1/admin/lottery/prizes" "${hdr_admin[@]}" -d "$(cat <<JSON
|
||||
{"activity_id":${ACTIVITY_ID},"slot":2,"type":"manual_other","name":"运营手工奖","icon_url":"",
|
||||
"config":{"desc":"运营线下联系发放"},
|
||||
"weight":33,"is_fallback":false}
|
||||
JSON
|
||||
)")
|
||||
expect_ok "$resp" "prize manual_other"
|
||||
|
||||
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 "Grant 10 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\":10,\"source_ref\":\"qa-stage2\"}")
|
||||
expect_ok "$resp" "chances grant"
|
||||
|
||||
# ---- User: draw until we land on a crypto prize -------------------------
|
||||
|
||||
CRYPTO_DRAW_ID=""
|
||||
attempts=0
|
||||
while [[ -z "${CRYPTO_DRAW_ID}" && "$attempts" -lt 20 ]]; do
|
||||
attempts=$((attempts + 1))
|
||||
nonce=$(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}\"}")
|
||||
expect_ok "$resp" "draw #${attempts}"
|
||||
ptype=$(printf '%s' "$resp" | jq -r '.data.prize.type // ""')
|
||||
if [[ "$ptype" == "crypto" ]]; then
|
||||
CRYPTO_DRAW_ID=$(printf '%s' "$resp" | jq -r '.data.draw_id')
|
||||
printf 'crypto draw id: %s\n' "$CRYPTO_DRAW_ID"
|
||||
printf 'claim response fields:\n%s\n' "$(printf '%s' "$resp" | jq '.data.claim')"
|
||||
# 验证 schema 包含 enum
|
||||
if ! printf '%s' "$resp" | jq -e '.data.claim.claim_form_schema.properties.network.enum | length > 0' >/dev/null; then
|
||||
echo "✗ crypto schema missing network enum" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
if [[ -z "$CRYPTO_DRAW_ID" ]]; then
|
||||
echo "✗ 20 draws still no crypto — weight/rand seed check" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---- User: submit claim (crypto) ----------------------------------------
|
||||
|
||||
log "POST /claim with valid BTC address"
|
||||
resp=$(curl -sS -X POST "${BASE_URL}/v1/lottery/claim" "${hdr_user[@]}" -d "$(cat <<JSON
|
||||
{"draw_id":${CRYPTO_DRAW_ID},"claim_data":{"network":"BTC","address":"bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh"}}
|
||||
JSON
|
||||
)")
|
||||
expect_ok "$resp" "claim submit (crypto)"
|
||||
|
||||
log "POST /claim second time — expect 100005 already_submitted"
|
||||
resp=$(curl -sS -X POST "${BASE_URL}/v1/lottery/claim" "${hdr_user[@]}" -d "$(cat <<JSON
|
||||
{"draw_id":${CRYPTO_DRAW_ID},"claim_data":{"network":"BTC","address":"bc1qanotheraddresslongenoughxxxx"}}
|
||||
JSON
|
||||
)")
|
||||
expect_code "$resp" "claim duplicate reject" "100005"
|
||||
|
||||
# ---- Admin: find claim id in list ----------------------------------------
|
||||
|
||||
log "GET /admin/lottery/claims?status=reviewing"
|
||||
resp=$(curl -sS "${BASE_URL}/v1/admin/lottery/claims?status=reviewing&activity_id=${ACTIVITY_ID}" "${hdr_admin[@]}")
|
||||
expect_ok "$resp" "list claims"
|
||||
CLAIM_ID=$(printf '%s' "$resp" | jq -r ".data.claims[] | select(.draw_id == ${CRYPTO_DRAW_ID}) | .id" | head -1)
|
||||
if [[ -z "$CLAIM_ID" ]]; then
|
||||
echo "✗ could not locate claim for crypto draw ${CRYPTO_DRAW_ID}" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf 'claim id: %s\n' "$CLAIM_ID"
|
||||
|
||||
# ---- Admin: reject then user resubmits ----------------------------------
|
||||
|
||||
log "POST /claims/reject"
|
||||
resp=$(curl -sS -X POST "${BASE_URL}/v1/admin/lottery/claims/reject" "${hdr_admin[@]}" -d "{\"id\":${CLAIM_ID},\"reason\":\"地址格式待核对\"}")
|
||||
expect_ok "$resp" "reject"
|
||||
|
||||
log "GET /records — should show rejected + claim_form_schema"
|
||||
resp=$(curl -sS "${BASE_URL}/v1/lottery/records?activity_id=${ACTIVITY_ID}" "${hdr_user[@]}")
|
||||
expect_ok "$resp" "records after reject"
|
||||
status=$(printf '%s' "$resp" | jq -r ".data.list[] | select(.draw_id == ${CRYPTO_DRAW_ID}) | .claim.status")
|
||||
if [[ "$status" != "rejected" ]]; then
|
||||
echo "✗ expected status=rejected, got '$status'" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! printf '%s' "$resp" | jq -e ".data.list[] | select(.draw_id == ${CRYPTO_DRAW_ID}) | .claim.claim_form_schema.properties.network.enum" >/dev/null; then
|
||||
echo "✗ rejected state must still expose claim_form_schema for resubmit" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "User resubmits after reject"
|
||||
resp=$(curl -sS -X POST "${BASE_URL}/v1/lottery/claim" "${hdr_user[@]}" -d "$(cat <<JSON
|
||||
{"draw_id":${CRYPTO_DRAW_ID},"claim_data":{"network":"BTC","address":"bc1qgoodaddresslongenoughforthevalidator1"}}
|
||||
JSON
|
||||
)")
|
||||
expect_ok "$resp" "resubmit after reject"
|
||||
|
||||
# ---- Admin: approve then mark-paid --------------------------------------
|
||||
|
||||
log "POST /claims/approve (reviewing → paying)"
|
||||
resp=$(curl -sS -X POST "${BASE_URL}/v1/admin/lottery/claims/approve" "${hdr_admin[@]}" -d "{\"id\":${CLAIM_ID}}")
|
||||
expect_ok "$resp" "approve"
|
||||
|
||||
log "POST /claims/mark-paid (missing tx_hash → 400)"
|
||||
resp=$(curl -sS -X POST "${BASE_URL}/v1/admin/lottery/claims/mark-paid" "${hdr_admin[@]}" -d "{\"id\":${CLAIM_ID}}")
|
||||
expect_code "$resp" "mark-paid rejects empty tx_hash for crypto" "400"
|
||||
|
||||
log "POST /claims/mark-paid (with tx_hash)"
|
||||
resp=$(curl -sS -X POST "${BASE_URL}/v1/admin/lottery/claims/mark-paid" "${hdr_admin[@]}" -d "$(cat <<JSON
|
||||
{"id":${CLAIM_ID},"tx_hash":"0xdeadbeefcafebabe12345678"}
|
||||
JSON
|
||||
)")
|
||||
expect_ok "$resp" "mark paid"
|
||||
|
||||
log "GET /records — final should show paid + tx_hash"
|
||||
resp=$(curl -sS "${BASE_URL}/v1/lottery/records?activity_id=${ACTIVITY_ID}" "${hdr_user[@]}")
|
||||
expect_ok "$resp" "records final"
|
||||
tx=$(printf '%s' "$resp" | jq -r ".data.list[] | select(.draw_id == ${CRYPTO_DRAW_ID}) | .claim.tx_hash")
|
||||
if [[ "$tx" != "0xdeadbeefcafebabe12345678" ]]; then
|
||||
echo "✗ expected tx_hash=0xdead..., got '$tx'" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---- Summary endpoint ---------------------------------------------------
|
||||
|
||||
log "GET /admin/lottery/claims/summary"
|
||||
resp=$(curl -sS "${BASE_URL}/v1/admin/lottery/claims/summary" "${hdr_admin[@]}")
|
||||
expect_ok "$resp" "summary"
|
||||
printf 'summary:\n%s\n' "$(printf '%s' "$resp" | jq '.data')"
|
||||
|
||||
printf '\n\033[1;32m✅ Stage 2 end-to-end curl passed.\033[0m\n'
|
||||
Reference in New Issue
Block a user