---
url: /docs/modules/auth-user.md
---
# 模块手册 · 认证与用户域（auth-user）

> 收录 **3** 个模块的权威开发手册（源自各模块 `CLAUDE.md`，随仓库同步）。

## 本文目录

* 1. oauth
* 2. user
* 3. shared

***

## 1. oauth 模块

> 源：`src/app/oauth/CLAUDE.md`（随仓库同步）

## src/app/oauth/

> OAuth 双 token 登录认证域 — DDD 4 层架构，9 个 use case，P0 阶段完成。
> 业务码区间 `40101xxx`（顶层宽容归一化 → HTTP 200 + code）。

### 模块职责

OAuth 域负责 C 端用户的**短信验证码登录**与**设备绑定/会话续期**全链路，覆盖 9 个 use case：

| use case | 路径 | 角色 | 关键点 |
| --- | --- | --- | --- |
| `SmsSendUseCase` | `application/usecase/sms_send.py` | 匿名 | 发送短信码（Mock SMS fallback） |
| `SmsVerifyUseCase` | `application/usecase/sms_verify.py` | 匿名 | 校验短信码（不签发 token） |
| `SmsLoginUseCase` | `application/usecase/sms_login.py` | 匿名 | 短信码登录 + 自动注册 + 签发双 token + DeviceBinding upsert |
| `CaptchaUseCase` | `application/usecase/captcha.py` | 匿名 | 图形验证码生成/校验 |
| `BindPhoneUseCase` | `application/usecase/bind_phone.py` | 已登录 | 绑定手机号（实名场景） |
| `UnbindPhoneUseCase` | `application/usecase/unbind.py` | 已登录 | 解绑手机号 |
| `RotateTokenUseCase` | `application/usecase/rotate.py` | 已登录 | refresh\_token 换发新 access\_token |
| `CallbackUseCase` | `application/usecase/callback.py` | 已登录 | 三方登录回调（微信/Apple） |
| `LogoutUseCase` | `application/usecase/logout.py` | 已登录 | 注销 + 清 DeviceBinding |

合计 **9 use case**（P0 完整交付）。

### DDD 4 层结构

```
src/app/oauth/
├── domain/                              # 零 web 依赖（已被 scripts/check_layers.py 卡控）
│   ├── entities/
│   │   ├── captcha.py                   # Captcha + 校验状态机
│   │   ├── sms_code.py                  # SmsCode + VerifyResult 状态机
│   │   └── device_binding.py            # DeviceBinding（多设备会话上限）
│   ├── value_objects/
│   │   ├── phone.py                     # Phone（E.164 校验 + 脱敏）
│   │   ├── client_id.py                 # ClientId + DEFAULT_CLIENT_ID
│   │   └── captcha_id.py                # CaptchaId
│   └── exceptions.py                    # OAuthErrorCode 40101xxx + OAuthError
│
├── application/                         # use case 编排 + 端口契约
│   ├── ports/
│   │   ├── repositories.py              # SmsCodeRepo / DeviceBindingRepo / UserRepo ...
│   │   └── services.py                  # TokenServicePort / EventPublisherPort / SmsSenderPort
│   ├── factories/
│   │   └── create_oauth_usecases.py     # 装配入口（与 pickRepo 工厂协同）
│   └── usecase/                         # 9 use case
│       ├── sms_send.py / sms_verify.py / sms_login.py
│       ├── captcha.py
│       ├── bind_phone.py / unbind.py
│       ├── rotate.py
│       ├── callback.py
│       └── logout.py
│
├── infrastructure/                      # 适配器 + 双仓储
│   ├── persistence/
│   │   ├── inmemory_repositories.py     # 内存实现（dev / 单测）
│   │   ├── sqlmodel_repositories.py     # SQLModel + PostgreSQL（prod）
│   │   ├── redis_repositories.py        # Redis（短信码 / 限流 / 设备绑定）
│   │   ├── repository_factory.py        # pickRepo 工厂（V2_USE_PRISMA / DATABASE_URL）
│   │   ├── db.py                        # 异步 session 工厂
│   │   └── sqlmodel_models/             # orm.py + user_extensions.py
│   └── adapters/
│       ├── token_service.py             # RS256 JWT 双 token（ADR-0004）
│       ├── jwks.py                      # JWKS 公钥端点
│       ├── captcha_generator.py         # 图形验证码
│       ├── sms_sender_factory.py        # 自动 fallback Mock ↔ Tencent
│       ├── mock_sms_sender.py           # dev mock
│       ├── tencent_sms_sender.py        # 腾讯云 SMS
│       ├── rate_limiter.py              # 内存实现（dev）
│       └── rate_limiter_redis.py        # Redis 实现（prod）
│
└── interface/                           # FastAPI 路由 + 顶层归一化
    ├── api/
    │   └── oauth.py                     # OAuth 公开路由（/api/v1/oauth/*）
    ├── normalizer.py                    # 顶层宽容归一化（HTTP 200 + code 40101xxx）
    └── handlers.py                      # ExceptionHandler 注册顺序（OATH-M-006 教训修复）
```

