ShopSystem Day 7 把”用户注册 → 商家升级”完整链路打通。之前商家申请是匿名提交(无账号关联,审核通过后无法升级 role),Day 7 强制要求先注册成为会员(role=customer),审核通过后同一账号角色升 merchant——一个用户一个账号,role 是字段不是身份。海外部署的注册方式分阶段做,Day 7 只做”邮箱密码”,Magic Link / Google / Apple 留到 Day 8-10。

注册 API `POST /api/auth/register` 是单接口闭环:① 限流 5 次/分钟/IP(防批量注册垃圾账号,新加的 register scope 走 60 秒窗口而非 1 秒),② 字段校验(邮箱正则 / 密码 ≥6 位 / 姓名 ≥2 字),③ email 唯一性查重(409 EMAIL_EXISTS 提示”该邮箱已注册,请直接登录”),④ bcrypt 加密(10 rounds),⑤ INSERT users(role=’customer’, status=’active’),⑥ 调 `createSession` 自动登录 + 写双 cookie(shop_user_id + shop_sess)。注册成功返回 `{ok:true, redirect:’/account’, user:{…}}`,前端跳 redirect 参数。`/register` 页用 Server Component 包 Client Component `RegisterForm`(用 `useSearchParams` 需要 Suspense boundary,build 时踩了 useSearchParams 报错,改用 `useSearchParams` 必须包 Suspense 的方式);表单字段:邮箱 / 姓名 / 手机(选填) / 密码 / 确认密码,密码输入下方实时显示强度条(长度 + 大小写 + 数字 + 特殊字符 综合打分 弱/中/良/强),前端 clientValidate 拦截明显错误再发请求,服务端二次校验防绕过。`?redirect=` 参数透传:从 `/merchant-apply` 跳到 `/register?redirect=/merchant-apply`,注册成功后跳回 `/merchant-apply` 自动继续申请。

SiteHeader 未登录状态从原本的”登录”链接升级为”登录 | 注册”两件套——注册按钮走 accent 黄色实色突出(和登录的幽灵按钮形成主次对比),已登录状态不变。SiteFooter 在”购物指南”列加”注册账号”链接,版本号从”Day 3 视觉风格上线”改成”Day 7 用户注册上线”。`/merchant-apply` 强制登录:API 入口加 `getOptionalUser()` 检查,未登录返回 401 `code:NEED_REGISTER` + 错误信息”请先注册成为会员再申请商家”;页面用 Client Component 在 useEffect 里 fetch /api/me 探测登录状态,未登录直接 `router.replace(‘/register?redirect=/merchant-apply’)` 跳注册页(同时显示 loading 占位避免空白闪烁),已登录自动填充姓名/邮箱(从 /api/me 返回的 user 拿),submit 时如果遇到 401 NEED_REGISTER 也跳注册页(防 cookie 过期)。

商家审核 API `/api/admin/merchants/[id]/review` 兼容老申请:Days 4-6 老申请 `applicant_user_id` 是 NULL(Day 7 前允许匿名申请),审核通过不会升级 role(没账号可关联),新加 `roleUpgraded: boolean` 字段 + 详细 message 告诉 admin “该申请无关联账号,请联系申请人用邮箱注册账号后手动关联”。新申请(Day 7 后)走 `/api/merchant-apply` 强制登录检查,`applicant_user_id` 必有值,审核通过会 `UPDATE users SET role=’merchant’` 自动升级。同账号同状态 ‘pending’ 不允许重复申请,DB 加了 ALREADY_PENDING 提示防 spam。Day 7 收工踩了 3 个坑:① `useSearchParams()` 在 build 时报”should be wrapped in a suspense boundary”,用 `
OpenClaw—AI研究