Skip to content

feat(auth): 开放公开注册,邀请码选填 - #357

Merged
minorcell merged 11 commits into
1024XEngineer:mainfrom
xiaocheny214:feat/invite-code-registration
Aug 18, 2026
Merged

feat(auth): 开放公开注册,邀请码选填#357
minorcell merged 11 commits into
1024XEngineer:mainfrom
xiaocheny214:feat/invite-code-registration

Conversation

@xiaocheny214

@xiaocheny214 xiaocheny214 commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Summary

  • #355 对齐:邀请码只在 注册 时生效,删除 POST /quota/invite/redeem(不再登录后补填)。
  • 邀请码默认 30 天有效(QUOTA_INVITE_CODE_TTL_DAYS)。过期注册返回「邀请码已过期」(业务码 404);非法/不存在仍是「邀请码无效」。
  • 码全局唯一、只增不删。轮换插入新行,把仍有效的旧行 expires_at 截到现在;邀请记录继续指向历史码。
  • invite_code 选填:无码只发注册赠送 300。login-by-code 未知邮箱建号只送 300。
  • 邀请人每日奖励上限:每个 UTC 自然日最多因 3 次邀请入账(QUOTA_INVITE_REWARD_DAILY_LIMIT=3,3×200=600)。计数只看 windup_invite_record,不扫积分流水,因此不影响注册赠送、生成扣费,以及后续付费购买 / 套餐兑换(应使用独立 CreditReason)。第 4 人起仍建号、仍写关系、被邀请人仍得 200,邀请人当日不再 +200。
  • 前端仍不在本 PR(feat(account): add invite rewards center #362 / feat(workspace): add invite reward hint #363 需去掉补填,并处理过期与日限额提示)。

Closes #355

Test plan

  • 不带邀请码或空白可注册,只发 300
  • 未过期邀请码注册:新用户 500;邀请人当日第 1–3 次得 200
  • 同一 UTC 日第 4 次邀请:关系写入、被邀请人仍得 200,邀请人当日不再 +200;次日重置
  • 非法/不存在不建号,「邀请码无效」
  • 过期码不建号,「邀请码已过期」
  • 轮换后旧行保留且不能再注册;GET 在过期后签发新行
  • POST /quota/invite/redeem 已移除(404)
  • login-by-code 未知邮箱建号送 300
  • OpenAPI 同步;相关后端测试通过

公开注册仍关闭自动建号,改为邀请码门槛,并补齐查看、轮换和补填邀请码接口。

Closes 1024XEngineer#355
@vercel

vercel Bot commented Aug 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
windup Ignored Ignored Preview Aug 18, 2026 4:45am

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查了邀请码生成/兑换的事务契约、注册链路、账户中心状态同步以及 OpenAPI。后端与前端聚焦测试均通过,但无积分账户用户可以拿到无法兑换的邀请码,兑换成功后的积分视图也不会同步刷新;前者已用最小复现确认会抛出“积分账户不存在”。

Comment thread backend/packages/app/src/windup_app/server/quota/service.py
Comment thread frontend/src/pages/account/index.tsx Outdated
@codecov

codecov Bot commented Aug 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.83784% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...ackages/app/src/windup_app/server/quota/service.py 96.70% 3 Missing ⚠️
...ackend/packages/app/src/windup_app/web/api/auth.py 92.85% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

覆盖空邀请码、发码碰撞、兑换接口以及账号中心邀请码加载/生成失败。
兑换成功只改了提示,同页余额与流水会停在旧数据。
@minorcell

Copy link
Copy Markdown
Member

@fennoai

  1. 接口是否冗余
  2. 邀请码轮换之后先前的邀请码已经发放出去会怎么样?
  3. 推理邀请码从创建到失效的整个生命周期的多种情况的可能性。

@fennoai

fennoai Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

结论

接口数量本身不算冗余,三个接口分别对应 查询当前码、主动轮换、兑换邀请码 三个独立用例;真正的问题是语义有重叠且生命周期模型过薄。

当前轮换实现是直接覆盖 windup_invite_code.codequota/service.py:339-353)。因此旧码通常会立即失效,但系统没有保存码的版本、状态、失效时间或轮换历史,存在并发结果不稳定、历史旧码可能被重新分配等问题。

1. 接口是否冗余

接口 当前语义 判断
GET /quota/invite/code 查询;不存在时自动创建 查询用例必要,但 GET 有写副作用
POST /quota/invite/generate 不存在时创建,存在时轮换 轮换用例必要;“首次创建”与 GET 重叠
POST /quota/invite/redeem 老用户补填邀请码 只要仍支持历史用户补填,就不能删除