### 关键依赖与配置

| 类别 | 文件 | 说明 |
| --- | --- | --- |
| JWT 算法 | `infrastructure/adapters/token_service.py` | RS256（ADR-0004），私钥/公钥分离 |
| 短信适配 | `infrastructure/adapters/sms_sender_factory.py` | `V2_USE_TENCENT_SMS=1` 切换真发；否则 Mock |
| 限流 | `infrastructure/adapters/rate_limiter_redis.py` | Redis 滑窗；`V2_USE_REDIS=1` |
| 双仓储 | `infrastructure/persistence/repository_factory.py` | `V2_USE_PRISMA=1` + `DATABASE_URL` |
| JWKS 端点 | `infrastructure/adapters/jwks.py` | `/.well-known/jwks.json` |

### 数据模型

* `Captcha` — captcha\_id + base64 image + 答案（一次性）
* `SmsCode` — phone + code + 有效期 + 重发冷却 + 尝试次数
* `DeviceBinding` — user\_id + device\_id + last\_seen + revoke\_token（多设备上限 N）
* `User`（跨域）— 由 `application/ports/services.py: UserRepositoryPort` 抽象

### 错误码（40101xxx）

完整列表见 `docs/internal-wiki/architecture/error-codes.md` 与 `src/app/oauth/domain/exceptions.py:OAuthErrorCode`。

### 测试与质量

* `tests/oauth/integration/test_e2e.py` — 短信登录 E2E（mock SMS）
* `tests/unit/domain/test_captcha_state_machine.py` — 状态机单测
* `tests/unit/domain/test_sms_code_state_machine.py` — 状态机单测
* `tests/unit/application/test_oauth_p1_usecases.py` — use case 单元
* `tests/integration/test_admin_oauth_integration.py` — 与 admin 共生集成
* 守卫：`scripts/check_layers.py` 卡 domain 零依赖；`scripts/check_exception_handlers.py` 卡注册顺序

### 常见问题 (FAQ)

1. **Mock SMS 怎么开启？** — 默认自动 Mock；设 `V2_USE_TENCENT_SMS=1` 走真发。dev 不需要任何配置。
2. **为什么是双 token？** — access（短期，RS256）+ refresh（长期，DeviceBinding 锚定）。多设备上限由 DeviceBinding 控制。
3. **顶层归一化为什么重要？** — OATH-M-006 教训：ExceptionHandler 注册顺序错误导致 OAuthError 漏归一化，统一以 HTTP 200 + code 40101xxx 返回。
4. **如何接入三方登录？** — `application/usecase/callback.py` 抽象；具体 Provider（微信/Apple）在 infrastructure/adapters/ 下扩展。

### 相关文件清单

* `docs/api-docs/v1/oauth/` — OpenAPI/接口文档
* `docs/internal-wiki/oauth-deep-dive/` — JWT 解剖 / 多设备会话 / Cookbook
* `docs/internal-wiki/architecture/oauth-implementation.md` — 实施总览
* `docs/internal-wiki/decisions/` — RS256 决策

### 变更记录 (Changelog)

| 日期 (Asia/Shanghai) | 版本 | 变更 |
| --- | --- | --- |
| 2026-09-16 11:37:50 | 0.1.0 | `/init-project` 首次沉淀模块 CLAUDE.md（含面包屑 + DDD 4 层 + 9 use case + FAQ） |
| 2026-09-15 22:00:00 | — | P0 完成（与根 0.2.0 一致）— 内容仅在 docs/api-docs/v1/oauth/ |

