498 lines
22 KiB
Bash
Executable File
498 lines
22 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# 为内部狗粮预置 10 个创作者与 30 个玩家真实密码账号。
|
||
# 本脚本不修改 Nacos、不创建 new-api 资源;临时 provisioning 配置由操作者在运行前设置、运行后恢复。
|
||
set -euo pipefail
|
||
|
||
readonly EXPECTED_ACK='PROVISION_DOGFOOD_40_USERS_WITH_TEMP_CONFIG'
|
||
readonly CODE_USERNAME_EXISTS='1002090004'
|
||
readonly CODE_REGISTER_DISABLED='1002090007'
|
||
readonly CODE_AD_DOGFOOD_DISABLED='1111002006'
|
||
readonly CODE_TRADE_DOGFOOD_DISABLED='1106008000'
|
||
|
||
MODE='provision'
|
||
if [ "${1:-}" = '--cleanup' ]; then
|
||
MODE='cleanup'
|
||
CLEANUP_MANIFEST="${2:-}"
|
||
CLEANUP_RUN_ID="${3:-}"
|
||
elif [ -n "${1:-}" ]; then
|
||
printf 'PROVISION_FAIL 未知参数:%s\n' "$1" >&2
|
||
exit 2
|
||
fi
|
||
|
||
BASE="${BASE:-http://100.64.0.7:48080}"
|
||
TENANT="${TENANT:-1}"
|
||
DOGFOOD_ACCOUNTS_FILE="${DOGFOOD_ACCOUNTS_FILE:-/root/game-staging/dogfood-accounts.tsv}"
|
||
MYSQL_HOST="${MYSQL_HOST:-127.0.0.1}"
|
||
MYSQL_PORT="${MYSQL_PORT:-13306}"
|
||
MYSQL_DATABASE="${MYSQL_DATABASE:-ruoyi-vue-pro}"
|
||
MYSQL_USER="${MYSQL_USER:-root}"
|
||
MYSQL_BIN="${MYSQL_BIN:-mysql}"
|
||
MYSQL_SSH_HOST="${MYSQL_SSH_HOST-__AUTO__}"
|
||
MYSQL_ENV_FILE="${MYSQL_ENV_FILE:-/root/huijing-dev.env}"
|
||
VERIFY_TIMEOUT_SEC="${VERIFY_TIMEOUT_SEC:-60}"
|
||
ADMIN_TOKEN_FILE="${ADMIN_TOKEN_FILE:-}"
|
||
ADMIN_USERNAME="${ADMIN_USERNAME:-}"
|
||
ADMIN_PASSWORD="${ADMIN_PASSWORD:-}"
|
||
DOGFOOD_PROVISION_RUN_ID="${DOGFOOD_PROVISION_RUN_ID:-}"
|
||
DOGFOOD_PROVISION_CAPABILITY="${DOGFOOD_PROVISION_CAPABILITY:-}"
|
||
DOGFOOD_PROVISION_CLIENT_IP="${DOGFOOD_PROVISION_CLIENT_IP:-127.0.0.1}"
|
||
DOGFOOD_CLEANUP_MANIFEST_FILE="${DOGFOOD_CLEANUP_MANIFEST_FILE:-${DOGFOOD_ACCOUNTS_FILE}.runtime-cleanup.tsv}"
|
||
REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
|
||
REDIS_PORT="${REDIS_PORT:-16379}"
|
||
REDIS_DATABASE="${REDIS_DATABASE:-0}"
|
||
REDIS_CLI_BIN="${REDIS_CLI_BIN:-redis-cli}"
|
||
|
||
log() { printf '%s\n' "$*"; }
|
||
die() { printf 'PROVISION_FAIL %s\n' "$*" >&2; exit 1; }
|
||
|
||
require_env() {
|
||
local name
|
||
for name in "$@"; do
|
||
[ -n "${!name:-}" ] || die "缺少必填环境变量:$name"
|
||
done
|
||
}
|
||
|
||
cleanup_runtime_manifest() {
|
||
local manifest="$1" expected_run_id="$2" keys_file key cleanup_trap deleted=0
|
||
[ -f "$manifest" ] && [ ! -L "$manifest" ] || die '运行态清理清单必须是非 symlink 普通文件'
|
||
[ "$(stat -c '%a' "$manifest" 2>/dev/null || stat -f '%Lp' "$manifest")" = 600 ] || \
|
||
die '运行态清理清单权限必须为 0600'
|
||
[[ "$expected_run_id" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ ]] || die '清理 run-id 非法'
|
||
[[ "$REDIS_PORT" =~ ^[0-9]+$ && "$REDIS_DATABASE" =~ ^[0-9]+$ ]] || die 'Redis 端口或 database 非法'
|
||
require_env REDIS_PASSWORD
|
||
command -v "$REDIS_CLI_BIN" >/dev/null || die "缺少 Redis CLI:$REDIS_CLI_BIN"
|
||
keys_file="$(mktemp)"
|
||
printf -v cleanup_trap 'rm -f -- %q' "$keys_file"
|
||
trap "$cleanup_trap" EXIT
|
||
python3 - "$manifest" "$expected_run_id" >"$keys_file" <<'PY'
|
||
import os,re,stat,sys
|
||
path,expected_run_id=sys.argv[1:]
|
||
metadata=os.stat(path,follow_symlinks=False)
|
||
if not stat.S_ISREG(metadata.st_mode) or stat.S_IMODE(metadata.st_mode) != 0o600:
|
||
raise SystemExit("运行态清理清单必须是 0600 普通文件")
|
||
schema=None
|
||
run_id=None
|
||
keys=[]
|
||
with open(path,encoding="utf-8") as handle:
|
||
for raw in handle:
|
||
raw=raw.rstrip("\n")
|
||
if not raw: continue
|
||
kind,separator,value=raw.partition(" ")
|
||
if not separator: raise SystemExit("运行态清理清单行格式非法")
|
||
if kind == "schema": schema=value
|
||
elif kind == "run_id": run_id=value
|
||
elif kind == "key": keys.append(value)
|
||
else: raise SystemExit("运行态清理清单字段非法")
|
||
if schema != "dogfood-provision-cleanup/1" or run_id != expected_run_id:
|
||
raise SystemExit("运行态清理清单 schema 或 run-id 不匹配")
|
||
rate=re.compile(
|
||
r"passport_ip:(?:register|login):(?:hour:[0-9A-Fa-f:.]+:[0-9]{10}|day:[0-9A-Fa-f:.]+:[0-9]{8})"
|
||
)
|
||
token=re.compile(r"oauth2_access_token:[A-Za-z0-9_-]{16,256}")
|
||
unique=[]
|
||
for key in keys:
|
||
if not (rate.fullmatch(key) or token.fullmatch(key)):
|
||
raise SystemExit("运行态清理 key 越界")
|
||
if key not in unique: unique.append(key)
|
||
print(*unique,sep="\n")
|
||
PY
|
||
while IFS= read -r key; do
|
||
[ -n "$key" ] || continue
|
||
# key 经 stdin 传入,避免 OAuth token 出现在进程参数或日志。
|
||
printf '%s' "$key" | REDISCLI_AUTH="$REDIS_PASSWORD" "$REDIS_CLI_BIN" \
|
||
-h "$REDIS_HOST" -p "$REDIS_PORT" -n "$REDIS_DATABASE" -x DEL >/dev/null || \
|
||
die '定向 Redis 清理失败'
|
||
deleted=$((deleted + 1))
|
||
done <"$keys_file"
|
||
rm -f -- "$manifest" "$keys_file"
|
||
trap - EXIT
|
||
printf 'DOGFOOD_RUNTIME_CLEANUP_PASS run_id=%s keys=%s\n' "$expected_run_id" "$deleted"
|
||
}
|
||
|
||
if [ "$MODE" = cleanup ]; then
|
||
umask 077
|
||
cleanup_runtime_manifest "$CLEANUP_MANIFEST" "$CLEANUP_RUN_ID"
|
||
exit 0
|
||
fi
|
||
|
||
require_env PROVISION_ACK DOGFOOD_PASSWORD_SEED DOGFOOD_PROVISION_RUN_ID DOGFOOD_PROVISION_CAPABILITY
|
||
[ "$PROVISION_ACK" = "$EXPECTED_ACK" ] || die "PROVISION_ACK 不匹配;确认临时配置后必须精确设为 $EXPECTED_ACK"
|
||
[[ "$DOGFOOD_PROVISION_RUN_ID" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ ]] || die 'DOGFOOD_PROVISION_RUN_ID 非法'
|
||
[[ "$DOGFOOD_PROVISION_CAPABILITY" =~ ^[0-9a-f]{64}$ ]] || die 'DOGFOOD_PROVISION_CAPABILITY 必须是 256 bit 小写十六进制'
|
||
[[ "$DOGFOOD_PROVISION_CLIENT_IP" =~ ^[0-9A-Fa-f:.]+$ ]] || die 'DOGFOOD_PROVISION_CLIENT_IP 非法'
|
||
if [ -n "$ADMIN_TOKEN_FILE" ]; then
|
||
[ -z "$ADMIN_USERNAME" ] && [ -z "$ADMIN_PASSWORD" ] || \
|
||
die "ADMIN_TOKEN_FILE 不能与 ADMIN_USERNAME/ADMIN_PASSWORD 混用"
|
||
else
|
||
require_env ADMIN_USERNAME ADMIN_PASSWORD
|
||
fi
|
||
|
||
# 只允许调用 mini-desktop 本机或 Tailscale 地址,防止误向其它环境批量建号。
|
||
BASE_HOST="$(python3 - "$BASE" <<'PY'
|
||
import sys
|
||
from urllib.parse import urlparse
|
||
u=urlparse(sys.argv[1])
|
||
if u.scheme not in ("http","https") or not u.hostname or u.username or u.password or u.query or u.fragment:
|
||
raise SystemExit(2)
|
||
print(u.hostname)
|
||
PY
|
||
)" || die "BASE 必须是合法 http(s) 地址"
|
||
case "$BASE_HOST" in
|
||
127.0.0.1|100.64.0.7) ;;
|
||
*) die "BASE host 只允许 127.0.0.1 或 100.64.0.7,actual=$BASE_HOST" ;;
|
||
esac
|
||
|
||
# 从开发机访问 mini-desktop 时默认在远端执行只读查询;显式传空值可覆盖自动选择。
|
||
if [ "$MYSQL_SSH_HOST" = '__AUTO__' ]; then
|
||
if [ "$BASE_HOST" = '100.64.0.7' ]; then
|
||
MYSQL_SSH_HOST='root@100.64.0.7'
|
||
else
|
||
MYSQL_SSH_HOST=''
|
||
fi
|
||
fi
|
||
|
||
command -v curl >/dev/null || die "缺少 curl"
|
||
command -v python3 >/dev/null || die "缺少 python3"
|
||
|
||
read_private_token() {
|
||
python3 - "$1" <<'PY'
|
||
import os,stat,sys
|
||
path=sys.argv[1]
|
||
flags=os.O_RDONLY|getattr(os,"O_NOFOLLOW",0)
|
||
try:
|
||
fd=os.open(path,flags)
|
||
except OSError as error:
|
||
raise SystemExit(f"无法安全打开 ADMIN_TOKEN_FILE:{error}")
|
||
with os.fdopen(fd,"r",encoding="utf-8") as handle:
|
||
metadata=os.fstat(handle.fileno())
|
||
if not stat.S_ISREG(metadata.st_mode) or stat.S_IMODE(metadata.st_mode) != 0o600:
|
||
raise SystemExit("ADMIN_TOKEN_FILE 必须是 0600 普通文件")
|
||
token=handle.read().strip()
|
||
if not token or any(character.isspace() for character in token):
|
||
raise SystemExit("ADMIN_TOKEN_FILE 内容为空或包含空白字符")
|
||
print(token,end="")
|
||
PY
|
||
}
|
||
|
||
umask 077
|
||
BODY_FILE="$(mktemp)"
|
||
TMP_ACCOUNTS="$(mktemp)"
|
||
PROVISION_HEADER_FILE="$(mktemp)"
|
||
{
|
||
printf 'header = "X-Dogfood-Provisioning-Run-Id: %s"\n' "$DOGFOOD_PROVISION_RUN_ID"
|
||
printf 'header = "X-Dogfood-Provisioning-Capability: %s"\n' "$DOGFOOD_PROVISION_CAPABILITY"
|
||
} >"$PROVISION_HEADER_FILE"
|
||
chmod 600 "$PROVISION_HEADER_FILE"
|
||
cleanup() { rm -f "$BODY_FILE" "$TMP_ACCOUNTS" "$PROVISION_HEADER_FILE"; }
|
||
trap cleanup EXIT
|
||
|
||
LAST_HTTP=''
|
||
LAST_BODY=''
|
||
request() {
|
||
local token="$1" method="$2" path="$3" data="${4:-}"
|
||
local -a args=(-sS --connect-timeout 5 --max-time 30 -o "$BODY_FILE" -w '%{http_code}'
|
||
-X "$method" -H "tenant-id: $TENANT")
|
||
[ -z "$token" ] || args+=(-H "Authorization: Bearer $token")
|
||
[ "$path" != '/app-api/passport/password-register' ] || args+=(--config "$PROVISION_HEADER_FILE")
|
||
[ -z "$data" ] || args+=(-H 'Content-Type: application/json' --data "$data")
|
||
LAST_HTTP="$(curl "${args[@]}" "$BASE$path" 2>/dev/null || printf '000')"
|
||
LAST_BODY="$(<"$BODY_FILE")"
|
||
}
|
||
|
||
mkdir -p "$(dirname "$DOGFOOD_CLEANUP_MANIFEST_FILE")"
|
||
{
|
||
printf 'schema dogfood-provision-cleanup/1\n'
|
||
printf 'run_id %s\n' "$DOGFOOD_PROVISION_RUN_ID"
|
||
} >"$DOGFOOD_CLEANUP_MANIFEST_FILE"
|
||
chmod 600 "$DOGFOOD_CLEANUP_MANIFEST_FILE"
|
||
|
||
record_rate_keys() {
|
||
local scene="$1"
|
||
printf 'key passport_ip:%s:hour:%s:%s\n' "$scene" "$DOGFOOD_PROVISION_CLIENT_IP" "$(date +%Y%m%d%H)" \
|
||
>>"$DOGFOOD_CLEANUP_MANIFEST_FILE"
|
||
printf 'key passport_ip:%s:day:%s:%s\n' "$scene" "$DOGFOOD_PROVISION_CLIENT_IP" "$(date +%Y%m%d)" \
|
||
>>"$DOGFOOD_CLEANUP_MANIFEST_FILE"
|
||
}
|
||
|
||
record_oauth_tokens() {
|
||
local access_token="$1" refresh_token="$2" token
|
||
for token in "$access_token" "$refresh_token"; do
|
||
[ -n "$token" ] || continue
|
||
[ "${#token}" -ge 16 ] && [ "${#token}" -le 256 ] \
|
||
&& [[ "$token" =~ ^[A-Za-z0-9_-]+$ ]] || \
|
||
die '后端返回的 OAuth token 格式非法,拒绝写入清理清单'
|
||
printf 'key oauth2_access_token:%s\n' "$token" >>"$DOGFOOD_CLEANUP_MANIFEST_FILE"
|
||
done
|
||
}
|
||
|
||
json_value() {
|
||
local path="$1"
|
||
printf '%s' "$LAST_BODY" | python3 -c '
|
||
import json,sys
|
||
try:
|
||
value=json.load(sys.stdin)
|
||
for part in sys.argv[1].split("."):
|
||
value=value.get(part) if isinstance(value,dict) else None
|
||
if value is None: break
|
||
print("" if value is None else value)
|
||
except Exception:
|
||
print("")
|
||
' "$path"
|
||
}
|
||
|
||
json_body() {
|
||
DOGFOOD_JSON_USERNAME="$1" DOGFOOD_JSON_PASSWORD="$2" DOGFOOD_JSON_NICKNAME="${3:-}" \
|
||
python3 - <<'PY'
|
||
import json,os
|
||
body={"username":os.environ["DOGFOOD_JSON_USERNAME"],"password":os.environ["DOGFOOD_JSON_PASSWORD"]}
|
||
if os.environ.get("DOGFOOD_JSON_NICKNAME"):
|
||
body["nickname"]=os.environ["DOGFOOD_JSON_NICKNAME"]
|
||
print(json.dumps(body,separators=(",",":"),ensure_ascii=False))
|
||
PY
|
||
}
|
||
|
||
derive_password() {
|
||
DOGFOOD_DERIVE_USERNAME="$1" python3 - <<'PY'
|
||
import hashlib,hmac,os
|
||
seed=os.environ["DOGFOOD_PASSWORD_SEED"].encode()
|
||
username=os.environ["DOGFOOD_DERIVE_USERNAME"].encode()
|
||
# 28 位十六进制字符串满足后端 6-32 位密码约束;同 seed+username 可确定性重跑。
|
||
print(hmac.new(seed,username,hashlib.sha256).hexdigest()[:28])
|
||
PY
|
||
}
|
||
|
||
# MySQL 查询只返回聚合计数。密码只经 MYSQL_PWD 传给客户端,不出现在命令行与日志。
|
||
mysql_query() {
|
||
local sql="$1"
|
||
if [ -n "$MYSQL_SSH_HOST" ]; then
|
||
local sql_base64
|
||
sql_base64="$(printf '%s' "$sql" | base64 | tr -d '\n')"
|
||
ssh -o ProxyCommand=none "$MYSQL_SSH_HOST" bash -s -- \
|
||
"$MYSQL_ENV_FILE" "$MYSQL_HOST" "$MYSQL_PORT" "$MYSQL_DATABASE" "$MYSQL_USER" "$sql_base64" <<'REMOTE'
|
||
set -euo pipefail
|
||
env_file="$1"; host="$2"; port="$3"; database="$4"; user="$5"; sql_base64="$6"
|
||
[ -f "$env_file" ] || { printf '远端 MySQL 环境文件不存在\n' >&2; exit 2; }
|
||
set -a; . "$env_file"; set +a
|
||
: "${MYSQL_ROOT_PASSWORD:?远端环境文件缺 MYSQL_ROOT_PASSWORD}"
|
||
printf '%s' "$sql_base64" | base64 -d | MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysql --batch --skip-column-names \
|
||
-h "$host" -P "$port" -u "$user" "$database"
|
||
REMOTE
|
||
return
|
||
fi
|
||
if [ -z "${MYSQL_PASSWORD:-}" ]; then
|
||
[ -f "$MYSQL_ENV_FILE" ] || die "本地查询需 MYSQL_PASSWORD 或 MYSQL_ENV_FILE=$MYSQL_ENV_FILE"
|
||
set -a
|
||
# shellcheck disable=SC1090
|
||
. "$MYSQL_ENV_FILE"
|
||
set +a
|
||
MYSQL_PASSWORD="${MYSQL_ROOT_PASSWORD:-}"
|
||
fi
|
||
[ -n "${MYSQL_PASSWORD:-}" ] || die "无法取得 MySQL 只读查询密码"
|
||
printf '%s' "$sql" | MYSQL_PWD="$MYSQL_PASSWORD" "$MYSQL_BIN" --batch --skip-column-names \
|
||
-h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" "$MYSQL_DATABASE"
|
||
}
|
||
|
||
TARGET_NAMES=()
|
||
for i in $(seq 1 10); do
|
||
printf -v suffix '%02d' "$i"
|
||
TARGET_NAMES+=("dogfoodc$suffix")
|
||
done
|
||
for i in $(seq 1 30); do
|
||
printf -v suffix '%02d' "$i"
|
||
TARGET_NAMES+=("dogfoodp$suffix")
|
||
done
|
||
TARGET_SQL="$(printf "'%s'," "${TARGET_NAMES[@]}")"
|
||
TARGET_SQL="${TARGET_SQL%,}"
|
||
|
||
count_sql() {
|
||
cat <<SQL
|
||
SELECT COUNT(DISTINCT p.id),
|
||
COUNT(DISTINCT CASE WHEN p.username IN ('dogfoodc01','dogfoodc02','dogfoodc03','dogfoodc04','dogfoodc05','dogfoodc06','dogfoodc07','dogfoodc08','dogfoodc09','dogfoodc10') AND p.creator_flag=1 THEN p.id END),
|
||
COUNT(DISTINCT CASE
|
||
WHEN p.username LIKE 'dogfoodc%' AND COALESCE(p.creator_flag,0)<>1 THEN p.id
|
||
WHEN p.username LIKE 'dogfoodp%' AND COALESCE(p.creator_flag,0)<>0 THEN p.id
|
||
END),
|
||
COUNT(DISTINCT CASE WHEN q.claim_count=1 THEN p.id END),
|
||
COUNT(DISTINCT CASE WHEN q.claim_count>1 THEN p.id END),
|
||
(SELECT COUNT(*) FROM newapi_quota_pool WHERE status='FREE' AND deleted=b'0')
|
||
FROM game_player p
|
||
LEFT JOIN (
|
||
SELECT claimed_by_game_player_id, COUNT(*) AS claim_count
|
||
FROM newapi_quota_pool
|
||
WHERE status='CLAIMED' AND deleted=b'0'
|
||
GROUP BY claimed_by_game_player_id
|
||
) q ON q.claimed_by_game_player_id=p.id
|
||
WHERE p.username IN ($TARGET_SQL) AND p.deleted=b'0';
|
||
SQL
|
||
}
|
||
|
||
read_counts() {
|
||
local raw
|
||
raw="$(mysql_query "$(count_sql)")" || die "MySQL 只读计数查询失败"
|
||
read -r ACCOUNT_COUNT CREATOR_COUNT ROLE_MISMATCH_COUNT CLAIMED_ACCOUNT_COUNT \
|
||
DUPLICATE_CLAIM_COUNT FREE_COUNT <<<"$raw"
|
||
[[ "${ACCOUNT_COUNT:-}" =~ ^[0-9]+$ && "${CREATOR_COUNT:-}" =~ ^[0-9]+$ \
|
||
&& "${ROLE_MISMATCH_COUNT:-}" =~ ^[0-9]+$ && "${CLAIMED_ACCOUNT_COUNT:-}" =~ ^[0-9]+$ \
|
||
&& "${DUPLICATE_CLAIM_COUNT:-}" =~ ^[0-9]+$ && "${FREE_COUNT:-}" =~ ^[0-9]+$ ]] \
|
||
|| die "MySQL 计数结果格式非法"
|
||
}
|
||
|
||
# 只查询本次 40 个目标账号中尚无 CLAIMED 记录的 game_player.id;补偿范围以此只读结果为起点。
|
||
missing_claim_sql() {
|
||
cat <<SQL
|
||
SELECT p.id
|
||
FROM game_player p
|
||
LEFT JOIN newapi_quota_pool q
|
||
ON q.claimed_by_game_player_id=p.id AND q.status='CLAIMED' AND q.deleted=b'0'
|
||
WHERE p.username IN ($TARGET_SQL) AND p.deleted=b'0' AND q.id IS NULL
|
||
ORDER BY p.id;
|
||
SQL
|
||
}
|
||
|
||
# 登录返回的 40 个 userId 是本轮允许补偿的白名单;数据库返回值必须再次落在该集合内。
|
||
is_target_user_id() {
|
||
local expected="$1" candidate
|
||
for candidate in "${TARGET_USER_IDS[@]}"; do
|
||
[ "$candidate" = "$expected" ] && return 0
|
||
done
|
||
return 1
|
||
}
|
||
|
||
read_missing_claim_user_ids() {
|
||
local raw user_id candidate
|
||
raw="$(mysql_query "$(missing_claim_sql)")" || die "MySQL 缺失 claim 查询失败"
|
||
MISSING_CLAIM_USER_IDS=()
|
||
while IFS= read -r user_id; do
|
||
[ -n "$user_id" ] || continue
|
||
[[ "$user_id" =~ ^[0-9]+$ ]] || die "MySQL 缺失 claim 查询返回非法 userId:$user_id"
|
||
is_target_user_id "$user_id" || die "缺失 claim 的 userId=$user_id 不属于本次 40 个目标账号,拒绝越界补偿"
|
||
for candidate in "${MISSING_CLAIM_USER_IDS[@]}"; do
|
||
[ "$candidate" != "$user_id" ] || die "MySQL 缺失 claim 查询返回重复 userId:$user_id"
|
||
done
|
||
MISSING_CLAIM_USER_IDS+=("$user_id")
|
||
done <<<"$raw"
|
||
}
|
||
|
||
log "预置目标:10 creator + 30 player;BASE host=$BASE_HOST runId=$DOGFOOD_PROVISION_RUN_ID"
|
||
log "运行前置:dogfood=true,临时 password-register/provisioning=true,注册与登录 IP 时/日上限均为 60;结束后恢复配置。"
|
||
|
||
# 临时开窗只能放行密码注册;广告和资金入口必须继续由 dogfood 后端守卫拒绝。
|
||
request '' GET /app-api/ad/slot/list-enabled
|
||
[ "$LAST_HTTP" = '200' ] && [ "$(json_value code)" = "$CODE_AD_DOGFOOD_DISABLED" ] \
|
||
|| die "dogfood 广告守卫未保持关闭(http=$LAST_HTTP code=$(json_value code))"
|
||
|
||
read_counts
|
||
MISSING_COUNT=$((40 - ACCOUNT_COUNT))
|
||
[ "$MISSING_COUNT" -ge 0 ] || die "目标账号计数异常:$ACCOUNT_COUNT"
|
||
[ "$DUPLICATE_CLAIM_COUNT" -eq 0 ] || \
|
||
die "额度池存在重复 claim:duplicate_claims=${DUPLICATE_CLAIM_COUNT},需先修复数据后再预置"
|
||
UNCLAIMED_COUNT=$((40 - CLAIMED_ACCOUNT_COUNT))
|
||
[ "$UNCLAIMED_COUNT" -ge 0 ] || die "目标账号 claim 计数异常:$CLAIMED_ACCOUNT_COUNT"
|
||
REQUIRED_FREE=$((UNCLAIMED_COUNT + 12))
|
||
if [ "$FREE_COUNT" -lt "$REQUIRED_FREE" ]; then
|
||
die "额度池 FREE=${FREE_COUNT},不足以让剩余账号各 claim 一条并保留 12(claimed_accounts=${CLAIMED_ACCOUNT_COUNT} required_free=${REQUIRED_FREE});请先运行受控 new-api import 补建 neice_051-054,本脚本不会创建 new-api 资源"
|
||
fi
|
||
|
||
# admin token 仅驻留内存,不输出、不写 accounts 文件。
|
||
if [ -n "$ADMIN_TOKEN_FILE" ]; then
|
||
ADMIN_TOKEN="$(read_private_token "$ADMIN_TOKEN_FILE")" || die "ADMIN_TOKEN_FILE 安全校验失败"
|
||
else
|
||
ADMIN_BODY="$(json_body "$ADMIN_USERNAME" "$ADMIN_PASSWORD")"
|
||
request '' POST /admin-api/system/auth/login "$ADMIN_BODY"
|
||
[ "$LAST_HTTP" = '200' ] && [ "$(json_value code)" = '0' ] || die "admin 登录失败"
|
||
ADMIN_TOKEN="$(json_value data.accessToken)"
|
||
[ -n "$ADMIN_TOKEN" ] || die "admin 登录响应缺 accessToken"
|
||
fi
|
||
|
||
printf 'username\trole\tuser_id\tpassword\n' >"$TMP_ACCOUNTS"
|
||
TARGET_USER_IDS=()
|
||
FUND_GUARD_VERIFIED=0
|
||
for index in "${!TARGET_NAMES[@]}"; do
|
||
username="${TARGET_NAMES[$index]}"
|
||
if [ "$index" -lt 10 ]; then role='creator'; else role='player'; fi
|
||
password="$(derive_password "$username")"
|
||
body="$(json_body "$username" "$password" "$username")"
|
||
|
||
record_rate_keys register
|
||
request '' POST /app-api/passport/password-register "$body"
|
||
code="$(json_value code)"
|
||
if [ "$LAST_HTTP" = '429' ] || [ "$code" = '429' ]; then
|
||
die "账号 $username 注册触发 429;请确认临时注册 IP 时/日上限均为 60"
|
||
elif [ "$LAST_HTTP" != '200' ]; then
|
||
die "账号 $username 注册请求失败(http=$LAST_HTTP)"
|
||
elif [ "$code" = "$CODE_REGISTER_DISABLED" ]; then
|
||
die "password-register 仍关闭;请启用临时 provisioning 配置"
|
||
elif [ "$code" != '0' ] && [ "$code" != "$CODE_USERNAME_EXISTS" ]; then
|
||
die "账号 $username 注册失败(code=${code:-none});若为 429,请确认临时注册 IP 时/日上限均为 60"
|
||
fi
|
||
[ "$code" != '0' ] || record_oauth_tokens "$(json_value data.accessToken)" "$(json_value data.refreshToken)"
|
||
|
||
# 新建与已存在统一使用派生密码登录;已存在但密码不匹配必须失败,不能冒充幂等。
|
||
record_rate_keys login
|
||
request '' POST /app-api/passport/password-login "$body"
|
||
code="$(json_value code)"
|
||
if [ "$LAST_HTTP" = '429' ] || [ "$code" = '429' ]; then
|
||
die "账号 $username 派生密码登录触发 429;请确认临时登录 IP 时/日上限均为 60"
|
||
elif [ "$LAST_HTTP" != '200' ] || [ "$code" != '0' ]; then
|
||
die "账号 $username 派生密码登录验证失败(http=$LAST_HTTP code=${code:-none});若为 429,请确认临时登录 IP 时/日上限均为 60"
|
||
fi
|
||
user_id="$(json_value data.userId)"
|
||
record_oauth_tokens "$(json_value data.accessToken)" "$(json_value data.refreshToken)"
|
||
[[ "$user_id" =~ ^[0-9]+$ ]] || die "账号 $username 登录响应缺合法 userId"
|
||
is_target_user_id "$user_id" && die "多个目标账号返回同一 userId=$user_id,拒绝继续"
|
||
TARGET_USER_IDS+=("$user_id")
|
||
|
||
# mock token 已关闭;只用首个真实登录账号的 token 验证一次资金后端守卫,且不落日志或证据。
|
||
if [ "$FUND_GUARD_VERIFIED" -eq 0 ]; then
|
||
PLAYER_TOKEN="$(json_value data.accessToken)"
|
||
[ -n "$PLAYER_TOKEN" ] || die "账号 $username 登录响应缺 accessToken,无法验证资金守卫"
|
||
request "$PLAYER_TOKEN" GET /app-api/trade/charge/points-balance
|
||
[ "$LAST_HTTP" = '200' ] && [ "$(json_value code)" = "$CODE_TRADE_DOGFOOD_DISABLED" ] \
|
||
|| die "dogfood 充值守卫未保持关闭(http=$LAST_HTTP code=$(json_value code))"
|
||
request "$PLAYER_TOKEN" GET '/app-api/trade/withdraw/page?pageNo=1&pageSize=1'
|
||
[ "$LAST_HTTP" = '200' ] && [ "$(json_value code)" = "$CODE_TRADE_DOGFOOD_DISABLED" ] \
|
||
|| die "dogfood 提现守卫未保持关闭(http=$LAST_HTTP code=$(json_value code))"
|
||
unset PLAYER_TOKEN
|
||
FUND_GUARD_VERIFIED=1
|
||
log "已用真实玩家身份验证充值/提现狗粮守卫"
|
||
fi
|
||
|
||
if [ "$role" = 'creator' ]; then
|
||
request "$ADMIN_TOKEN" PUT /admin-api/passport/player/set-creator \
|
||
"{\"userId\":$user_id,\"creatorFlag\":1}"
|
||
[ "$LAST_HTTP" = '200' ] && [ "$(json_value code)" = '0' ] || die "账号 $username creatorFlag 置位失败"
|
||
fi
|
||
printf '%s\t%s\t%s\t%s\n' "$username" "$role" "$user_id" "$password" >>"$TMP_ACCOUNTS"
|
||
log "已验证账号 $username role=$role userId=$user_id"
|
||
done
|
||
|
||
mkdir -p "$(dirname "$DOGFOOD_ACCOUNTS_FILE")"
|
||
install -m 600 "$TMP_ACCOUNTS" "$DOGFOOD_ACCOUNTS_FILE"
|
||
|
||
# 注册消息可能已被 ACK、进入 DLQ 或尚在重试;终验前只补偿这 40 人中数据库确认缺失的目标。
|
||
read_missing_claim_user_ids
|
||
for user_id in "${MISSING_CLAIM_USER_IDS[@]}"; do
|
||
request "$ADMIN_TOKEN" POST "/admin-api/aigc/quota/claims/$user_id/reconcile"
|
||
code="$(json_value code)"
|
||
claimed="$(json_value data)"
|
||
if [ "$LAST_HTTP" != '200' ] || [ "$code" != '0' ] || { [ "$claimed" != 'True' ] && [ "$claimed" != 'true' ]; }; then
|
||
die "userId=$user_id 额度 claim 补偿失败(http=$LAST_HTTP code=${code:-none} claimed=${claimed:-none})"
|
||
fi
|
||
log "已幂等补偿额度 claim userId=$user_id"
|
||
done
|
||
|
||
deadline=$((SECONDS + VERIFY_TIMEOUT_SEC))
|
||
while :; do
|
||
read_counts
|
||
if [ "$ACCOUNT_COUNT" -eq 40 ] && [ "$CREATOR_COUNT" -eq 10 ] \
|
||
&& [ "$ROLE_MISMATCH_COUNT" -eq 0 ] && [ "$CLAIMED_ACCOUNT_COUNT" -eq 40 ] \
|
||
&& [ "$DUPLICATE_CLAIM_COUNT" -eq 0 ] && [ "$FREE_COUNT" -ge 12 ]; then
|
||
break
|
||
fi
|
||
[ "$SECONDS" -lt "$deadline" ] || die "终验未收敛:accounts=$ACCOUNT_COUNT creators=$CREATOR_COUNT role_mismatches=$ROLE_MISMATCH_COUNT claimed_accounts=$CLAIMED_ACCOUNT_COUNT duplicate_claims=$DUPLICATE_CLAIM_COUNT FREE=${FREE_COUNT};额度池不足请先运行受控 new-api import 补建 neice_051-054"
|
||
sleep 2
|
||
done
|
||
|
||
log "PROVISION_PASS accounts=40 creators=10 role_mismatches=0 claimed_accounts=40 duplicate_claims=0 FREE=$FREE_COUNT run_id=$DOGFOOD_PROVISION_RUN_ID"
|
||
log "账号凭据已写入 ${DOGFOOD_ACCOUNTS_FILE}(mode=600);密码、token、new-api key 未输出。"
|