建议:

  • 保留三个业务能力,但把语义改清楚,例如 GET /invite-codePOST /invite-code/rotatePOST /invite-redemptions
  • 最好在用户创建或账户初始化时生成邀请码,让 GET 成为纯查询;否则明确使用一个“确保存在”的写接口。
  • generate 应只表达轮换,而不是同时承担首次创建。
  • 注册服务先查询 InviteCode,随后 redeem_invite_code() 又查询校验一次(user/service.py:177-185,209quota/service.py:367-386)。这是重复校验,但前置校验可避免明显无效邀请码先消耗邮箱验证码;应下沉成统一领域能力,而不是用户模块直接依赖邀请码 ORM 和具体服务实现。

2. 轮换后已发出去的旧码会怎样

  • 尚未兑换的旧码:轮换事务提交后,数据库里已找不到旧值,后续兑换返回“邀请码无效”。没有宽限期。
  • 已经兑换成功的旧码InviteRecord 和双方积分流水继续保留,轮换不会撤销关系或奖励。
  • 正在并发兑换的旧码:结果取决于锁顺序。兑换先锁到邀请码行时可以成功,轮换随后完成;轮换先提交时兑换失败。因此“点击轮换”的瞬间并不是绝对失效边界。
  • 并发轮换:轮换读取邀请码行时没有 FOR UPDATE。两个请求可能各自返回不同新码,但后提交者覆盖前者,导致其中一个接口刚返回的码立即无效。
  • 历史旧码可能复活:分配器只检查当前 InviteCode.code,不检查 InviteRecord 或历史码。旧码被覆盖后未来理论上可随机分配给另一用户;此前拿到旧码的人可能兑换成功,却奖励了新的拥有者。
  • used_count 不会在轮换时清零,所以它实际是“邀请人累计成功次数”,不是“当前邀请码使用次数”;create_at 也仍是首个码的创建时间。当前响应字段容易造成误解。

3. 当前邀请码生命周期推演

  1. 不存在:用户首次 GET 或 POST 时创建;每用户只有一行,当前码全局唯一。
  2. 有效:无过期时间、无最大使用次数、无停用状态;在轮换前可被任意多个不同用户使用。
  3. 发放:系统不记录分享对象或发放时间,因此无法判断一个旧码是否仍在外部传播。
  4. 注册兑换:先验证邀请码存在,再消费邮箱验证码、创建用户和积分账户,最后重新锁定并兑换;所有数据库写入在同一事务中,失败会回滚用户和积分。
  5. 注册与轮换竞态:前置校验通过后若邀请码被轮换,最终兑换会失败;但邮箱验证码已在 Redis 中删除,用户需要重新获取验证码。
  6. 成功兑换:写入唯一的 invitee_id 记录、累计 used_count,双方发奖;关系和奖励没有撤销机制。
  7. 重复/自邀:同一用户只能填一次,不能填自己的当前码。
  8. 并发重复兑换:数据库唯一约束能避免最终出现两条关系或双发奖励,但竞争失败的请求可能抛 IntegrityError,表现成 500,而不是稳定的“已填写过邀请码”。不同邀请码之间也没有共同业务锁。
  9. 历史用户补填:任意没有 InviteRecord 的老用户都可填一次。两个老用户可以互填彼此邀请码,双方各得到两次奖励;更长的互邀环也可成立,需要确认是否符合运营规则。
  10. 轮换失效:旧值直接丢失,没有 REVOKED/EXPIRED 状态,也不能审计未被兑换过的历史码及其有效区间。
  11. 再次分配:失效码没有永久保留,理论上可以成为其他用户的新码,因此“失效”并非永久终态。

建议的数据模型

若产品要求“轮换后旧码永久失效”,建议把覆盖模型改成版本模型:

  • 每次生成新增一条邀请码记录:id / user_id / code / status / issued_at / revoked_at / expires_at
  • 同一用户只允许一个 ACTIVE 版本;轮换在同一事务内锁定当前版本、标记 REVOKED,再插入新版本。
  • code 永久唯一,禁止历史码重新分配。
  • InviteRecord 保存 invite_code_id,同时保留兑换时的 code 快照。
  • 兑换时锁定具体版本,并原子检查状态、有效期、次数上限和 invitee 唯一性。
  • used_count 改成明确的累计统计,或按记录聚合;若保留字段,应命名为 total_used_count

如果希望轮换后旧码仍有宽限期,也应保留版本记录,并让旧版本进入 GRACE 状态直到明确的 expires_at,而不是覆盖。

建议补充的测试

  • 轮换后旧码失败、新码成功,历史奖励不受影响。
  • 兑换与轮换并发的确定性语义。
  • 两次并发轮换只能返回一个最终有效码。
  • 同一用户并发兑换同码及不同码时返回业务错误而非 500。
  • 历史旧码永不重新分配。
  • 注册前置校验后发生轮换时,邮箱验证码如何处理。
  • 老用户互填、循环邀请是否应禁止。
  • 明确 used_count/create_at/update_at 是按用户累计还是按邀请码版本统计。

