docs(change-password): 合并 7 个 second-pass issues

- 3.1 序列图:补 GetByID 调用,与 §4.3 一致
- 4.2: 加 call-site 审计 grep 命令 + 已确认 9 文件列表
- 4.6: 补 mocking 模式代码示例 + Test #9 错误类型规约
- 5.3.3: 解释手动 removeStorageSync 的原因(store 不清 temp_register_*)
- 5.3.4: 补 updatePasswordApi 调用点审计(2 文件已确认)
- 9: 补 AuthMiddleware 已挂载,前端凭 token 即可访问
This commit is contained in:
zheng020 2026-06-12 12:41:56 +08:00
parent c875151daa
commit 0a3d8e0afc

View File

@ -87,6 +87,7 @@
│ │ {old, new, verify_token} │ │ │ │ │ {old, new, verify_token} │ │ │
│ ├─────────────────────────→│ ─Dubbo→ │ │ │ ├─────────────────────────→│ ─Dubbo→ │ │
│ │ │ UpdatePassword │ │ │ │ │ UpdatePassword │ │
│ │ │ - GetByID(uid) │ │
│ │ │ - VerifyToken() │ Get+DeleteVerifyTok│ │ │ │ - VerifyToken() │ Get+DeleteVerifyTok│
│ │ │ ├───────────────────→│ │ │ │ ├───────────────────→│
│ │ │ - bcrypt 比对 │ │ │ │ │ - bcrypt 比对 │ │
@ -166,6 +167,15 @@ func verifyTokenKey(scene, mobile string) string {
- Register 仍用 `scene="register"`,改密用 `scene="password"`,两个 scene 的 Redis key 互不污染 - Register 仍用 `scene="register"`,改密用 `scene="password"`,两个 scene 的 Redis key 互不污染
- 老数据兼容:老的 `sms:register:*` / `verify:register:*` 在过渡期可保留(只要还有未过期的 token),过渡期后自然过期(60s/300s TTL) - 老数据兼容:老的 `sms:register:*` / `verify:register:*` 在过渡期可保留(只要还有未过期的 token),过渡期后自然过期(60s/300s TTL)
**Call-site 审计(实施前必跑)**:
```bash
grep -rn "SaveVerifyToken\|GetVerifyToken\|DeleteVerifyToken\|VerifyToken(\|SaveSMSCode\|GetSMSCode\|VerifyCode(\|SendCode(" \
--include="*.go" backend/
```
涉及文件(已通过 grep 确认):9 个 —— `auth_service.go / sms_service.go / user.proto / user.pb.go / auth_provider.go / unified_provider.go / sms_redis.go / auth_controller.go / user.triple.go`。其中 `*.proto` / `*.pb.go` / `*.triple.go` 是 regen 自动产物,人工改动集中在 `auth_service.go / sms_service.go / auth_provider.go / auth_controller.go / unified_provider.go`
### 4.3 Service 层 UpdatePassword ### 4.3 Service 层 UpdatePassword
**修改** [backend/services/userService/service/user_service.go](backend/services/userService/service/user_service.go#L484-L587) **修改** [backend/services/userService/service/user_service.go](backend/services/userService/service/user_service.go#L484-L587)
@ -262,7 +272,37 @@ var (
| 9 | Redis 故障 | mock `VerifyToken` error | wrap 后抛出 | | 9 | Redis 故障 | mock `VerifyToken` error | wrap 后抛出 |
| 10 | 事务回滚 | mock `tx.Updates` 失败 | 整事务回滚 | | 10 | 事务回滚 | mock `tx.Updates` 失败 | 整事务回滚 |
Mock 方案:`testify/mock` 包装 `UserRepository`;`VerifyToken` 是包级函数,用 package-level option 注入避免改签名。 Mock 方案:
- `UserRepository`:用 `testify/mock` 包装,实现 `UserRepository` 接口,注入到 `userService`
- `VerifyToken`(包级函数):用 package-level `var` 注入(标准 Go test 模式)
```go
// sms_service.go(运行时)
var VerifyTokenFn func(ctx context.Context, scene, mobile, token string) error = realVerifyToken
func VerifyToken(ctx context.Context, scene, mobile, token string) error {
return VerifyTokenFn(ctx, scene, mobile, token)
}
func realVerifyToken(ctx context.Context, scene, mobile, token string) error {
// 实际 Get+DeleteVerifyToken 逻辑
}
// user_service_password_test.go(测试时)
func TestUpdatePassword_VerifyTokenExpired(t *testing.T) {
orig := VerifyTokenFn
defer func() { VerifyTokenFn = orig }()
VerifyTokenFn = func(ctx context.Context, scene, mobile, token string) error {
return errors.New("verify token expired")
}
// ... 断言
}
```
**Test #9 错误类型规约**:当 `VerifyToken` 自身出错(Redis 故障、key 拼错等)时:
- `VerifyToken` 返回的原始 error → wrap 为 `fmt.Errorf("failed to verify token: %w", err)` → 在 Provider 层映射为 500
- 业务上的"token 不存在 / 已过期" → 仍返回 `ErrInvalidVerifyToken` → 401
- §7 错误码表中"其他 5xx"行即对应此场景,前端 toast 通用提示
--- ---
@ -487,6 +527,9 @@ const confirmChangePassword = async () => {
if (res.code === 200) { if (res.code === 200) {
uni.showToast({ title: '修改成功,请重新登录', icon: 'success' }) uni.showToast({ title: '修改成功,请重新登录', icon: 'success' })
setTimeout(() => { setTimeout(() => {
// 改密后清空所有登录态 + 注册临时数据
// 备注:store 的 'user/logout' 实际只 commit CLEAR_AUTH(清 access_token/user/star_id/login_mobile/gallery_owner_id),
// 不会清 temp_register_*,所以这里需要手动清;后续若 store 升级,本处可同步收敛
store.dispatch('user/logout') store.dispatch('user/logout')
uni.removeStorageSync('access_token') uni.removeStorageSync('access_token')
uni.removeStorageSync('user') uni.removeStorageSync('user')
@ -520,7 +563,15 @@ import { validatePassword } from '@/utils/validator.js' // 已有
**API 函数兼容性确认**: **API 函数兼容性确认**:
- `sendCodeApi(mobile, scene)`:见 [api.js:187-196](frontend/utils/api.js#L187-L196),已支持任意 `scene` 字符串(后端在 SendCode 中按 scene 路由),无需改动 - `sendCodeApi(mobile, scene)`:见 [api.js:187-196](frontend/utils/api.js#L187-L196),已支持任意 `scene` 字符串(后端在 SendCode 中按 scene 路由),无需改动
- `verifyCodeApi(mobile, code, scene)`:见 [api.js:199-209](frontend/utils/api.js#L199-L209),已支持任意 `scene`,无需改动 - `verifyCodeApi(mobile, code, scene)`:见 [api.js:199-209](frontend/utils/api.js#L199-L209),已支持任意 `scene`,无需改动
- `updatePasswordApi(oldPassword, newPassword, verifyToken)`:**新签名**,§5.3.5 中给出实现;如有其他调用点需同步更新 - `updatePasswordApi(oldPassword, newPassword, verifyToken)`:**新签名**,§5.3.5 中给出实现
**`updatePasswordApi` 调用点审计**(实施前必跑):
```bash
grep -rn "updatePasswordApi" --include="*.vue" --include="*.js" frontend/
```
经 grep 确认:仅 2 个调用点 —— `frontend/utils/api.js`(定义处)+ `frontend/pages/profile/profile.vue`(使用处)。本 PR 内一并更新即可,无连锁影响。
#### 5.3.5 API 函数扩展 #### 5.3.5 API 函数扩展
@ -629,13 +680,14 @@ export function updatePasswordApi(oldPassword, newPassword, verifyToken) {
| 项 | 措施 | | 项 | 措施 |
|---|---| |---|---|
| 鉴权强制 | 路由 `/api/v1/account/password` 已挂 `AuthMiddleware`(见 [router.go:185-189](backend/gateway/router/router.go#L185-L189)),token 无效直接 401,无需在 Service 重复校验 userID 来源 |
| 短信 token 一次性 | 复用 `DeleteVerifyToken` 消费后即删 | | 短信 token 一次性 | 复用 `DeleteVerifyToken` 消费后即删 |
| 短信 token TTL | 5 分钟(与 Register 一致) | | 短信 token TTL | 5 分钟(与 Register 一致) |
| 限流 | 复用 `sms:limit:mobile:` 计数器,scene=password | | 限流 | 复用 `sms:limit:mobile:` 计数器,scene=password |
| 旧密码明文只传不存 | bcrypt(成本因子 10) | | 旧密码明文只传不存 | bcrypt(成本因子 10) |
| 改密后清 token | 事务内 `access_token = nil` | | 改密后清 token | 事务内 `access_token = nil` |
| updated_at 同步更新 | 与 token 校验机制一致,旧 token 失效 | | updated_at 同步更新 | 与 token 校验机制一致,旧 token 失效 |
| 不暴露 mobile 是否存在 | 短信发送成功/失败统一 200 | | 不暴露 mobile 是否存在 | 短信发送成功/失败统一 200(继承 Register 的 anti-enumeration 行为) |
--- ---