修正兑换码按用户限兑校验
唯一码和私有码改为按用户限制重复兑换 兑换失败日志补充请求与用户上下文 补充兑换码模式校验测试 同步共享决策记录
This commit is contained in:
@@ -3758,3 +3758,10 @@
|
||||
- 指标口径:生产素材数统计 `editor_project_resource.source_type = generated`;消耗泥点数统计 `profile_wallet_ledger.source_type = asset_operation_consume` 的负向流水绝对值;总注册用户和新增用户数均来自 `profile_dashboard_state`,其中新增用户数按 `created_at` 落入当前时间窗统计;访问次数只统计 `tracking_daily_stat.scope_kind = site`;访问人数和当前使用人数按登录用户去重,匿名访问人数需要未来补 visitor id 后才能统计。
|
||||
- 影响范围:`/admin/api/dashboard`、`shared-contracts` admin DTO、`apps/admin-web` 默认路由和 Dashboard 页面、后台运营文档。
|
||||
- 验证方式:`cargo test -p api-server --manifest-path server-rs/Cargo.toml admin`、`npm run admin-web:typecheck`、`npx vitest run apps/admin-web/src/pages/AdminDashboardPage.test.tsx apps/admin-web/src/app/adminRoutes.test.ts --reporter verbose`、`npm run check:encoding`、`git diff --check`。
|
||||
|
||||
## 2026-06-30 唯一码和私有码按用户限兑一次
|
||||
|
||||
- 背景:运营私有码按内部 user_id 指定用户后,指定用户仍可能无法兑换;排查 release `SEEDUSERLUO0630` 时确认兑换校验把私有码当成全局次数上限,而不是每个允许用户各自限兑一次。
|
||||
- 决策:兑换码使用校验中,公共码继续按 `max_uses` 控制单用户可兑次数;唯一码和私有码改为同一 `code + user_id` 只能成功兑换一次。私有码仍先校验 `allowed_user_ids`,命中允许名单后再按该用户历史使用次数拒绝重复兑换;`global_used_count` 只保留为统计字段,不再作为唯一码 / 私有码的兑换阻断条件。
|
||||
- 影响范围:`module-runtime::validate_runtime_profile_redeem_code_usage`、`profile_redeem_code_usage` 计次语义。
|
||||
- 验证方式:`cargo test -p module-runtime --manifest-path server-rs/Cargo.toml runtime_profile_redeem_code_usage_validation_matches_modes`、`npm run check:encoding`、`git diff --check`。
|
||||
|
||||
@@ -601,9 +601,15 @@ pub async fn redeem_profile_reward_code(
|
||||
let redeemed_at_micros = OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000;
|
||||
let record = state
|
||||
.spacetime_client()
|
||||
.redeem_profile_reward_code(user_id, payload.code, redeemed_at_micros as i64)
|
||||
.redeem_profile_reward_code(user_id.clone(), payload.code, redeemed_at_micros as i64)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
tracing::warn!(
|
||||
request_id = request_context.request_id(),
|
||||
user_id = %user_id,
|
||||
error = %error,
|
||||
"个人中心兑换码兑换失败"
|
||||
);
|
||||
runtime_profile_error_response(
|
||||
&request_context,
|
||||
map_runtime_profile_client_error(error),
|
||||
|
||||
@@ -1837,14 +1837,14 @@ pub fn validate_runtime_profile_redeem_code_usage(
|
||||
RuntimeProfileRedeemCodeMode::Public if user_used_count >= code.max_uses => {
|
||||
Err(RuntimeProfileFieldError::RedeemCodeUsesExhausted)
|
||||
}
|
||||
RuntimeProfileRedeemCodeMode::Unique if code.global_used_count >= code.max_uses => {
|
||||
RuntimeProfileRedeemCodeMode::Unique if user_used_count >= 1 => {
|
||||
Err(RuntimeProfileFieldError::RedeemCodeUsesExhausted)
|
||||
}
|
||||
RuntimeProfileRedeemCodeMode::Private => {
|
||||
if !code.allowed_user_ids.iter().any(|item| item == user_id) {
|
||||
return Err(RuntimeProfileFieldError::RedeemCodeNotAllowedForUser);
|
||||
}
|
||||
if code.global_used_count >= code.max_uses {
|
||||
if user_used_count >= 1 {
|
||||
return Err(RuntimeProfileFieldError::RedeemCodeUsesExhausted);
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -1188,9 +1188,23 @@ mod tests {
|
||||
RuntimeProfileFieldError::RedeemCodeUsesExhausted
|
||||
);
|
||||
|
||||
let unique = RuntimeProfileRedeemCodeSnapshot {
|
||||
mode: RuntimeProfileRedeemCodeMode::Unique,
|
||||
global_used_count: 9,
|
||||
..base.clone()
|
||||
};
|
||||
validate_runtime_profile_redeem_code_usage(&unique, "user-1", 0)
|
||||
.expect("unique code should allow a user that has not redeemed it");
|
||||
assert_eq!(
|
||||
validate_runtime_profile_redeem_code_usage(&unique, "user-1", 1)
|
||||
.expect_err("unique code should reject the same user twice"),
|
||||
RuntimeProfileFieldError::RedeemCodeUsesExhausted
|
||||
);
|
||||
|
||||
let private = RuntimeProfileRedeemCodeSnapshot {
|
||||
mode: RuntimeProfileRedeemCodeMode::Private,
|
||||
allowed_user_ids: vec!["user-2".to_string()],
|
||||
global_used_count: 9,
|
||||
..base.clone()
|
||||
};
|
||||
assert_eq!(
|
||||
@@ -1198,6 +1212,13 @@ mod tests {
|
||||
.expect_err("private code should check allow list"),
|
||||
RuntimeProfileFieldError::RedeemCodeNotAllowedForUser
|
||||
);
|
||||
validate_runtime_profile_redeem_code_usage(&private, "user-2", 0)
|
||||
.expect("private code should allow an allowed user that has not redeemed it");
|
||||
assert_eq!(
|
||||
validate_runtime_profile_redeem_code_usage(&private, "user-2", 1)
|
||||
.expect_err("private code should reject the same allowed user twice"),
|
||||
RuntimeProfileFieldError::RedeemCodeUsesExhausted
|
||||
);
|
||||
|
||||
let disabled = RuntimeProfileRedeemCodeSnapshot {
|
||||
enabled: false,
|
||||
|
||||
Reference in New Issue
Block a user