Files
junhong_cmp_fiber/openspec/specs/personal-customer/spec.md
huang 409a68d60b
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m45s
feat: OpenAPI 契约对齐与框架优化
主要变更:
1. OpenAPI 文档契约对齐
   - 统一错误响应字段名为 msg(非 message)
   - 规范 envelope 响应结构(code, msg, data, timestamp)
   - 个人客户路由纳入文档体系(使用 Register 机制)
   - 新增 BuildDocHandlers() 统一管理 handler 构造
   - 确保文档生成的幂等性

2. Service 层错误处理统一
   - 全面替换 fmt.Errorf 为 errors.New/Wrap
   - 统一错误码使用规范
   - Handler 层参数校验不泄露底层细节
   - 新增错误码验证集成测试

3. 代码质量提升
   - 删除未使用的 Task handler 和路由
   - 新增代码规范检查脚本(check-service-errors.sh)
   - 新增注释路径一致性检查(check-comment-paths.sh)
   - 更新 API 文档生成指南

4. OpenSpec 归档
   - 归档 openapi-contract-alignment 变更(63 tasks)
   - 归档 service-error-unify-core 变更
   - 归档 service-error-unify-support 变更
   - 归档 code-cleanup-docs-update 变更
   - 归档 handler-validation-security 变更
   - 同步 delta specs 到主规范文件

