
“AI 软件开发实战教程”系列第 9 篇:用 TDD 完成社区邀请码、登录会话、账户限制与私密资料加密,再让 375px 的真实浏览器走完第一条用户流程。
上一篇完成了工程骨架。测试能运行、浏览器能打开页面、生产配置会尽早失败,但产品还不能让任何真实成员进入。
这一次开始第一个业务任务 K1:
收到社区邀请链接 → 注册登录账号和密码 → 直接进入信息大厅先浏览 → 设置微信号和喵码 → 用假渠道确认提醒状态 → 退出后不能继续查看社区内容它还没有发布和匹配,却已经是一条完整的用户流程。每一步都同时涉及产品规则、权限、安全、页面和测试,正适合展示纵向 TDD 与“先把所有数据表建完”的区别。
1. 先把产品句子变成失败测试
K1 负责的产品验收并不是一句“实现登录”。它至少包含:
有效邀请码可以注册,无效、停用、耗尽和过期邀请码不能注册; 两个顺风车群的邀请最终加入同一个社区; 注册只要求登录账号和密码,不强迫立即填写联系方式; 未登录不能查看社区信息; 受限或正在删除的账户不能登录,也不能靠旧会话继续访问; 登录错误不能透露账号是否存在; 登录账号不会展示给其他成员; 微信号和喵码加密保存,页面与 Admin 不回显原文; 修改喵码以后,以前的“已测试”状态立即失效。
第一批测试先导入尚不存在的社区和私密资料模块。运行结果是:
ModuleNotFoundError: No module named 'apps.communities'ModuleNotFoundError: No module named 'apps.accounts.services'这一次红灯明确说明目标模块还不存在。随后才创建模型、服务、页面和迁移。
2. 邀请码不是数据库里的一串明文
如果数据库直接保存邀请码,管理员后台、备份或一次误打日志就可能把仍然有效的注册链接全部暴露。
邻行采用和密码相似的思路:
随机生成邀请码 → 与服务端密钥做 HMAC-SHA256 → 数据库只保存 64 位摘要 → 明文注册链接只在签发时输出一次验证邀请时,对用户提交的明文做同样摘要,再查找对应记录。数据库不需要、也不能恢复原邀请码。
邀请码还保存:
所属社区; 来源群; 启用或停用状态; 有效期; 最大使用次数和已使用次数。
注册服务在数据库事务中锁定邀请、重新检查状态、创建用户并增加使用次数,设计目标是让同一张只允许使用一次的邀请不能被并发正常消费两次。当前自动测试已经覆盖顺序消费和耗尽拒绝;真正的 PostgreSQL 双连接并发证明仍要在具备 PostgreSQL 的环境补做,不能用 SQLite 测试替代。
3. 为什么需要单独的签发命令
最小实现完成后出现了一个实际可用性问题:Admin 可以看到邀请记录,却没有办法安全地产生“只显示一次”的明文。
如果允许管理员自己填写摘要,他必须先在别处计算,既容易出错,也会鼓励把明文复制进不受控工具。
因此增加管理命令:
python manage.py issue_invite <社区编号> <来源群>命令生成随机邀请码,只打印一次完整邀请路径;Admin 禁止直接新建邀请记录,只负责查看状态和停用。测试同时证明:输出中有邀请链接,数据库中只有摘要,摘要本身不会被当成可用链接输出。
工程安全不仅是“数据库用了哈希”,还要保证日常操作不会逼人绕过安全设计。
4. 注册账号和微信号必须是两个概念
用户可以把登录账号填写成与微信号相同的内容,但系统仍然把它们当作不同资料:
登录账号用于认证; 微信号只在合法候选交换后披露; 喵码只用于提醒渠道。
注册页面因此只包含登录账号、密码、确认密码和产品边界确认。账号帮助文字明确说明不会展示给其他成员。
注册成功后直接进入信息大厅,不弹出“还差两项资料”阻断浏览。这个决定来自前面的产品规划:用户可以先看看社区里有没有有价值的信息,第一次发布前再补齐联系方式和提醒。
K1 只证明“先浏览”和“能够完成资料准备”。真正阻止资料不完整的首次发布,要等 K2 有发布入口、K7 有真实提醒状态后才能完整验收。
5. 登录错误不能帮攻击者确认账号
登录流程支持:
登录成功后轮换会话; 打开受保护深层链接时,登录后安全返回原页面; 外部网址不能伪装成 next参数把用户带走;退出使用 POST,使当前会话失效; 受限或删除中的账号使用与密码错误相同的提示; 账户状态在已有会话期间改变时,下一次请求立即退出。
错误统一显示:
账号或密码不正确不存在的账号、错误密码和受限状态不会得到三个不同答案。
架构审阅还要求限制连续失败。实现没有保存原始账号和 IP,而是把“规范化账号 + 来源地址”与服务端密钥做 HMAC 摘要,只保存摘要、窗口开始时间和失败次数。
同一组合五分钟内连续失败五次后,下一次返回稍后再试;成功登录会清除对应计数。这样可以限速,又不在额外安全表中复制登录账号。
6. 敏感字段加密不是简单调用一次 encrypt
微信号和喵码需要在合法流程中恢复原文,所以不能像邀请码一样只保存不可逆摘要。邻行使用 Fernet 认证加密,并保存当前密钥版本。
测试不只断言“字段看起来不像原文”,还直接查询数据库:
encrypted_wechat_id 不包含测试微信号encrypted_miaocode 不包含测试喵码随后打开设置页,断言 HTML 也不包含两项原文。页面只显示:
微信号:已设置 / 未设置微信提醒:未设置 / 等待测试 / 已测试可用喵码永远不回填到输入框。当前微信号也不完整回显,避免旁观者、页面插件或浏览器表单恢复无意读取旧值。
7. 密钥轮换暴露了一个重构陷阱
私密资料最初只有一个 key_version,微信号和喵码共享这个版本。
假设旧数据都用密钥 1 加密,系统切换到密钥 2 后,用户只修改微信号。如果代码只加密新微信号并把版本改成 2,旧喵码仍然是密钥 1 的密文,却被标成版本 2,之后无法解密。
这类错误在“保存后字段不是明文”的测试中不会出现。
因此增加轮换测试:
用密钥 1 保存微信号和喵码 → 切换活动密钥到 2 → 只修改微信号 → 微信号和未修改的喵码都必须能以版本 2 解密实现遇到旧版本时,先用旧密钥解密两个已有字段,再用活动密钥一起重加密,最后保存统一版本。
这说明 TDD 的价值不只是让当前页面可用,也能把未来运维动作转成现在可以重复证明的行为。
8. 假提醒为什么不传真实喵码
K1 需要让页面出现“已设置”和“已测试可用”状态,但真实喵提醒适配器属于 K7。
测试渠道接收的是内部私密资料引用:
private-profile:<内部编号>而不是喵码原文。它记录一条测试消息并返回成功,不访问外网,也不会把喵码写进测试结果。
这只能证明站内状态变化和页面流程正确,不能证明真实第三方的失败、超时或正文分类。AC-05 因此继续保持未实现,由 K7 用故障桩和真实渠道契约关闭。
9. 页面设计从 375px 开始
K1 同时实现了第一组真实页面:
产品说明页; 邀请注册页; 登录页; 暂时为空的信息大厅; 联系与提醒设置页; 邀请不可用状态页。
页面沿用已经批准的蓝绿色设计系统,使用服务端模板、原生 CSS 和少量浏览器能力。表单控件至少 48px 高,焦点清晰,错误靠文字表达,支持深色模式和减少动效偏好。
没有为了让“大厅看起来真实”伪造任何车主、乘客或成功案例。K2 尚未实现信息发布,所以大厅诚实显示空状态和下一步能力说明。
10. 浏览器测试为什么也会先失败
单元和 HTTP 测试通过以后,Playwright 使用 375×812 视口执行:
打开邀请链接 → 填写账号和密码 → 确认产品边界 → 注册进入大厅 → 打开联系与提醒 → 保存微信号和喵码 → 发送假测试提醒 → 退出 → 再访问大厅被要求登录第一次浏览器测试停在确认密码输入框。原因不是页面坏了,而是自动化查找“确认密码”,页面真实可访问名称是“密码确认”。失败截图清楚显示了页面状态。
修正定位器后,旧的主页冒烟测试又失败了:V0 时它寻找一整句纯文本标题,而现在页面已经有正式的品牌、眉题和主标题结构。
测试没有把页面改回纯文本,而是更新为检查文档标题和真实一级标题。
最终两条 Chromium 测试都通过,并额外断言注册页和设置页在 375px 下没有水平溢出。
11. 如何诚实更新验收矩阵
K1 完成后,验收状态不是全部变绿:
AC-01:邀请注册规则有服务、HTTP 和浏览器证据,自动通过; AC-02:当前大厅的未登录阻断有 HTTP 和浏览器证据,自动通过; AC-03:两个来源群进入同一社区,自动通过; AC-04:先浏览和资料设置已经通过,但首次发布门禁等待 K2/K7,部分通过; AC-40:当前页面不向其他成员展示登录账号,但未来页面尚未出现,等待 K9 全量扫描,部分通过; AC-05:真实提醒失败语义尚未实现,保持未实现。
任务可以完成,同时某些跨任务验收仍是部分通过。只要负责人、剩余证据和后续节点写清楚,这比为了让看板漂亮而提前宣称完成更可靠。
12. 本节点的结果
K1 收口时得到:
47 个非浏览器测试通过; 分支覆盖率 92.25%; Ruff、格式、mypy strict、迁移检查和 Django system check 通过; 两条 Chromium 测试通过; 生产部署检查零警告; 邀请码、登录账号、微信号和喵码没有进入不该出现的页面或数据库字段; WebKit 与微信真机仍明确未验证; 只创建本地提交,不推送。
13. 写在最后
第一条业务流程让项目第一次像一个真实产品,而不仅是能启动的工程。
但真正重要的并不是出现了注册页,而是每一层都在表达同一组产品决定:社区邀请控制边界,用户可以先浏览,登录身份不公开,联系方式只为后续合法交换准备,第三方提醒不是站内事实源。
下一篇进入 K2:结构化发布、信息大厅和详情。
那时将第一次处理用户真正要发布的日期、时间窗口、截止、路线、座位和分享文案,也会遇到“现在出发”、重复提交、过去时间和详情权限这些更接近业务核心的 TDD 场景。
14. 关键代码与操作
下面是敏感资料测试的简化摘录。示例值都是虚构数据,完整测试还会直接检查数据库密文:
deftest_private_values_are_not_rendered(client, user): update_private_profile( user=user, wechat_id="example-wechat", miaocode="example-reminder-code", ) client.force_login(user) content = client.get("/settings/profile/").content.decode()assert"example-wechat"notin contentassert"example-reminder-code"notin contentassert"已设置"in content验证命令:make bugfix TEST=tests/accounts/test_private_profile.py::test_private_contact_values_are_encrypted_and_not_rendered
这里同时验证“数据能够使用”和“秘密不会被页面回显”,不能只断言保存成功。
15. 本篇验证摘要
邀请码只保存不可逆摘要,并在事务中检查有效期、启用状态和剩余次数; 登录失败统一提示并限制连续尝试,不向攻击者透露账号是否存在; 微信号和喵码使用版本化加密保存,页面、Admin 和日志不回显原文; 移动浏览器已经走通邀请注册、先浏览、设置资料、测试提醒和退出流程; SQLite 已覆盖顺序消费,邀请码并发仍需 PostgreSQL 双连接补充证明。
16. 附录:相关工具与仓库
16.1 gstack
仓库:garrytan/gstack 地址:https://github.com/garrytan/gstack
16.2 dev-harness
仓库:Dev-Wiki/dev-harness 地址:https://github.com/Dev-Wiki/dev-harness
16.3 UI UX Pro Max Skill
仓库:nextlevelbuilder/ui-ux-pro-max-skill 地址:https://github.com/nextlevelbuilder/ui-ux-pro-max-skill
夜雨聆风