# 架构决策：门店开通、一次性员工邀请与操作审计

> 日期：2026-08-02  
> 状态：Accepted / Code Complete，待生产迁移与微信真机验收  
> Owner：Backend Core + Store Miniapp FE + Store Admin FE  
> Review：Data Model / Architecture / Access & Privacy = Yes

## 1. 决策背景

旧流程把 `Store.invite_code` 当作永久共享凭证，公开员工注册只凭 8 位码和手填手机号即可创建 staff；老板注册也接受未核验手机号和明文密码。Admin/小程序的“直接创建员工”还生成无法安全交付的初始密码。该模型不满足真实试点的身份核验、撤销、有效期、审计和离职失效要求。

本批目标不是扩展组织/权限系统，而是让 3～5 家独立单店能自行完成开通，并让受邀员工本人安全加入。

## 2. 已冻结决策

### 2.1 门店开通

- 新老板只走 `POST /api/onboarding/register-boss`，服务端用微信 `phoneCode` 换取并核验手机号；不接受手填手机号或明文密码。
- 新门店状态为 `in_progress`；历史门店无法可靠判断是否已完成开通，迁移统一写 `unknown`。
- 开通清单四步：门店资料、预约容量、服务项目为必填；邀请员工为选填，支持单人门店。
- 只有 boss 可显式完成；完成回执保存 `completed_at/by` 并写 AuditLog。清单不阻断已有业务接口，避免历史门店升级即停摆。

### 2.2 员工邀请

- legacy `invite_code` 仅保留数据库兼容，不返回 Admin/小程序，不再参与查询或注册。
- 老板创建 `StaffInvitation`：姓名、本人微信手机号、1～30 天有效期。
- token 使用 32 字节 `SecureRandom`（256-bit），原文只在创建响应返回一次；数据库仅保存 SHA-256。
- 邀请状态：`pending → accepted`，或 `pending → revoked/expired`；只能首次接受，已接受不能撤销。
- 预览只返回门店名、受邀姓名、脱敏手机号、状态和有效期。
- 接受时必须由微信换得的手机号与邀请绑定手机号精确匹配；成功后才创建 staff、消费邀请并签发 session。
- 已存在 User/openid/unionid 冲突均拒绝。本期 User 仍是单角色模型，不把 customer 静默升级为 staff；试点遇到冲突时使用独立工作手机号，后续如有真实频次再引入 Account + StoreMembership。

### 2.3 会话与离职

HMAC 签名和过期校验之后，每次受保护请求继续读取活跃 User，校验账号未删除且 `role/storeId` 与 token 一致；boss/staff 还必须归属仍有效的 Store。删除员工、停用门店或权限范围变化后，旧 token 立即 401，无需等待 7 天到期。

### 2.4 AuditLog 与 BusinessEvent

- `BusinessEvent` 记录可统计经营事实；本批补齐 `store_registered`。
- `AuditLog` 记录门店开通/设置与员工权限操作，只追加、不更新/软删。
- Audit metadata 只允许服务端白名单标量，禁止手机号、token/URL、openid/unionid、密码/密钥、IP、地址/坐标、备注/内容和原始请求体。

## 3. API 契约

| 方法 | 路径 | 权限 | 关键语义 |
|------|------|------|----------|
| POST | `/api/onboarding/register-boss` | public + 微信手机号核验 | 原子创建门店/老板，返回 session |
| GET | `/api/admin/onboarding` | boss/staff + 本店 | 计算开通清单 |
| POST | `/api/admin/onboarding/complete` | boss + 本店 | 必填项齐全后显式完成 |
| POST | `/api/admin/staff-invitations` | boss + 本店 | 原始 token 仅本响应一次 |
| GET | `/api/admin/staff-invitations` | boss + 本店 | 脱敏列表，无 token/hash |
| DELETE | `/api/admin/staff-invitations` | boss + 本店 | 撤销 pending 邀请 |
| GET | `/api/staff-invitations/preview` | public | 脱敏预览 |
| POST | `/api/staff-invitations/accept` | public + 微信手机号核验 | 手机号匹配才创建 staff |

旧 `/api/user/register-boss`、`/api/user/register-staff`、`/api/user/create-staff`、`/api/store/register`、`/api/store/invite-code` 保留兼容入口但只返回明确 410 业务码，不产生写入。

## 4. 数据迁移与不变量

迁移：`backend/db/migrations/20260802_create_store_onboarding.sql`，在前五个迁移之后第六个执行。

新增 6 项生产只读不变量，总数由 18 增至 24：

1. 门店开通状态枚举合法；
2. completed 必须有完成回执；
3. unknown/in_progress 不得带完成回执；
4. 邀请状态枚举合法；
5. accepted 与接受回执一致；
6. revoked 与撤销回执一致。

## 5. 前端行为

- Admin 设置页顶部显示开通清单，员工页改为邀请中心；创建后必须立即复制，之后只能撤销并重建。
- 小程序老板可直接用 `open-type=share` 转发带受控 path 的邀请卡片。
- 员工从卡片进入可先匿名预览，再点击微信手机号授权接受。
- H5 允许预览，但不会提供绕过微信手机号核验的接受路径。

## 6. 明确不做

- 不引入多门店组织、复杂 RBAC、StoreMembership、审批流或通用 IAM。
- 不接入短信生产登录，不恢复明文密码交付。
- 不调用微信 URL Link/小程序码后台 API；试点先使用小程序原生分享卡片，Admin 提供复制消息作为补充。
- 不自动执行生产迁移、不改生产数据。

## 7. 回滚

应用回滚只切回前一 backend/Admin/H5/小程序版本；数据库新增列/表/索引保留，不执行 down migration，不覆盖已经产生的邀请、审计或员工账号。旧应用会忽略新增结构，但 legacy 注册仍应保持关闭，避免重新暴露永久凭证路径。