影响范围:
- pkg/openapi: 新增 handlers.go,优化 generator.go
- internal/service/*: 48 个 service 文件错误处理统一
- internal/handler/admin: 优化参数校验错误提示
- internal/routes: 个人客户路由改造,删除 task 路由
- scripts: 新增 3 个代码检查脚本
- docs: 更新 OpenAPI 文档(15750+ 行)
- openspec/specs: 同步 3 个主规范文件

破坏性变更:无
向后兼容:是
2026-01-30 11:40:36 +08:00

366 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# personal-customer Specification
## Purpose
TBD - created by archiving change add-personal-customer-wechat. Update Purpose after archive.
## Requirements
### Requirement: 短信验证码服务
系统 SHALL 提供短信验证码服务,对接行业短信平台,支持发送验证码到指定手机号,验证码存储在 Redis 中并设置过期时间。
#### 短信服务对接规范
**短信服务商**: 武汉聚惠富通(行业短信)
**接口网关**: `https://gateway.sms.whjhft.com:8443/sms`
**协议版本**: HTTP JSON API v1.6
**接口文档**: 参考 `docs/第三方文档/SMS_HTTP_1.6.md`
**使用接口**: 短信批量发送接口 `/api/sendMessageMass`
**发送方式**: 直接发送内容(不使用短信模板)
**短信内容格式**: `【签名】自定义内容`
- 签名部分(如 `【签名】`)需提前向服务商报备并审核通过
- 自定义内容为实际短信文本
- 示例: `【签名】您的验证码是1234565分钟内有效`
**请求参数规范**:
```json
{
"userName": "账号用户名(从配置读取)",
"content": "【签名】您的验证码是{验证码}5分钟内有效",
"phoneList": ["13500000001"],
"timestamp": 1596254400000, // 当前时间戳(毫秒)
"sign": "e315cf297826abdeb2092cc57f29f0bf" // MD5(userName + timestamp + MD5(password))
}
```
**Sign 计算规则**:
- 计算方式: `MD5(userName + timestamp + MD5(password))`
- 示例:
- `userName = "test"`
- `password = "123"`
- `timestamp = 1596254400000`
- `MD5(password) = "202cb962ac59075b964b07152d234b70"`
- `组合字符串 = "test1596254400000202cb962ac59075b964b07152d234b70"`
- `sign = MD5(组合字符串) = "e315cf297826abdeb2092cc57f29f0bf"`
**响应格式**:
```json
{
"code": 0, // 0-成功,其他-失败(参考响应状态码列表)
"message": "处理成功",
"msgId": 123456, // 短信消息ID用于后续追踪
"smsCount": 1 // 消耗计费数
}
```
**配置项** (需在 `config.yaml` 中添加):
```yaml
sms:
gateway_url: "https://gateway.sms.whjhft.com:8443/sms"
username: "账号用户名"
password: "账号密码"
signature: "【签名】" # 短信签名(需提前报备)
timeout: 10s
```
**错误处理**:
- `code=0`: 发送成功
- `code=5`: 账号余额不足(记录错误日志,返回用户友好提示)
- `code=16`: 时间戳差异过大(检查服务器时间)
- 其他错误码: 参考文档第13节"响应状态码列表"
**重要说明**:
- 本系统使用直接内容发送方式,不使用短信模板
- 请求中只需要 `content` 字段,不需要 `templateId``params` 参数
- 短信内容必须包含已报备的签名,格式为 `【签名】` + 自定义文本
#### Scenario: 发送验证码成功
- **WHEN** 用户请求发送验证码到有效手机号
- **THEN** 系统生成6位数字验证码存储到 Redis过期时间5分钟调用短信服务发送
#### Scenario: 验证码频率限制
- **WHEN** 用户在60秒内重复请求发送验证码
- **THEN** 系统拒绝请求并返回错误"请60秒后再试"
#### Scenario: 短信发送失败
- **WHEN** 短信服务返回错误(如余额不足、账号异常等)
- **THEN** 系统记录错误日志,返回用户友好提示"短信发送失败,请稍后重试"
#### Scenario: 验证码验证成功
- **WHEN** 用户提交正确的验证码
- **THEN** 系统验证通过并删除 Redis 中的验证码
#### Scenario: 验证码验证失败
- **WHEN** 用户提交错误的验证码
- **THEN** 系统返回错误"验证码错误"
#### Scenario: 验证码过期
- **WHEN** 用户提交的验证码已超过5分钟
- **THEN** 系统返回错误"验证码已过期"
---
### Requirement: 个人客户登录流程
系统 SHALL 支持个人客户通过 ICCID网卡号或 IMEI设备号登录首次登录需绑定手机号并验证。
#### Scenario: 已绑定用户登录
- **WHEN** 用户输入 ICCID/IMEI且该 ICCID/IMEI 已绑定手机号
- **THEN** 系统发送验证码到已绑定手机号,用户验证后登录成功
#### Scenario: 未绑定用户首次登录
- **WHEN** 用户输入 ICCID/IMEI且该 ICCID/IMEI 未绑定手机号
- **THEN** 系统提示用户输入手机号,发送验证码,验证后创建个人客户记录并登录
#### Scenario: 登录成功返回Token
- **WHEN** 用户验证码验证通过
- **THEN** 系统生成个人客户专用 Token 并返回
#### Scenario: ICCID/IMEI 不存在
- **WHEN** 用户输入的 ICCID/IMEI 在资产表中不存在
- **THEN** 系统返回错误"设备号不存在"(注:资产表后续实现)
---
### Requirement: 手机号绑定
系统 SHALL 支持个人客户绑定手机号,一个手机号可以关联多个 ICCID/IMEI即一个个人客户可以拥有多个资产
#### Scenario: 绑定新手机号
- **WHEN** 个人客户请求绑定手机号,且该手机号未被其他用户绑定
- **THEN** 系统发送验证码,验证后绑定手机号
#### Scenario: 手机号已被绑定
- **WHEN** 个人客户请求绑定的手机号已被其他用户绑定
- **THEN** 系统返回错误"该手机号已被绑定"
#### Scenario: 更换手机号
- **WHEN** 个人客户已有绑定手机号,请求更换为新手机号
- **THEN** 系统需要同时验证旧手机号和新手机号后才能更换
---
### Requirement: 微信信息绑定
系统 SHALL 支持个人客户绑定微信信息OpenID、UnionID用于后续的微信支付和消息推送。
#### Scenario: 微信授权绑定
- **WHEN** 个人客户在微信环境中授权登录
- **THEN** 系统获取并存储 OpenID 和 UnionID
#### Scenario: 微信信息更新
- **WHEN** 个人客户重新授权微信
- **THEN** 系统更新 OpenID 和 UnionID
#### Scenario: 查询微信绑定状态
- **WHEN** 请求个人客户信息时
- **THEN** 系统返回是否已绑定微信(不返回具体的 OpenID/UnionID
---
### Requirement: 个人客户认证中间件
系统 SHALL 提供独立于 B 端账号的个人客户认证中间件,用于 /api/c/ 路由组的请求认证。
#### Scenario: Token验证成功
- **WHEN** 请求携带有效的个人客户 Token
- **THEN** 中间件解析 Token在 context 中设置个人客户信息
#### Scenario: Token验证失败
- **WHEN** 请求携带无效或过期的 Token
- **THEN** 中间件返回 401 Unauthorized 错误
#### Scenario: 跳过B端数据权限过滤
- **WHEN** 个人客户认证成功后
- **THEN** 中间件在 context 中设置 SkipOwnerFilter 标记Store 层跳过 shop_id 过滤
#### Scenario: 公开接口跳过认证
- **WHEN** 请求访问 /api/c/v1/login 或 /api/c/v1/login/send-code
- **THEN** 中间件跳过认证,允许访问
---
### Requirement: 个人客户路由分组
系统 SHALL 将个人客户相关的 API 放在 /api/c/v1/ 路由组下,与 B 端 API/api/v1/)隔离。
#### Scenario: 登录相关接口
- **WHEN** 请求 POST /api/c/v1/login/send-code
- **THEN** 系统发送验证码(公开接口)
#### Scenario: 个人信息接口
- **WHEN** 请求 GET /api/c/v1/profile
- **THEN** 系统返回当前登录的个人客户信息(需认证)
#### Scenario: B端和C端隔离
- **WHEN** 个人客户 Token 访问 /api/v1/ 接口
- **THEN** 系统返回 401 UnauthorizedToken 类型不匹配)
---
### Requirement: OpenAPI 文档集成
个人客户 API SHALL 纳入项目的 OpenAPI 文档生成体系,使用统一的 `Register()` 机制注册路由。
#### Scenario: 路由注册纳入文档
- **WHEN** 个人客户路由使用 `Register()` 函数注册
- **THEN** 路由自动出现在生成的 OpenAPI 文档中
- **AND** 文档包含完整的请求/响应结构、认证信息和中文描述
#### Scenario: 文档标签分类
- **WHEN** 生成 OpenAPI 文档
- **THEN** 个人客户 API 使用 "个人客户 - 认证" 和 "个人客户 - 账户" 等中文标签分类
- **AND** 与后台管理 API 标签区分
#### Scenario: 响应格式统一
- **WHEN** 个人客户 API 返回响应
- **THEN** 使用统一的 envelope 格式:`{code, msg, data, timestamp}`
- **AND** 与后台管理 API 响应格式一致
**实现位置**: `internal/routes/personal.go`
**文档路径**: `/api/c/v1` 路由组在 `docs/admin-openapi.yaml` 中可见
---
### Requirement: 个人客户路由必须纳入文档体系
个人客户 API 路由注册 SHALL 使用 `Register(...)` 机制与其他路由admin、h5保持一致。
#### Scenario: RegisterPersonalRoutes 函数签名变更
- **WHEN** 定义 `RegisterPersonalRoutes` 函数
- **THEN** 函数签名为:
```go
func RegisterPersonalRoutes(doc *openapi.Generator, basePath string, handlers *bootstrap.Handlers)
```
- **AND** 不再接受 `*fiber.App` 参数
#### Scenario: 使用 RouteSpec 注册路由
- **WHEN** 在 `RegisterPersonalRoutes` 中注册路由
- **THEN** 使用 `doc.Register(openapi.RouteSpec{...})` 注册
- **AND** 每个路由包含完整的元数据Method, Path, Handler, Summary, Tags, Auth, Input, Output
#### Scenario: 路由路径保持不变
- **WHEN** 改造路由注册方式
- **THEN** 路由路径保持 `/api/c/v1/xxx` 格式
- **AND** 不修改路径结构
- **AND** 与现有客户端保持兼容
### Requirement: 个人客户 API 的文档元数据
个人客户 API 的 RouteSpec SHALL 包含中文 Summary 和统一的 Tags。
#### Scenario: Summary 使用中文描述
- **WHEN** 定义个人客户 API 的 RouteSpec
- **THEN** Summary 字段使用中文描述(如 "获取个人客户卡详情"
- **AND** 描述简洁明了(一行以内)
#### Scenario: Tags 统一为"个人客户"
- **WHEN** 定义个人客户 API 的 RouteSpec
- **THEN** Tags 字段包含 `["个人客户"]`
- **AND** 所有个人客户 API 使用相同的 tag
- **AND** 在 OpenAPI 文档中归类到同一分组
#### Scenario: Auth 字段正确设置
- **WHEN** 定义个人客户 API 的 RouteSpec
- **THEN** 需要认证的接口设置 `Auth: true`
- **AND** 无需认证的接口(如微信登录)设置 `Auth: false`
### Requirement: 个人客户路由在文档中可见
生成的 OpenAPI 文档 SHALL 包含所有个人客户 API 路由。
#### Scenario: 文档包含 /api/c/v1 路径
- **WHEN** 生成 OpenAPI 文档(`go run cmd/gendocs/main.go`
- **THEN** 生成的 `logs/openapi.yaml` 包含 `/api/c/v1` 路径
- **AND** 路径数量与 `RegisterPersonalRoutes` 中注册的一致
#### Scenario: 个人客户接口在文档中正确分组
- **WHEN** 查看生成的 OpenAPI 文档
- **THEN** 个人客户接口在 "个人客户" tag 下
- **AND** 与其他模块admin、h5分组隔离
#### Scenario: 接口元数据完整
- **WHEN** 查看个人客户接口的 OpenAPI 定义
- **THEN** 每个接口包含:
- Summary中文摘要
- Description详细说明如有
- Parameters路径参数、查询参数
- RequestBody请求体 schema
- Responses响应 schema包含 envelope
- Security认证要求
### Requirement: 个人客户 Handler 在文档生成器中注册
个人客户 Handler SHALL 在 `BuildDocHandlers()` 中构造。
#### Scenario: BuildDocHandlers 包含 PersonalCustomer
- **WHEN** 调用 `openapi.BuildDocHandlers()`
- **THEN** 返回的 `bootstrap.Handlers` 包含 `PersonalCustomer` 字段
- **AND** PersonalCustomer 使用 `personal.NewPersonalCustomerHandler(nil)` 构造
#### Scenario: 文档生成不执行 Handler 逻辑
- **WHEN** 为文档生成构造 PersonalCustomer handler
- **THEN** 所有依赖参数传入 `nil`
- **AND** 文档生成过程不会调用 handler 的实际业务逻辑
- **AND** nil 依赖不会导致 panic
### Requirement: 路由注册调用方式更新
`internal/routes/routes.go` 中对 `RegisterPersonalRoutes` 的调用 SHALL 传入正确的参数。
#### Scenario: routes.go 传入 doc 参数
- **WHEN** 在 `routes.go` 中调用 `RegisterPersonalRoutes`
- **THEN** 传入 `doc *openapi.Generator` 参数
- **AND** 传入 basePath如 `/api/c/v1`
- **AND** 传入 handlers
#### Scenario: 文档生成时调用 RegisterPersonalRoutes
- **WHEN** 文档生成流程调用路由注册
- **THEN** `RegisterPersonalRoutes` 被调用
- **AND** 个人客户路由被注册到文档生成器
- **AND** 不启动 Fiber 服务器
### Requirement: 向后兼容性
路由注册方式的改造 SHALL 保持 API 行为不变。
#### Scenario: 改造后 API 响应格式不变
- **WHEN** 改造路由注册方式
- **THEN** API 的响应格式与改造前一致
- **AND** 响应包含 envelope`{code, msg, data, timestamp}`
#### Scenario: 改造后路径不变
- **WHEN** 改造路由注册方式
- **THEN** 所有路径保持 `/api/c/v1/xxx` 格式
- **AND** 客户端无需修改请求 URL
#### Scenario: 改造后认证逻辑不变
- **WHEN** 改造路由注册方式
- **THEN** 认证中间件继续生效
- **AND** 需要认证的接口仍需提供有效 Token
- **AND** 认证失败时返回 401 错误
---