#!/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