验证

已基于固定范围 e385f098...aca1a814 审查后端模型、服务、API、前端调用和相关测试。仓库测试未执行:当前环境没有 uv,系统 Python 也未安装 pytest

View job run

Comment thread frontend/src/features/account-panel/index.tsx
Comment thread frontend/src/features/account-panel/index.tsx
Comment thread frontend/src/features/account-panel/index.tsx
Comment thread frontend/src/features/account-panel/index.tsx Outdated
Comment thread frontend/src/pages/account/index.tsx Outdated

@johnnyzhang-eng johnnyzhang-eng left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

邀请码没有使用次数上限。used_countquota/service.py 只递增、没有任何地方校验,config/quota.py 里也没有对应字段。

注册链路是 register_by_email_create_credit_account(注册礼 register_gift_amount) + redeem_invite_code(双方各 invite_reward_amount),所以同一个码可以被无限次用来建号:每个新号自身拿到注册礼加被邀请奖励,邀请人每拉一个号再加一份。#351 合入后积分直接对应真实生成调用(generate_action_cost 一次 50),这条链路等于可以被无限复制成真实调用额度。

自邀检查 invite.user_id == user_id 挡的是「本人兑换自己的码」,挡不住「新注册的号填老号的码」—— 那恰好是设计流程本身。#326 当初关闭注册要控的就是这类敞口,而产品现在已经对外,建议合入前给 used_count 加一个可配上限,或给邀请奖励加总量/每日上限。

其余读下来没问题:with_for_update() 锁住了码行,InviteRecord.invitee_id 的唯一约束兜住并发重复兑换,邀请码 8 位 32 字母表经 secrets 生成,测试覆盖了自邀、重复填写、双方到账与碰撞重试。

邀请奖励页和工作台提示已分别由 1024XEngineer#3621024XEngineer#363 承担,本 PR 只保留后端接口与 OpenAPI。
@xiaocheny214

Copy link
Copy Markdown
Contributor Author

邀请码没有使用次数上限。used_countquota/service.py 只递增、没有任何地方校验,config/quota.py 里也没有对应字段。

项目冷启动阶段,需要用户。如果前期就对邀请码的使用次数增加限制的话,那么会让用户反感甚至远离我们的产品,这有点得不偿失。

@minorcell

Copy link
Copy Markdown
Member

邀请码没有使用次数上限。used_countquota/service.py 只递增、没有任何地方校验,config/quota.py 里也没有对应字段。

注册链路是 register_by_email_create_credit_account(注册礼 register_gift_amount) + redeem_invite_code(双方各 invite_reward_amount),所以同一个码可以被无限次用来建号:每个新号自身拿到注册礼加被邀请奖励,邀请人每拉一个号再加一份。#351 合入后积分直接对应真实生成调用(generate_action_cost 一次 50),这条链路等于可以被无限复制成真实调用额度。

自邀检查 invite.user_id == user_id 挡的是「本人兑换自己的码」,挡不住「新注册的号填老号的码」—— 那恰好是设计流程本身。#326 当初关闭注册要控的就是这类敞口,而产品现在已经对外,建议合入前给 used_count 加一个可配上限,或给邀请奖励加总量/每日上限。

其余读下来没问题:with_for_update() 锁住了码行,InviteRecord.invitee_id 的唯一约束兜住并发重复兑换,邀请码 8 位 32 字母表经 secrets 生成,测试覆盖了自邀、重复填写、双方到账与碰撞重试。

邀请码一般会有使用次数、有效期、状态等字段。不过现在还好没有后台什么的,也还没有运营需求。

冷启动不加兑换次数上限。并发轮换用 FOR UPDATE 串行化;invitee 唯一约束冲突返回已填写过邀请码。邀请双方奖励默认改为 200,与账号中心 PR 对齐。
注册不再返回「请填写邀请码」:前端从链接传入 invite_code,空码和非法字符统一为邀请码无效。
注册无邀请码只发注册赠送;有有效邀请码再发双方奖励。login-by-code 对未知邮箱建号。注册赠送默认改为 300,邀请奖励保持 200。
去掉登录后补填;邀请码 30 天过期,轮换插入新行并截断旧码有效期。
未过期邀请码仍可继续被使用并写入关系;邀请人每个 UTC 日最多入账 600 分,超出只跳过邀请人奖励。
@xiaocheny214 xiaocheny214 changed the title feat(auth): 用邀请码重新开放注册 feat(auth): 开放公开注册,邀请码选填 Aug 18, 2026

@minorcell minorcell left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

够效率的 👍👍👍

@minorcell
minorcell merged commit 1c8ee09 into 1024XEngineer:main Aug 18, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(auth): 开放邀请码注册,补齐填写邀请码接口

3 participants