***

## 2. user 模块

> 源：`src/app/user/CLAUDE.md`（随仓库同步）

## src/app/user/

> user 模块 — V1 user 11 action 迁移（V2 DDD 4 层 + InMemory + 行为兼容），V2.0.14 落地。

### 模块职责

`user` 承接 C 端用户域（对齐 V1 `server/src/modules/user/`），**路由风格 = action 协议**（非 admin 的 REST 风格）：

* 入口：`POST /api/v1/user` + `body {action, payload}`（V1 `POST /api/user` 路径平移，协议 100% 兼容）
* 11 action：`completeOnboarding` / `resetOnboarding` / `bindApps` / `bindInviter` / `checkReportQuota` / `redeemMember` / `submitOnboarding` / `onboardingSuggestion` / `adoptPlanOption` / `precheckDeleteAccount` / `deleteAccount`
* 鉴权：全部需已登录（`Authorization: Bearer <access_token>` — access token 解码桥接 oauth token 服务）
* 错误码区段：**40301xxx**（与 OAuth 40101xxx / admin 40201xxx 三层隔离）
* HTTP 状态：**非** admin「默认 200」口径 —— 默认 400 / 401 / 404（V1 行为 100% 兼容）

### DDD 4 层结构

```
src/app/user/
├── domain/                        # 零 web 依赖
│   ├── entities/user.py           # User 实体 + UserDTO（25 字段，camelCase 输出契约）
│   ├── services/
│   │   ├── onboarding_service.py  # 纯函数：画像/目标/方案/安全边界（V1 onboarding 逐条对拍）
│   │   └── growth_level.py        # 成长等级派生（total_points_earned 驱动）
│   └── exceptions.py              # UserErrorCode (40301xxx) + cz_user_error_to_http_status
│
├── application/
│   ├── ports/
│   │   ├── repositories.py        # UserRepositoryPort / TeamRepositoryPort / ReportQuotaPort /
│   │   │                          # BodyCompositionRepositoryPort / AccountDeletionPort / UserCachePort
│   │   └── services.py            # PasswordHasherPort / AccessTokenDecoderPort
│   ├── usecase/                   # 11 use case（4 文件）
│   │   ├── onboarding.py          # complete / reset / submit / suggestion / adoptPlanOption（5）
│   │   ├── account_binding.py     # bindApps / bindInviter（2 — 邀请双方奖励）
│   │   ├── membership.py          # checkReportQuota / redeemMember（2）
│   │   └── account_deletion.py    # precheck / deleteAccount（2 — 个保法 + 苹果 5.1.1(v)）
│   └── factories/create_user_usecases.py   # 聚合工厂：注入 port → use_cases dict
│
├── infrastructure/
│   ├── adapters/access_token_adapter.py    # SimpleUserAccessTokenAdapter（oauth token 服务桥接）
│   └── persistence/inmemory_user_repositories.py  # 全 InMemory 实现 + seed_dev_fixtures
│
└── interface/
    ├── api/user_actions.py        # 11 action 分发 + pydantic payload 校验（V1 zod 语义）
    └── handlers.py                # cz_register_user_exception_handlers
```

### 对外接口（11 action 一览）

详细字段表见 [API Docs → user/actions](/api-docs/v1/user/actions)。

| action | 说明 | 关键错误码 |
|---|---|---|
| `completeOnboarding` / `resetOnboarding` | onboardingDone 状态切换 | 40301004 |
| `bindApps` | 绑定 phone/email/password/username（全局唯一防重） | 40301005 / 40301001 |
| `bindInviter` | 邀请裂变（邀请人 +7 天 +50 分封顶 90 天；被邀人 +3 天 +20 分） | 40301008 / 40301009 |
| `checkReportQuota` | 报告免费周配额（会员不限 quota=-1 / 免费每周 1 次） | 40301004 |
| `redeemMember` | 积分兑换会员（7 天=100 分 / 30 天=300 分，白名单） | 40301007 / 40301006 |
| `submitOnboarding` | 存资料 + 分型 + 画像（不标 done）；写 BodyComp 失败不阻塞 | 40301013 |
| `onboardingSuggestion` | 健康目标 + 2~3 套方案（供选不自动采纳） | 40301010 |
| `adoptPlanOption` | 采纳方案（**服务端二次校验防伪造 deficit** — M4-15 修复） | 40301011 |
| `precheckDeleteAccount` | 注销前置检查（积分/会员 blockers + 数据概览） | 40301004 |
| `deleteAccount` | 注销（precheck 兜底 + 级联清理，**不可逆**） | 40301012 |

