feat(invite): 发奖口径改为「比价并下单」+ DEV dev-reset 出厂重置接口
- 发奖触发点从「比价成功上报」(compare/record)挪到「实际下单上报」 (order/report):被邀请人完成比价并真实下单才给邀请人发 2 元邀请奖励金 (冰 2026-07-02 拍板);仅完成比价不再发奖。幂等闸 compare_reward_granted 不变。 - 新增 POST /api/v1/invite/dev-reset:把固定测试号(TEST_ACCOUNT_PHONE)打回 出厂态(清邀请关系/奖励金/savings/流水 + 刷 created_at 骗过 72h 新用户闸), 便于反复当新用户测发奖。双门控:生产 404 + 只认配置的测试号,无鉴权。 - 测试:8 例改为下单驱动 + 新增 2 例覆盖 dev-reset(可反复邀请 / 开关关闭 400)。
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
# 邀请发奖真环境验证脚本(方案①:纯后端)—— PowerShell 版 ·【192 局域网 debug 环境】
|
||||
# ==============================================================================
|
||||
# 证明:好友 B 实际下单(POST /api/v1/order/report)→ 邀请人 A 到账 2 元(invite_cash),
|
||||
# 且「只绑定不下单不发」「重复不重发」「无关系不发」;并证明固定测试号 B 经
|
||||
# dev-reset 后可【反复】当新用户,再次触发发奖。
|
||||
# 不用真下单 / 不用付钱 / 不用无障碍 / 不用第二台设备。
|
||||
#
|
||||
# 打的是本地局域网 debug 环境(默认 http://192.168.0.144:8770 = 本机 LAN IP)。
|
||||
# 两个前提:① SMS_MOCK=true(默认) → 任意 6 位验证码通过;② .env 配了
|
||||
# TEST_ACCOUNT_PHONE=11111111111 并【重启过】app-server(B 靠它免验证码 + dev-reset)。
|
||||
#
|
||||
# 用法(PowerShell):
|
||||
# .\invite_reward_e2e.ps1 # 默认 192.168.0.144:8770
|
||||
# .\invite_reward_e2e.ps1 -BaseUrl http://127.0.0.1:8770
|
||||
# ==============================================================================
|
||||
param(
|
||||
# 目标环境:本地局域网 debug :8770(默认本机 LAN IP)
|
||||
[string]$BaseUrl = "http://192.168.0.144:8770"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$BaseUrl = $BaseUrl.TrimEnd('/')
|
||||
|
||||
# B 固定用测试号(免验证码 + 可 dev-reset 反复当新用户)。必须与 .env 的 TEST_ACCOUNT_PHONE 一致。
|
||||
$phoneB = "11111111111"
|
||||
# A(邀请人)/ X(无关系)用带时间戳的新号。
|
||||
$stamp = [int][double]::Parse((Get-Date -UFormat %s))
|
||||
$suffix = ($stamp % 100000000)
|
||||
$phoneX = "139" + ("{0:D8}" -f $suffix)
|
||||
|
||||
$expectCents = 200 # = INVITE_COMPARE_REWARD_CENTS(2 元)
|
||||
|
||||
Write-Host "======================================================================"
|
||||
Write-Host " 邀请发奖·192 局域网 debug 验证 BASE_URL=$BaseUrl"
|
||||
Write-Host " B(固定测试号·被邀请人)=$phoneB X(无关系·下单)=$phoneX"
|
||||
Write-Host "======================================================================"
|
||||
|
||||
# ---- HTTP 小工具(Invoke-RestMethod 自动解析 JSON)---------------------------
|
||||
function Invoke-Api {
|
||||
param([string]$Method, [string]$Path, $Body, [string]$Token)
|
||||
$headers = @{ "Content-Type" = "application/json; charset=utf-8" }
|
||||
if ($Token) { $headers["Authorization"] = "Bearer $Token" }
|
||||
$req = @{ Method = $Method; Uri = "$BaseUrl$Path"; Headers = $headers }
|
||||
if ($null -ne $Body) {
|
||||
# 用 UTF8 字节发,保证「美团」等中文不乱码
|
||||
$json = ($Body | ConvertTo-Json -Compress)
|
||||
$req["Body"] = [System.Text.Encoding]::UTF8.GetBytes($json)
|
||||
}
|
||||
return Invoke-RestMethod @req
|
||||
}
|
||||
|
||||
# 登录:发码(mock 不真发)→ 用 123456 登录 → 回 access_token
|
||||
# 唯一 device_id 避开「device_id+IP 每小时 5 次」限流(测试号 B 本不受限,统一带上无害)。
|
||||
function Login {
|
||||
param([string]$Phone)
|
||||
$dev = "e2e-$Phone-$stamp"
|
||||
Invoke-Api POST "/api/v1/auth/sms/send" @{ phone = $Phone; device_id = $dev } | Out-Null
|
||||
$r = Invoke-Api POST "/api/v1/auth/sms/login" @{ phone = $Phone; code = "123456"; device_id = $dev }
|
||||
if (-not $r.access_token) { throw "登录失败 phone=$Phone" }
|
||||
return $r.access_token
|
||||
}
|
||||
|
||||
# 把固定测试号 B 打回出厂态(清绑定/发奖/savings + 刷 created_at 骗过 72h 闸)。无需鉴权。
|
||||
function Reset-TestAccount {
|
||||
try {
|
||||
$r = Invoke-Api POST "/api/v1/invite/dev-reset" @{}
|
||||
} catch {
|
||||
throw "dev-reset 失败:$($_.Exception.Message) (检查 app-server 是否用最新代码重启?.env 是否配了 TEST_ACCOUNT_PHONE 并重启?)"
|
||||
}
|
||||
if ($r.status -ne "ok") { throw "dev-reset 返回非 ok:$($r | ConvertTo-Json -Compress)" }
|
||||
}
|
||||
|
||||
function Get-Reward { param([string]$Tok) (Invoke-Api GET "/api/v1/invite/me" $null $Tok).reward_balance_cents }
|
||||
function Get-MyCode { param([string]$Tok) (Invoke-Api GET "/api/v1/invite/me" $null $Tok).invite_code }
|
||||
function Bind-To { param([string]$Tok,[string]$Code) Invoke-Api POST "/api/v1/invite/bind" @{ invite_code = $Code } $Tok }
|
||||
function Report-Order {
|
||||
param([string]$Tok,[string]$EventId)
|
||||
Invoke-Api POST "/api/v1/order/report" @{
|
||||
client_event_id = $EventId
|
||||
platform = "美团"
|
||||
pay_channel = "wechat"
|
||||
compared_price_cents = 2016
|
||||
paid_amount_cents = 2016
|
||||
} $Tok | Out-Null
|
||||
}
|
||||
|
||||
# ---- 断言 -------------------------------------------------------------------
|
||||
$script:pass = 0; $script:fail = 0
|
||||
function Assert-Eq {
|
||||
param($Expected, $Actual, [string]$Desc)
|
||||
if ("$Expected" -eq "$Actual") { Write-Host " ✓ $Desc (=$Actual)"; $script:pass++ }
|
||||
else { Write-Host " ✗ $Desc 期望=$Expected 实得=$Actual" -ForegroundColor Red; $script:fail++ }
|
||||
}
|
||||
|
||||
# ---- 一轮完整发奖验证:reset B → 新 A 绑定 → B 下单 → A 到账 -----------------
|
||||
function Invoke-RewardCycle {
|
||||
param([int]$Tag)
|
||||
$phoneA = "138" + ("{0:D8}" -f (($suffix + $Tag) % 100000000))
|
||||
Write-Host "`n===== 第 $Tag 轮:A=$phoneA 邀请固定号 B=$phoneB ====="
|
||||
|
||||
Reset-TestAccount # 把 B 打回新用户态
|
||||
$tokA = Login $phoneA
|
||||
$tokB = Login $phoneB
|
||||
$codeA = Get-MyCode $tokA
|
||||
Write-Host " A 邀请码=$codeA;B 已 dev-reset 回新用户"
|
||||
|
||||
Assert-Eq 0 (Get-Reward $tokA) "[$Tag] 发奖前 A.invite_cash"
|
||||
|
||||
$bind = Bind-To $tokB $codeA
|
||||
Write-Host " bind → status=$($bind.status)(期望 success)"
|
||||
Assert-Eq 0 (Get-Reward $tokA) "[$Tag] 仅绑定后 A.invite_cash(绑定不发)"
|
||||
|
||||
Report-Order $tokB "evt-$stamp-$Tag-1"
|
||||
Assert-Eq $expectCents (Get-Reward $tokA) "[$Tag] B 首单后 A.invite_cash(下单才发)"
|
||||
|
||||
Report-Order $tokB "evt-$stamp-$Tag-1" # 重复同 event
|
||||
Report-Order $tokB "evt-$stamp-$Tag-2" # 不同 event 第二笔
|
||||
Assert-Eq $expectCents (Get-Reward $tokA) "[$Tag] B 重复/多次上报后 A.invite_cash(只发一次)"
|
||||
}
|
||||
|
||||
# ============================== 开跑 =========================================
|
||||
# 第 1、2 轮:同一个固定号 B 反复当新用户各拿一次发奖(证明 dev-reset 让它可反复用)。
|
||||
Invoke-RewardCycle 1
|
||||
Invoke-RewardCycle 2
|
||||
|
||||
# 无关系用户下单 → 不发奖
|
||||
Write-Host "`n===== 无关系校验:X 没绑定任何人,下单不应发奖 ====="
|
||||
$tokX = Login $phoneX
|
||||
Report-Order $tokX "evt-$stamp-x"
|
||||
Assert-Eq 0 (Get-Reward $tokX) "无邀请关系 X 下单后 X.invite_cash(不发)"
|
||||
|
||||
# ============================== 结果 =========================================
|
||||
Write-Host "`n======================================================================"
|
||||
if ($script:fail -eq 0) {
|
||||
Write-Host " ✅ 全部通过($script:pass 项):下单才发奖 / 绑定不发 / 重复不重发 / 无关系不发" -ForegroundColor Green
|
||||
Write-Host " 且固定号 B 经 dev-reset 后【反复】当新用户两轮均发奖成功。" -ForegroundColor Green
|
||||
exit 0
|
||||
} else {
|
||||
Write-Host " ❌ 有 $($script:fail) 项未通过(通过 $($script:pass) 项)。看上面 ✗ 行。" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env bash
|
||||
# 邀请发奖真环境验证脚本(方案①:纯后端 curl)——【192 局域网 debug 包环境】
|
||||
# ------------------------------------------------------------------------------
|
||||
# 证明:好友 B 实际下单(POST /api/v1/order/report)→ 邀请人 A 到账 2 元(invite_cash),
|
||||
# 且「只绑定不下单不发」「重复上报不重发」「无邀请关系不发」;
|
||||
# 并证明固定测试号 B 经 dev-reset 后可【反复】当新用户,再次触发发奖。
|
||||
# 不用真下单 / 不用付钱 / 不用无障碍 / 不用第二台设备。
|
||||
#
|
||||
# 打的是本地局域网 debug 环境(默认 http://192.168.0.144:8770 = 本机 LAN IP)。
|
||||
# 两个前提(不满足脚本白跑):
|
||||
# ① SMS_MOCK=true(默认即是)→ /sms/login 放行任意 6 位验证码,故用 123456 拿 token。
|
||||
# ② .env 里配了 TEST_ACCOUNT_PHONE=11111111111 并【重启过】app-server —— B 用它免验证码
|
||||
# 登录,且脚本靠 POST /invite/dev-reset 每轮把它打回新用户态。
|
||||
#
|
||||
# 用法:
|
||||
# ./invite_reward_e2e.sh # 用默认 192.168.0.144:8770
|
||||
# ./invite_reward_e2e.sh http://192.168.0.144:8770 # 位置参数传 base
|
||||
# BASE_URL=http://127.0.0.1:8770 ./invite_reward_e2e.sh
|
||||
# 依赖:仅 curl(JSON 用 grep/sed 抽字段,不依赖 python/jq —— Windows Git Bash 里
|
||||
# python3 常是 WindowsApps 假 stub,故意不用)。
|
||||
# ==============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
# ---- 目标环境:本地局域网 debug :8770(改成你的开发机内网 IP)------------------
|
||||
BASE_URL="${1:-${BASE_URL:-http://192.168.0.144:8770}}"
|
||||
BASE_URL="${BASE_URL%/}" # 去掉结尾多余的 /
|
||||
|
||||
# B 固定用测试号(免验证码 + 可 dev-reset 反复当新用户)。必须与 .env 的 TEST_ACCOUNT_PHONE 一致。
|
||||
PHONE_B="11111111111"
|
||||
|
||||
# A(邀请人)/ X(无关系)每轮用带时间戳的新号:A 需从 0 余额验起、X 只要没关系即可。
|
||||
STAMP="$(date +%s)"
|
||||
SUFFIX="${STAMP: -8}"
|
||||
PHONE_X="139${SUFFIX}"
|
||||
|
||||
EXPECT_CENTS=200 # = INVITE_COMPARE_REWARD_CENTS(2 元)
|
||||
|
||||
echo "======================================================================"
|
||||
echo " 邀请发奖·192 局域网 debug 验证 BASE_URL=$BASE_URL"
|
||||
echo " B(固定测试号·被邀请人)=$PHONE_B X(无关系·下单)=$PHONE_X"
|
||||
echo "======================================================================"
|
||||
|
||||
# ---- 小工具:从 JSON 取字段(纯 grep/sed,不依赖 python/jq)------------------
|
||||
# 用法: echo "$json" | jget access_token
|
||||
# 支持字符串值("key":"val")与数字值("key":123);只取顶层首个匹配,够本脚本用。
|
||||
jget() {
|
||||
local key="$1" body; body="$(cat)"
|
||||
local v
|
||||
v="$(printf '%s' "$body" | grep -o "\"$key\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed -E "s/.*:[[:space:]]*\"([^\"]*)\"/\1/")"
|
||||
if [ -n "$v" ]; then printf '%s' "$v"; return; fi
|
||||
printf '%s' "$body" | grep -o "\"$key\"[[:space:]]*:[[:space:]]*-\?[0-9]\+" | head -1 | sed -E "s/.*:[[:space:]]*(-?[0-9]+)/\1/"
|
||||
}
|
||||
|
||||
# ---- POST JSON:body 走 stdin + --data-binary,不内联 -d --------------------
|
||||
# ⚠️ Windows Git Bash 里内联 -d "{...美团...}" 会把中文 UTF-8 字节搞乱 → 后端
|
||||
# "error parsing the body"。把 body 从 stdin 以原始字节喂进去可避开。
|
||||
post_json() {
|
||||
local path="$1" token="$2"
|
||||
local auth=()
|
||||
[ -n "$token" ] && auth=(-H "Authorization: Bearer $token")
|
||||
curl -sS -X POST "$BASE_URL$path" \
|
||||
-H 'Content-Type: application/json' "${auth[@]}" \
|
||||
--data-binary @-
|
||||
}
|
||||
|
||||
# ---- 登录:发码(mock 不真发)→ 用 123456 登录 → 回 access_token -----------
|
||||
# 每个号带唯一 device_id(=phone+stamp):登录/发码限流是「device_id+IP 每小时 5 次」,
|
||||
# 空 device_id 会让本机所有请求挤同一桶,自己把自己限流掉。给唯一 id 各走各的桶。
|
||||
# (测试号 B 本就不受该限流约束,但带上 device_id 无害、统一处理。)
|
||||
login() {
|
||||
local phone="$1"
|
||||
local dev="e2e-${phone}-${STAMP}"
|
||||
post_json /api/v1/auth/sms/send "" <<< "{\"phone\":\"$phone\",\"device_id\":\"$dev\"}" >/dev/null
|
||||
local resp
|
||||
resp="$(post_json /api/v1/auth/sms/login "" <<< "{\"phone\":\"$phone\",\"code\":\"123456\",\"device_id\":\"$dev\"}")"
|
||||
local tok; tok="$(printf '%s' "$resp" | jget access_token)"
|
||||
if [ -z "$tok" ]; then
|
||||
echo "✗ 登录失败 phone=$phone,响应:$resp" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf '%s' "$tok"
|
||||
}
|
||||
|
||||
# 把固定测试号 B 打回出厂态(清绑定/发奖/savings + 刷 created_at 骗过 72h 闸)。无需鉴权。
|
||||
dev_reset() {
|
||||
local resp; resp="$(post_json /api/v1/invite/dev-reset "" <<< '{}')"
|
||||
if [ "$(printf '%s' "$resp" | jget status)" != "ok" ]; then
|
||||
echo "✗ dev-reset 失败,响应:$resp" >&2
|
||||
echo " (检查:app-server 是否已用最新代码重启?.env 是否配了 TEST_ACCOUNT_PHONE 并重启?)" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# GET /invite/me 的某个字段(邀请码 / 奖励金余额都从这里读)
|
||||
invite_me() { curl -sS "$BASE_URL/api/v1/invite/me" -H "Authorization: Bearer $1"; }
|
||||
my_code() { invite_me "$1" | jget invite_code; }
|
||||
reward() { invite_me "$1" | jget reward_balance_cents; }
|
||||
|
||||
bind_to() { post_json /api/v1/invite/bind "$1" <<< "{\"invite_code\":\"$2\"}"; }
|
||||
report_order() {
|
||||
post_json /api/v1/order/report "$1" <<< "{\"client_event_id\":\"$2\",\"platform\":\"美团\",\"pay_channel\":\"wechat\",\"compared_price_cents\":2016,\"paid_amount_cents\":2016}"
|
||||
}
|
||||
|
||||
# ---- 断言 -------------------------------------------------------------------
|
||||
PASS=0; FAIL=0
|
||||
assert_eq() { # 期望 实得 说明
|
||||
if [ "$1" = "$2" ]; then echo " ✓ $3 (=$2)"; PASS=$((PASS+1));
|
||||
else echo " ✗ $3 期望=$1 实得=$2"; FAIL=$((FAIL+1)); fi
|
||||
}
|
||||
|
||||
# ---- 一轮完整发奖验证:reset B → 新 A 绑定 → B 下单 → A 到账 -----------------
|
||||
# $1=轮次标签(用于 event_id 唯一 + 展示)。A 每轮用不同新号(从 0 验起)。
|
||||
run_reward_cycle() {
|
||||
local tag="$1"
|
||||
local phone_a="138$(printf '%08d' $(( (10#$SUFFIX + tag) % 100000000 )) )"
|
||||
echo; echo "===== 第 $tag 轮:A=$phone_a 邀请固定号 B=$PHONE_B ====="
|
||||
|
||||
dev_reset # 把 B 打回新用户态
|
||||
local tok_a tok_b code_a
|
||||
tok_a="$(login "$phone_a")"
|
||||
tok_b="$(login "$PHONE_B")"
|
||||
code_a="$(my_code "$tok_a")"
|
||||
echo " A 邀请码=$code_a;B 已 dev-reset 回新用户"
|
||||
|
||||
assert_eq 0 "$(reward "$tok_a")" "[$tag] 发奖前 A.invite_cash"
|
||||
|
||||
local bind_resp; bind_resp="$(bind_to "$tok_b" "$code_a")"
|
||||
echo " bind → status=$(printf '%s' "$bind_resp" | jget status)(期望 success)"
|
||||
assert_eq 0 "$(reward "$tok_a")" "[$tag] 仅绑定后 A.invite_cash(绑定不发)"
|
||||
|
||||
report_order "$tok_b" "evt-$STAMP-$tag-1" >/dev/null
|
||||
assert_eq "$EXPECT_CENTS" "$(reward "$tok_a")" "[$tag] B 首单后 A.invite_cash(下单才发)"
|
||||
|
||||
report_order "$tok_b" "evt-$STAMP-$tag-1" >/dev/null # 重复同 event
|
||||
report_order "$tok_b" "evt-$STAMP-$tag-2" >/dev/null # 不同 event 的第二笔
|
||||
assert_eq "$EXPECT_CENTS" "$(reward "$tok_a")" "[$tag] B 重复/多次上报后 A.invite_cash(只发一次)"
|
||||
}
|
||||
|
||||
# ============================== 开跑 =========================================
|
||||
# 第 1、2 轮:同一个固定号 B 反复当新用户各拿一次发奖(证明 dev-reset 让它可反复用)。
|
||||
run_reward_cycle 1
|
||||
run_reward_cycle 2
|
||||
|
||||
# 无关系用户下单 → 不发奖
|
||||
echo; echo "===== 无关系校验:X 没绑定任何人,下单不应发奖 ====="
|
||||
TOK_X="$(login "$PHONE_X")"
|
||||
report_order "$TOK_X" "evt-$STAMP-x" >/dev/null
|
||||
assert_eq 0 "$(reward "$TOK_X")" "无邀请关系 X 下单后 X.invite_cash(不发)"
|
||||
|
||||
# ============================== 结果 =========================================
|
||||
echo
|
||||
echo "======================================================================"
|
||||
if [ "$FAIL" -eq 0 ]; then
|
||||
echo " ✅ 全部通过($PASS 项):下单才发奖 / 绑定不发 / 重复不重发 / 无关系不发"
|
||||
echo " 且固定号 B 经 dev-reset 后【反复】当新用户两轮均发奖成功。"
|
||||
exit 0
|
||||
else
|
||||
echo " ❌ 有 $FAIL 项未通过(通过 $PASS 项)。看上面 ✗ 行。"
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user