347 lines
11 KiB
Bash
Executable File
347 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
|
||
set -euo pipefail
|
||
|
||
BASE_URL="${BASE_URL:-http://127.0.0.1:8080}"
|
||
CONFIG_PATH="${CONFIG_PATH:-etc/ppanel.yaml}"
|
||
EMAIL_A="${EMAIL_A:-family_cleanup_$(date +%s)@example.com}"
|
||
DEVICE_A="${DEVICE_A:-qa-cleanup-device-a-$(date +%s)}"
|
||
DEVICE_B="${DEVICE_B:-qa-cleanup-device-b-$(date +%s)}"
|
||
SEND_CODE_RETRY_WAIT="${SEND_CODE_RETRY_WAIT:-61}"
|
||
SEND_CODE_MAX_RETRY="${SEND_CODE_MAX_RETRY:-8}"
|
||
VERIFY_CODE_PROVIDER="${VERIFY_CODE_PROVIDER:-}"
|
||
VERIFY_CODE_OVERRIDE="${VERIFY_CODE_OVERRIDE:-}"
|
||
REGISTER_CODE_OVERRIDE="${REGISTER_CODE_OVERRIDE:-}"
|
||
SECURITY_CODE_OVERRIDE="${SECURITY_CODE_OVERRIDE:-}"
|
||
DELETE_ACCOUNT_CODE_OVERRIDE="${DELETE_ACCOUNT_CODE_OVERRIDE:-}"
|
||
ALLOW_INTERACTIVE_INPUT="${ALLOW_INTERACTIVE_INPUT:-auto}"
|
||
ENABLE_REDIS_CODE_FALLBACK="${ENABLE_REDIS_CODE_FALLBACK:-1}"
|
||
REDIS_CLI_BIN="${REDIS_CLI_BIN:-redis-cli}"
|
||
REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
|
||
REDIS_PORT="${REDIS_PORT:-6379}"
|
||
REDIS_DB="${REDIS_DB:-0}"
|
||
REDIS_PASSWORD="${REDIS_PASSWORD:-}"
|
||
BIND_VERIFY_RETRY="${BIND_VERIFY_RETRY:-1}"
|
||
|
||
bold() { printf "\033[1m%s\033[0m\n" "$*"; }
|
||
info() { printf "[INFO] %s\n" "$*" >&2; }
|
||
warn() { printf "[WARN] %s\n" "$*" >&2; }
|
||
fail() { printf "[FAIL] %s\n" "$*" >&2; exit 1; }
|
||
|
||
require_cmd() {
|
||
command -v "$1" >/dev/null 2>&1 || fail "缺少命令: $1"
|
||
}
|
||
|
||
json_code() { jq -r '.code // ""' <<<"$1"; }
|
||
json_msg() { jq -r '.msg // ""' <<<"$1"; }
|
||
|
||
can_prompt_input() {
|
||
if [[ "$ALLOW_INTERACTIVE_INPUT" == "1" ]]; then
|
||
return 0
|
||
fi
|
||
if [[ "$ALLOW_INTERACTIVE_INPUT" == "0" ]]; then
|
||
return 1
|
||
fi
|
||
[[ -t 0 ]]
|
||
}
|
||
|
||
redis_get_key_raw() {
|
||
local key="$1"
|
||
command -v "$REDIS_CLI_BIN" >/dev/null 2>&1 || return 1
|
||
|
||
local -a args
|
||
args=("$REDIS_CLI_BIN" -h "$REDIS_HOST" -p "$REDIS_PORT" -n "$REDIS_DB" --raw)
|
||
if [[ -n "$REDIS_PASSWORD" ]]; then
|
||
args+=(-a "$REDIS_PASSWORD")
|
||
fi
|
||
"${args[@]}" GET "$key" 2>/dev/null || return 1
|
||
}
|
||
|
||
get_verify_code_from_redis() {
|
||
local email="$1"
|
||
local typ="$2"
|
||
local scene key raw code
|
||
|
||
if [[ "$ENABLE_REDIS_CODE_FALLBACK" != "1" ]]; then
|
||
return 1
|
||
fi
|
||
|
||
case "$typ" in
|
||
1) scene="register" ;;
|
||
2) scene="security" ;;
|
||
4) scene="delete_account" ;;
|
||
*) scene="unknown" ;;
|
||
esac
|
||
|
||
key="auth:verify:email:${scene}:${email}"
|
||
raw="$(redis_get_key_raw "$key" || true)"
|
||
[[ -n "$raw" ]] || return 1
|
||
|
||
code="$(jq -r '.code // ""' <<<"$raw" 2>/dev/null || true)"
|
||
[[ -n "$code" && "$code" != "null" ]] || return 1
|
||
|
||
printf "%s" "$code"
|
||
return 0
|
||
}
|
||
|
||
api_request() {
|
||
local method="$1"
|
||
local path="$2"
|
||
local body="${3:-}"
|
||
local token="${4:-}"
|
||
|
||
local -a args
|
||
args=(-sS -X "$method" "${BASE_URL}${path}" -H "Content-Type: application/json")
|
||
if [[ -n "$token" ]]; then
|
||
args+=(-H "Authorization: ${token}")
|
||
fi
|
||
if [[ "$method" != "GET" ]]; then
|
||
args+=(-d "$body")
|
||
fi
|
||
curl "${args[@]}"
|
||
}
|
||
|
||
assert_success() {
|
||
local resp="$1"
|
||
local step="$2"
|
||
local code
|
||
code="$(json_code "$resp")"
|
||
if [[ "$code" != "200" ]]; then
|
||
fail "${step} 失败, code=${code}, msg=$(json_msg "$resp"), resp=${resp}"
|
||
fi
|
||
}
|
||
|
||
token_or_keep() {
|
||
local resp="$1"
|
||
local fallback="$2"
|
||
local token
|
||
token="$(jq -r '.data.token // ""' <<<"$resp")"
|
||
if [[ -n "$token" && "$token" != "null" ]]; then
|
||
printf "%s" "$token"
|
||
else
|
||
printf "%s" "$fallback"
|
||
fi
|
||
}
|
||
|
||
device_login() {
|
||
local identifier="$1"
|
||
local user_agent="$2"
|
||
local payload resp token
|
||
|
||
payload="$(jq -nc --arg identifier "$identifier" --arg user_agent "$user_agent" '{identifier:$identifier,user_agent:$user_agent}')"
|
||
resp="$(api_request "POST" "/v1/auth/login/device" "$payload")"
|
||
assert_success "$resp" "设备登录(${identifier})"
|
||
|
||
token="$(jq -r '.data.token // ""' <<<"$resp")"
|
||
[[ -n "$token" && "$token" != "null" ]] || fail "设备登录(${identifier})返回空 token: ${resp}"
|
||
printf "%s" "$token"
|
||
}
|
||
|
||
send_code_with_retry() {
|
||
local email="$1"
|
||
local typ="$2"
|
||
local payload resp code retry
|
||
|
||
payload="$(jq -nc --arg email "$email" --argjson type "$typ" '{email:$email,type:$type}')"
|
||
retry=0
|
||
while true; do
|
||
resp="$(api_request "POST" "/v1/common/send_code" "$payload")"
|
||
code="$(json_code "$resp")"
|
||
if [[ "$code" == "200" ]]; then
|
||
printf "%s" "$resp"
|
||
return 0
|
||
fi
|
||
if [[ "$code" == "401" ]]; then
|
||
retry=$((retry + 1))
|
||
if (( retry > SEND_CODE_MAX_RETRY )); then
|
||
fail "发送验证码达到重试上限, email=${email}, type=${typ}, resp=${resp}"
|
||
fi
|
||
warn "验证码发送频率限制,等待 ${SEND_CODE_RETRY_WAIT}s 后重试(${retry}/${SEND_CODE_MAX_RETRY})..."
|
||
sleep "${SEND_CODE_RETRY_WAIT}"
|
||
continue
|
||
fi
|
||
fail "发送验证码失败, email=${email}, type=${typ}, code=${code}, msg=$(json_msg "$resp"), resp=${resp}"
|
||
done
|
||
}
|
||
|
||
get_verify_code() {
|
||
local email="$1"
|
||
local typ="$2"
|
||
local send_resp code
|
||
|
||
send_resp="$(send_code_with_retry "$email" "$typ")"
|
||
code="$(jq -r '.data.code // ""' <<<"$send_resp")"
|
||
if [[ -n "$code" && "$code" != "null" ]]; then
|
||
info "验证码已从接口返回(type=${typ})"
|
||
printf "%s" "$code"
|
||
return 0
|
||
fi
|
||
|
||
code="$(get_verify_code_from_redis "$email" "$typ" || true)"
|
||
if [[ -n "$code" ]]; then
|
||
info "验证码已从 Redis 回读(type=${typ})"
|
||
printf "%s" "$code"
|
||
return 0
|
||
fi
|
||
|
||
if [[ -n "$VERIFY_CODE_PROVIDER" ]]; then
|
||
code="$("$VERIFY_CODE_PROVIDER" "$email" "$typ" || true)"
|
||
if [[ -n "$code" ]]; then
|
||
info "验证码由 VERIFY_CODE_PROVIDER 提供(type=${typ})"
|
||
printf "%s" "$code"
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
if [[ "$typ" == "1" && -n "$REGISTER_CODE_OVERRIDE" ]]; then
|
||
printf "%s" "$REGISTER_CODE_OVERRIDE"
|
||
return 0
|
||
fi
|
||
if [[ "$typ" == "2" && -n "$SECURITY_CODE_OVERRIDE" ]]; then
|
||
printf "%s" "$SECURITY_CODE_OVERRIDE"
|
||
return 0
|
||
fi
|
||
if [[ "$typ" == "4" && -n "$DELETE_ACCOUNT_CODE_OVERRIDE" ]]; then
|
||
printf "%s" "$DELETE_ACCOUNT_CODE_OVERRIDE"
|
||
return 0
|
||
fi
|
||
if [[ -n "$VERIFY_CODE_OVERRIDE" ]]; then
|
||
printf "%s" "$VERIFY_CODE_OVERRIDE"
|
||
return 0
|
||
fi
|
||
|
||
if ! can_prompt_input; then
|
||
fail "验证码不可自动获取(type=${typ}, email=${email})"
|
||
fi
|
||
read -r -p "请输入邮箱 ${email} 的验证码(type=${typ}):" code
|
||
[[ -n "$code" ]] || fail "验证码为空"
|
||
printf "%s" "$code"
|
||
}
|
||
|
||
bind_email_with_verification() {
|
||
local token="$1"
|
||
local email="$2"
|
||
local code="$3"
|
||
local payload
|
||
payload="$(jq -nc --arg email "$email" --arg code "$code" '{email:$email,code:$code}')"
|
||
api_request "POST" "/v1/public/user/bind_email_with_verification" "$payload" "$token"
|
||
}
|
||
|
||
bind_email_with_verification_auto() {
|
||
local token="$1"
|
||
local email="$2"
|
||
local typ="$3"
|
||
local max_retry="${4:-$BIND_VERIFY_RETRY}"
|
||
local attempt=0 code resp api_code
|
||
|
||
while true; do
|
||
code="$(get_verify_code "$email" "$typ")"
|
||
resp="$(bind_email_with_verification "$token" "$email" "$code")"
|
||
api_code="$(json_code "$resp")"
|
||
if [[ "$api_code" == "200" ]]; then
|
||
printf "%s" "$resp"
|
||
return 0
|
||
fi
|
||
if [[ "$api_code" == "70001" && "$attempt" -lt "$max_retry" ]]; then
|
||
warn "绑定邮箱验证码校验失败,重新获取验证码重试($((attempt+1))/${max_retry})..."
|
||
attempt=$((attempt + 1))
|
||
continue
|
||
fi
|
||
fail "绑定邮箱(${email}) 失败, code=${api_code}, msg=$(json_msg "$resp"), resp=${resp}"
|
||
done
|
||
}
|
||
|
||
delete_account() {
|
||
local token="$1"
|
||
local email="$2"
|
||
local code="$3"
|
||
local payload resp
|
||
payload="$(jq -nc --arg email "$email" --arg code "$code" '{email:$email,code:$code}')"
|
||
resp="$(api_request "POST" "/v1/public/user/delete_account" "$payload" "$token")"
|
||
assert_success "$resp" "注销账号(${email})"
|
||
printf "%s" "$resp"
|
||
}
|
||
|
||
db_query_json() {
|
||
local query="$1"
|
||
GOCACHE=/tmp/go-build go run ./script/db_query.go --config "$CONFIG_PATH" --query "$query"
|
||
}
|
||
|
||
assert_db_field_equals() {
|
||
local json="$1"
|
||
local field="$2"
|
||
local expected="$3"
|
||
local actual
|
||
actual="$(jq -r ".[0].${field} // \"\"" <<<"$json")"
|
||
if [[ "$actual" != "$expected" ]]; then
|
||
fail "DB校验失败: 字段 ${field} 期望=${expected}, 实际=${actual}, 数据=${json}"
|
||
fi
|
||
}
|
||
|
||
main() {
|
||
require_cmd curl
|
||
require_cmd jq
|
||
require_cmd go
|
||
|
||
bold "家庭组注销清理校验脚本"
|
||
info "BASE_URL=${BASE_URL}"
|
||
info "CONFIG_PATH=${CONFIG_PATH}"
|
||
info "EMAIL_A=${EMAIL_A}"
|
||
info "DEVICE_A=${DEVICE_A}"
|
||
info "DEVICE_B=${DEVICE_B}"
|
||
|
||
local token_a token_b
|
||
local bind_a_resp bind_b_resp del_b_resp del_a_resp
|
||
local family_id owner_user_id member_user_id
|
||
local code_delete
|
||
local q_member_before q_member_after q_owner_after q_family_after
|
||
|
||
bold "步骤1: A设备登录并绑定邮箱A"
|
||
token_a="$(device_login "$DEVICE_A" "qa-cleanup-device-a")"
|
||
bind_a_resp="$(bind_email_with_verification_auto "$token_a" "$EMAIL_A" 1)"
|
||
token_a="$(token_or_keep "$bind_a_resp" "$token_a")"
|
||
|
||
bold "步骤2: B设备登录并绑定邮箱A加入家庭"
|
||
token_b="$(device_login "$DEVICE_B" "qa-cleanup-device-b")"
|
||
bind_b_resp="$(bind_email_with_verification_auto "$token_b" "$EMAIL_A" 2)"
|
||
token_b="$(token_or_keep "$bind_b_resp" "$token_b")"
|
||
|
||
family_id="$(jq -r '.data.family_id // ""' <<<"$bind_b_resp")"
|
||
owner_user_id="$(jq -r '.data.owner_user_id // ""' <<<"$bind_b_resp")"
|
||
member_user_id="$(jq -r '.data.user_id // ""' <<<"$bind_b_resp")"
|
||
[[ -n "$family_id" && "$family_id" != "null" ]] || fail "无法获取 family_id: ${bind_b_resp}"
|
||
[[ -n "$owner_user_id" && "$owner_user_id" != "null" ]] || fail "无法获取 owner_user_id: ${bind_b_resp}"
|
||
[[ -n "$member_user_id" && "$member_user_id" != "null" ]] || fail "无法获取 member_user_id: ${bind_b_resp}"
|
||
|
||
q_member_before="$(db_query_json "SELECT status, role, family_id FROM user_family_member WHERE user_id = ${member_user_id} ORDER BY id DESC LIMIT 1")"
|
||
assert_db_field_equals "$q_member_before" "status" "1"
|
||
info "B注销前家庭状态校验通过(status=1)"
|
||
|
||
bold "步骤3: 注销设备B账号,检查B离组"
|
||
code_delete="$(get_verify_code "$EMAIL_A" 4)"
|
||
del_b_resp="$(delete_account "$token_b" "$EMAIL_A" "$code_delete")"
|
||
info "B注销结果: $(jq -c '.data' <<<"$del_b_resp")"
|
||
|
||
q_member_after="$(db_query_json "SELECT status, role, family_id FROM user_family_member WHERE user_id = ${member_user_id} ORDER BY id DESC LIMIT 1")"
|
||
assert_db_field_equals "$q_member_after" "status" "3"
|
||
info "B注销后家庭状态校验通过(status=3)"
|
||
|
||
bold "步骤4: 注销设备A(家庭主)账号,检查家庭解散"
|
||
code_delete="$(get_verify_code "$EMAIL_A" 4)"
|
||
del_a_resp="$(delete_account "$token_a" "$EMAIL_A" "$code_delete")"
|
||
info "A注销结果: $(jq -c '.data' <<<"$del_a_resp")"
|
||
|
||
q_owner_after="$(db_query_json "SELECT status, role, family_id FROM user_family_member WHERE user_id = ${owner_user_id} ORDER BY id DESC LIMIT 1")"
|
||
assert_db_field_equals "$q_owner_after" "status" "3"
|
||
info "A注销后主账号家庭状态校验通过(status=3)"
|
||
|
||
q_family_after="$(db_query_json "SELECT status FROM user_family WHERE id = ${family_id} ORDER BY id DESC LIMIT 1")"
|
||
assert_db_field_equals "$q_family_after" "status" "0"
|
||
info "家庭状态校验通过(status=0)"
|
||
|
||
bold "校验通过 ✅ 注销后家庭成员与家庭状态已正确更新"
|
||
}
|
||
|
||
main "$@"
|
||
|