### 关键设计约束

#### 响应序列化（V1 契约）

* route 层 `cz_serialize()`：dataclass 字段 **snake\_case → camelCase**；datetime → ISO
* 信封 `{code: "0", msg: "ok", data}`（V2 全局）
* payload 校验用**手动 pydantic**（`cz__validate` → `UserError(PARAM_INVALID, "<字段路径>: <原因>")`，对齐 V1 zod parse 语义）

#### 缓存失效锚点

V1 `Cache.del(user:me:{userId})` 等价物 = `UserCachePort.cz_del(user_id)` ——
**全部写 action 落库后必调**（InMemory 无 IO 也要调，保持行为一致）。

#### 事务语义（InMemory 阶段）

* `bindInviter`：V1 单事务 → V2「前置校验 + 顺序编排」模拟（cap 超限在 team.create 之前拦截）
* `deleteAccount`：v2 子集级联（body compositions + team + user）；生产 Prisma 阶段补全 V1 22 表清单

#### 邀请奖励常量（V1 逐字对齐）

`CZ_INVITER_BONUS_DAYS=7` / `CZ_INVITEE_BONUS_DAYS=3` / `CZ_INVITER_CAP_DAYS=90` /
`CZ_INVITER_BONUS_POINTS=50` / `CZ_INVITEE_BONUS_POINTS=20`；套餐 `REDEEM_PACKAGES=((7,100),(30,300))`。

### 测试与质量

| 文件 | 范围 |
|---|---|
| `tests/integration/test_user_actions.py` | 11 action E2E（含错误分支）354 行 |
| `tests/unit/application/test_user_usecases.py` | use case 单元 |
| `tests/unit/domain/test_user_onboarding_service.py` | onboarding 纯函数（画像/目标/方案/安全边界） |

质量门禁与全仓一致：覆盖率 ≥ 80% / Ruff + mypy strict / DDD 4 层守卫。

### FAQ

**Q1: 为什么 user 模块用 action 协议而不是 REST 风格？**
A: V1（`server/src/modules/user/user.routes.ts`）是 action 风格，金标准 = V1 行为 100% 兼容 → 小程序客户端契约不变是硬约束（协议由 nginx 灰度按 path 切流定位 V1/V2）。admin 是新建后台无历史包袱，才用 REST。

**Q2: 为什么错误码从 40301xxx 开始？**
A: 三层隔离约定：OAuth 40101xxx / admin 40201xxx / user 40301xxx（聚合错位，永不冲突）。设备线延续：honor 40401xxx / zepp 40402xxx / device 40403xxx。

**Q3: `adoptPlanOption` 为什么不信前端传的 deficit？**
A: M4-15 修复 —— 未成年 / BMI<18.5 / C 类用户有安全边界（不得强加热量缺口）。服务端重新生成 suggestion 取该方案值（`PLAN_OPTION_INVALID` 兜底方案已不适用）。

### 相关文件

```
src/app/user/                         ← 本文件所在模块
├── domain/{entities/user.py, services/{onboarding_service,growth_level}.py, exceptions.py}
├── application/{usecase/ 4 文件 = 11 use case, ports/, factories/create_user_usecases.py}
├── infrastructure/{adapters/access_token_adapter.py, persistence/inmemory_user_repositories.py}
└── interface/{api/user_actions.py, handlers.py}

tests/integration/test_user_actions.py
tests/unit/{application/test_user_usecases.py, domain/test_user_onboarding_service.py}
docs/api-docs/v1/user/actions.md       ← 11 action 字段表（对外契约）
```

### 变更记录 (Changelog)

