Files
shaguabijia-app-server/scripts/invite_reward_e2e.sh
T
liujiahui 2229dfaf04 feat(invite): 好友列表已比价状态 + DEV 造虚拟好友接口; fix(deploy): 每日兑换定时器锁北京时区 (#104)
## 改动概述

本 PR 含两个主题(关联 7-1 邀请页 / 兑换时区工作)。

### 1. 邀请好友列表「已比价」状态
- `get_invitees` 返回新增 `is_compared` 字段(= `compare_reward_granted`)。
  客户端据此区分好友列表「去提醒 / 邀请成功」、在途列表只取未比价、算在途好友数与在途收益。
- 新增开发接口 `POST/DELETE /invite/seed-fake`(仅非生产, `is_prod` 挡 404):
  造未比价 + 已比价虚拟好友, 已比价者走真实入账发 2 元邀请奖励金,
  方便真机联调在途 / 好友列表 UI; 清理时原路退回, 不留脏账。

### 2. 每日兑换定时器时区修复
- `deploy/daily-exchange.service` / `.timer` 锁定 `Asia/Shanghai`,
  避免按服务器本地时区在错误时刻触发每日兑换(此前隐患: worker 已用 cn_today,
  但 systemd timer 仍按服务器本地时区)。
- 新增 `test_daily_exchange_timezone` 覆盖同一北京日内幂等。

## 测试
- 邀请 + 兑换时区相关用例全过(`28 passed`)。
- 说明: 仓库预存的 3 个 proxy 转发测试(`test_compare_proxy` / `test_coupon_proxy`)失败,
  已在干净 HEAD 上复现确认与本改动无关, 不在本 PR 处理范围。

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: no_gen_mu <liujianhishen@gmail.com>
Reviewed-on: #104
Co-authored-by: liujiahui <liujiahui@wonderable.ai>
Co-committed-by: liujiahui <liujiahui@wonderable.ai>
2026-07-03 14:42:50 +08:00

161 lines
7.9 KiB
Bash

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