docs(change-password): 追加 §12 顺带修复 Login 账号状态码 BUG
问题: auth_service.go Login 流程被冻结/封禁时用 fmt.Errorf/errors.New
返回 generic error,落到 ToStatusCode default 分支 -> 500 而非 403
修复:
- errors.go: 新增 NewAccountBannedError / NewAccountFrozenError 2 helper,
保留 typed error 身份但允许附加 reason / frozenUntil 信息
- errors.go: ToStatusCode 改用 errors.Is 全面识别 wrapped error
(一处 switch 大改造,使其他 service 中已有的 fmt.Errorf("%w") 自动受益)
- auth_service.go: Login 流程 2 处 return 改用 helper
- auth_service_login_test.go (新建或追加): 2 个新单测验证 403 行为
This commit is contained in:
parent
e5d5808a84
commit
8c90de5b08
@ -744,7 +744,7 @@ export function updatePasswordApi(oldPassword, newPassword, verifyToken) {
|
|||||||
| Service | 修改 | `backend/services/userService/service/auth_service.go`(Register 中 `VerifyToken(ctx, mobile, token)` → `VerifyToken(ctx, "register", mobile, token)`,共 1 处) |
|
| Service | 修改 | `backend/services/userService/service/auth_service.go`(Register 中 `VerifyToken(ctx, mobile, token)` → `VerifyToken(ctx, "register", mobile, token)`,共 1 处) |
|
||||||
| Service | 新增 | `backend/services/userService/service/user_service_password_test.go` |
|
| Service | 新增 | `backend/services/userService/service/user_service_password_test.go` |
|
||||||
| Provider | 修改 | `backend/services/userService/provider/user_provider.go`(传 ctx) |
|
| Provider | 修改 | `backend/services/userService/provider/user_provider.go`(传 ctx) |
|
||||||
| Errors | 修改 | `backend/pkg/errors/errors.go`(新增 3 个错误码 `ErrInvalidVerifyToken`/`ErrSameAsOldPassword`/`ErrInvalidOldPassword`;`ToStatusCode` 中 `ErrInvalidVerifyToken` 改映射 400) |
|
| Errors | 修改 | `backend/pkg/errors/errors.go`(新增 3 个错误码 `ErrInvalidVerifyToken`/`ErrSameAsOldPassword`/`ErrInvalidOldPassword`;`ToStatusCode` 中 `ErrInvalidVerifyToken` 改映射 400;`ToStatusCode` 改用 `errors.Is` 全面识别 wrapped error;新增 `NewAccountBannedError` / `NewAccountFrozenError` 2 个 helper) |
|
||||||
| Frontend utils | 修改 | `frontend/utils/api.js`(拦截器去掉 400 自动登出,保留 401/403 + `updatePasswordApi` 增 `verify_token` 字段) |
|
| Frontend utils | 修改 | `frontend/utils/api.js`(拦截器去掉 400 自动登出,保留 401/403 + `updatePasswordApi` 增 `verify_token` 字段) |
|
||||||
| Frontend pages | 修改 | `frontend/pages/profile/profile.vue`(弹窗 + 状态 + handler + CSS) |
|
| Frontend pages | 修改 | `frontend/pages/profile/profile.vue`(弹窗 + 状态 + handler + CSS) |
|
||||||
|
|
||||||
@ -758,3 +758,133 @@ export function updatePasswordApi(oldPassword, newPassword, verifyToken) {
|
|||||||
- [ ] 前端手动验证 10 条 checklist 通过
|
- [ ] 前端手动验证 10 条 checklist 通过
|
||||||
- [ ] Swagger 文档重新生成
|
- [ ] Swagger 文档重新生成
|
||||||
- [ ] 灰度发布,先开 10% 流量观察错误率
|
- [ ] 灰度发布,先开 10% 流量观察错误率
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 关联修复:Login 流程账号状态码 BUG(顺带修)
|
||||||
|
|
||||||
|
### 12.1 问题
|
||||||
|
|
||||||
|
§7 设计的统一规则是"`ErrAccountFrozen` / `ErrAccountBanned` → 403 → 自动登出"。但实际 Login 流程并未使用这两个 typed error,导致:
|
||||||
|
|
||||||
|
[auth_service.go:327-377](backend/services/userService/service/auth_service.go#L327-L377) 在 banned / frozen 情况下:
|
||||||
|
|
||||||
|
```go
|
||||||
|
if accountStatus.IsBanned() {
|
||||||
|
return nil, fmt.Errorf("账号已被封禁%s%s", ...) // ← 不是 ErrAccountBanned
|
||||||
|
}
|
||||||
|
if accountStatus.IsFrozen() {
|
||||||
|
return nil, errors.New(errMsg) // ← 不是 ErrAccountFrozen
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这两个 error 都是 generic 的,**不匹配 `ToStatusCode()` 中 `case ErrAccountFrozen, ErrAccountBanned:` 分支**,落到 `default` → `STATUS_INTERNAL_ERROR` (500)。
|
||||||
|
|
||||||
|
**实际行为**:用户账号被冻结/封禁后尝试登录,后端返回 500(而非 403),前端按 §5.1 规则不会自动登出,只 toast 一个"修改失败"类的提示,体验不一致。
|
||||||
|
|
||||||
|
### 12.2 修复
|
||||||
|
|
||||||
|
**修改** [backend/services/userService/service/auth_service.go](backend/services/userService/service/auth_service.go#L327-L377)
|
||||||
|
|
||||||
|
引入两个新的辅助函数,把 reason / frozenUntil 信息塞进 error message 但保留 typed error 身份:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 新增到 errors.go
|
||||||
|
// NewAccountBannedError returns ErrAccountBanned with reason wrapped into message
|
||||||
|
func NewAccountBannedError(reason string) error {
|
||||||
|
if reason != "" {
|
||||||
|
return fmt.Errorf("%w: %s", ErrAccountBanned, reason)
|
||||||
|
}
|
||||||
|
return ErrAccountBanned
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAccountFrozenError returns ErrAccountFrozen with reason + frozenUntil wrapped into message
|
||||||
|
func NewAccountFrozenError(reason string, frozenUntil *int64) error {
|
||||||
|
parts := []string{}
|
||||||
|
if reason != "" {
|
||||||
|
parts = append(parts, "原因:"+reason)
|
||||||
|
}
|
||||||
|
if frozenUntil != nil {
|
||||||
|
parts = append(parts, "解封时间:"+time.UnixMilli(*frozenUntil).Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
if len(parts) > 0 {
|
||||||
|
return fmt.Errorf("%w: %s", ErrAccountFrozen, strings.Join(parts, ", "))
|
||||||
|
}
|
||||||
|
return ErrAccountFrozen
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键技巧**:`fmt.Errorf("%w: ...", ErrAccountBanned, ...)` 用 `%w` 保留 wrap 关系,使 `errors.Is(err, ErrAccountBanned)` 仍能识别。同时 `ToStatusCode()` 的 `switch err` 不识别 wrapped error,所以需要扩展 `ToStatusCode()` 用 `errors.Is`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// errors.go
|
||||||
|
func ToStatusCode(err error) pb.StatusCode {
|
||||||
|
if err == nil {
|
||||||
|
return pb.StatusCode_STATUS_OK
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, ErrUserNotFound), errors.Is(err, ErrFanProfileNotFound), errors.Is(err, ErrStarNotFound):
|
||||||
|
return pb.StatusCode_STATUS_NOT_FOUND
|
||||||
|
case errors.Is(err, ErrUserAlreadyExists), errors.Is(err, ErrFanProfileAlreadyExists), errors.Is(err, ErrNicknameAlreadyExists):
|
||||||
|
return pb.StatusCode_STATUS_BAD_REQUEST
|
||||||
|
case errors.Is(err, ErrInvalidPassword), errors.Is(err, ErrInvalidToken), errors.Is(err, ErrTokenExpired), errors.Is(err, ErrTokenMismatch):
|
||||||
|
return pb.StatusCode_STATUS_UNAUTHORIZED
|
||||||
|
case errors.Is(err, ErrAccountFrozen), errors.Is(err, ErrAccountBanned):
|
||||||
|
return pb.StatusCode_STATUS_FORBIDDEN
|
||||||
|
// ... 后续同样改用 errors.Is
|
||||||
|
default:
|
||||||
|
return pb.StatusCode_STATUS_INTERNAL_ERROR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**auth_service.go Login 流程**改造:
|
||||||
|
|
||||||
|
```go
|
||||||
|
if accountStatus.IsBanned() {
|
||||||
|
reason := ""
|
||||||
|
if accountStatus.Reason != nil {
|
||||||
|
reason = *accountStatus.Reason
|
||||||
|
}
|
||||||
|
logger.Logger.Warn("User account is banned", ...)
|
||||||
|
return nil, appErrors.NewAccountBannedError(reason) // ← 改用 typed error
|
||||||
|
}
|
||||||
|
|
||||||
|
if accountStatus.IsFrozen() {
|
||||||
|
if accountStatus.FrozenUntil != nil && time.Now().UnixMilli() > *accountStatus.FrozenUntil {
|
||||||
|
// 冻结已过期,放行
|
||||||
|
logger.Logger.Info("User account frozen but expired", ...)
|
||||||
|
} else {
|
||||||
|
reason := ""
|
||||||
|
if accountStatus.Reason != nil {
|
||||||
|
reason = *accountStatus.Reason
|
||||||
|
}
|
||||||
|
logger.Logger.Warn("User account is frozen", ...)
|
||||||
|
return nil, appErrors.NewAccountFrozenError(reason, accountStatus.FrozenUntil) // ← 改用 typed error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 12.3 影响面
|
||||||
|
|
||||||
|
| 项 | 变化 |
|
||||||
|
|---|---|
|
||||||
|
| `errors.go` | 新增 `NewAccountBannedError` / `NewAccountFrozenError` 2 个 helper;`ToStatusCode` 改用 `errors.Is` 全面识别 wrapped error(1 处 switch 大改造) |
|
||||||
|
| `auth_service.go` | Login 流程的 2 处 `return` 改用 helper |
|
||||||
|
| 其他服务 | `ToStatusCode` 改用 `errors.Is` 后,其他 service 中已有的 `fmt.Errorf("%w", ErrXxx)` 自动受益,**无需逐个改动** |
|
||||||
|
| 前端 | 无需改动:403 业务码已在 §5.1 拦截器白名单中被正确处理(自动登出 + 跳登录页) |
|
||||||
|
|
||||||
|
### 12.4 单测追加
|
||||||
|
|
||||||
|
在 `auth_service_login_test.go`(新建,或追加到现有 test)加 2 个用例:
|
||||||
|
|
||||||
|
| # | 场景 | 期望 |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | 账号被封禁 | 业务码 403(不再是 500),`errors.Is(err, ErrAccountBanned)` 为 true |
|
||||||
|
| 2 | 账号被冻结 | 业务码 403(不再是 500),`errors.Is(err, ErrAccountFrozen)` 为 true |
|
||||||
|
|
||||||
|
### 12.5 与"状态码重构"的关系
|
||||||
|
|
||||||
|
本次修复是**最小变更**,仅让 Login 流程复用现有的 `ErrAccountFrozen` / `ErrAccountBanned` 语义,业务码到 HTTP 码的映射规则不变。
|
||||||
|
|
||||||
|
如果你后续要做"状态码体系重构"(见 `docs/superpowers/specs/2026-06-12-status-code-refactor-design.md` 草稿),本次修复的 `errors.Is` 改造会作为重构的前置工作存在,可以平滑迁移。
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user