| 日期 (Asia/Shanghai) | 版本 | 变更 |
| --- | --- | --- |
| 2026-09-16 | 1.0.0 | 模块 CLAUDE.md 首版 — 11 action + DDD 4 层 + 40301xxx + V1 兼容约束 |
| 2026-09-16 | — | V2.0.14：V1 user 模块 11 action 迁移落地（commit bbd0d1e） |
| 2026-09-17 | — | zone.py 心率区间三算法迁移（V1 zone.util.ts 逐行对拍 — hrr/mhr/lthr ×5 区 + Tanaka；domain/services/zone.py + 63 用例；V1 中该 util 为 Sigma 回滚后孤儿，纯函数先行移植） |

***

## 3. shared 模块

> 源：`src/app/shared/CLAUDE.md`（随仓库同步）

## src/app/shared/

> 跨模块共享层 — Tenancy 上下文 + Celery 异步任务 + 基础设施复用。
> 不含业务 use case，仅承载**横向能力**与**异步副作用**。

### 模块职责

`shared` 是被 `oauth` / `admin` 等业务模块复用的"地基"层，专注于四件事：

1. **Tenancy 上下文** — 多租户 schema 隔离（ADR-0003）
2. **Celery 异步任务** — 审计日志 / 短信发送 / 验证码清理
3. **Celery 应用工厂** — Redis broker + structlog 集成
4. **可观测性（Phase 4 §7.6）** — audit 批量写入缓冲 + Prometheus 指标 + 慢操作上报 + Sentry 可选接入

任何业务 use case 都不应直接落在 shared；它只提供"轮子"，不提供"业务"。

### 目录结构

```
src/app/shared/
├── celery_app.py                       # Celery 应用工厂（cz_create_celery_app / cz_get_celery_app）
├── system_codes.py                     # 系统级业务码区段（5xxxxxxx — 慢操作 / 未捕获异常）
├── observability/                      # 可观测性横切能力（MIS Phase 4 §7.6）
│   ├── audit_batch.py                  # CzAuditBatchWriter — audit 异步批量写入（缓冲层）
│   ├── metrics.py                      # Prometheus 指标注册表 + cz_track_result 装饰器
│   ├── middleware.py                   # HTTP 耗时直方图 + 慢操作上报（SLOW_REQUEST_MS）
│   └── sentry.py                       # Sentry 可选接入（SENTRY_DSN 存在才 init）
├── tenancy/
│   ├── __init__.py
│   └── tenant_context.py               # TenantContext（async contextvar）
└── tasks/
    ├── __init__.py
    ├── audit_log.py                    # 审计日志写入（admin 高敏感动作触发）
    ├── sms_send.py                     # 短信发送（异步解耦，OAuth/Admin 复用）
    └── captcha_cleanup.py              # 验证码过期清理（定时任务）
```

### 四大子能力

#### 1. Tenancy 上下文（`tenancy/`）

* `tenant_context.py:TenantContext` — async contextvar 持有当前请求的 schema / tenant\_id
* 与 ADR-0003（多租户 schema 隔离）配套
* Alembic 迁移链 `alembic/env.py` 异步 + 多租户 schema 循环依赖此 context

#### 2. Celery 异步任务（`tasks/`）

| 任务 | 触发方 | 说明 |
| --- | --- | --- |
| `audit_log` | admin P0+ use case | 高敏感动作（rollback/close 等）落 audit log |
| `sms_send` | oauth + admin | 真发腾讯 SMS（带结构化日志） |
| `captcha_cleanup` | 定时 beat | 清理过期验证码（Redis TTL 兜底） |

#### 3. Celery 应用工厂（`celery_app.py`）

* `cz_create_celery_app(name="qm-serverv2")` — 创建 Celery 实例（Redis broker + result backend + 任务基类配置）
* `cz_get_celery_app()` — 单例获取工厂（避免 import 循环）
* 集成 `structlog` 信号（`task_prerun` / `task_postrun` / `task_failure`）
* 自动 include：`src.app.shared.tasks.{audit_log, sms_send, captcha_cleanup}`

#### 4. 可观测性（`observability/` — MIS Phase 4 §7.6）

| 模块 | 能力 | 关键点 |
| --- | --- | --- |
| `audit_batch.py:CzAuditBatchWriter` | audit 异步批量写入（包装而非替换 — 缓冲层） | eager（默认 dev/test，读路径等价直写）/ batched（`V2_AUDIT_BATCH=1` 生产：阈值 50 条或 1s flush）；逐条失败隔离 + `audit_log_write_total{result}` 计数；关机经 main.py shutdown 钩子 `aclose()` 终刷 |
| `metrics.py` | Prometheus 指标注册表 | `admin_user_ban_total` / `admin_order_refund_amount_yuan_total` / `admin_withdrawal_payout_total` / `http_request_duration_seconds` / `slow_operations_total` / `audit_log_write_total`；`cz_track_result` 装饰器零侵入埋点（domain 层禁依赖 prometheus） |
| `middleware.py` | 慢操作上报 | 每请求观测耗时直方图（path\_template = 路由模板，404 归并 `__unmatched__`）；> `SLOW_REQUEST_MS`（默认 1000）→ structlog 警告（系统码 50001002）+ `slow_operations_total` + Sentry |
| `sentry.py` | Sentry 可选接入 | env `SENTRY_DSN` 存在且 SDK 已装（`uv sync --extra observability`）才 init；无 DSN 零副作用；上报失败静默 |
| `system_codes.py` | 系统级业务码（5xxxxxxx 区段） | `SYSTEM_INTERNAL=50001001` / `SYSTEM_SLOW_OPERATION=50001002` / `SYSTEM_AUDIT_WRITE_FAILED=50001003` / `SYSTEM_DEPENDENCY_UNAVAILABLE=50001004`（admin-mis-design.md §6.1 区段） |

接线点：`src/main.py`（中间件注册 + `/metrics` 端点 + audit 缓冲层装配 + shutdown flush）。
压测基线报告：`docs/internal-wiki/operations/perf-baseline-v2.0.12.md`（`scripts/locustfile.py` 场景）。

### 关键约束

* **零业务依赖**：shared 不允许 import `src/app/oauth/` 或 `src/app/admin/` 的 use case
* **Celery 命名空间前缀**：使用 `cz_` 前缀避免与 cz\_ 前缀 agent 冲突（见 `celery_app.py` 注释）
* **不动现有 oauth 代码**：见 `celery_app.py` 顶部 `⚠️` 警告
* **可观测性依赖方向**：`shared.observability` 用 Protocol 结构化契约（`CzAuditWritePort`）包装 admin 端口 — 不反向 import admin；domain 层零 prometheus 依赖（check\_layers 卡控）

### 相关测试

* `tests/shared/test_celery.py` — Celery 应用工厂单测（broker/backend/任务注册）
* `tests/unit/shared/test_tenant_context.py` — TenantContext 上下文隔离
* `tests/unit/shared/test_observability_{audit_batch,metrics,middleware,sentry}.py` — Phase 4 可观测性单测（26 例）
* `tests/integration/test_observability_endpoints.py` — `/metrics` + 中间件端到端（真装配）

### 常见问题 (FAQ)

1. **新增共享任务放哪里？** — `src/app/shared/tasks/<name>.py`，并在 `celery_app.py:include` 注册。
2. **业务模块如何发异步？** — 通过 `cz_get_celery_app()` 获取实例后 `.send_task()` 或 `@shared_task` 装饰器。
3. **Tenancy 怎么传递到 SQLAlchemy session？** — 由 `tenant_context` 注入 `search_path`，Alembic env.py 异步循环保证 schema 存在。
4. **audit 批量写入为什么默认 eager？** — 存量集成测试断言 read-after-write（`records_by_action` 请求返回后立即可读）；eager 保读路径语义不变，生产以 `V2_AUDIT_BATCH=1` 启用真批量。读路径 `list_all()` 会穿透队列（审计闭环不漏）。
5. **新增业务指标怎么埋点？** — 在 `metrics.py` 注册 Counter/Histogram，use case 里 `@cz_track_result(<counter>)`（成功/失败双计数）或直接 `.inc()`；测试用 `cz_sample_value(name, labels)` 增量断言。

### 变更记录 (Changelog)

| 日期 (Asia/Shanghai) | 版本 | 变更 |
| --- | --- | --- |
| 2026-09-16 | 0.2.0 | MIS Phase 4：新增 `observability/` 四模块 + `system_codes.py`；audit 批量写入接线 main.py（eager 默认）+ `/metrics` 端点 + 慢操作上报 |
| 2026-09-16 11:37:50 | 0.1.0 | `/init-project` 首次沉淀模块 CLAUDE.md（含面包屑 + 三子能力说明 + FAQ） |
