269 lines
6.5 KiB
Markdown
Executable File
269 lines
6.5 KiB
Markdown
Executable File
# 📋 实施计划:后台管理端 - 添加修改用户手机号功能
|
||
|
||
## 任务类型
|
||
- [x] 后端 (→ Codex)
|
||
- [ ] 前端
|
||
- [ ] 全栈
|
||
|
||
---
|
||
|
||
## 📊 需求描述
|
||
|
||
为后台管理端添加修改用户手机号的功能,允许管理员直接修改用户的手机号。
|
||
|
||
---
|
||
|
||
## 🎯 技术方案
|
||
|
||
### API 设计
|
||
|
||
```http
|
||
PUT /api/admin/users/{user_id}/mobile
|
||
Content-Type: application/json
|
||
Authorization: Bearer {admin_token}
|
||
|
||
Request Body:
|
||
{
|
||
"mobile": "13800138000"
|
||
}
|
||
|
||
Response:
|
||
{
|
||
"success": true,
|
||
"message": "手机号更新成功"
|
||
}
|
||
```
|
||
|
||
### 安全校验
|
||
|
||
1. ✅ 管理员权限验证(RBAC: `user:edit`)
|
||
2. ✅ 手机号格式验证(11位数字,1开头)
|
||
3. ✅ 手机号唯一性检查(不能与其他用户重复)
|
||
4. ✅ 用户存在性验证
|
||
|
||
---
|
||
|
||
## 🔧 实施步骤
|
||
|
||
### 步骤 1:添加 API 处理函数
|
||
|
||
**文件**:`internal/api/admin/users_admin.go`
|
||
**位置**:Line 1893 后(在 `UpdateUserRemark()` 函数之后)
|
||
|
||
**代码**:
|
||
|
||
```go
|
||
// updateUserMobileRequest 更新用户手机号请求
|
||
type updateUserMobileRequest struct {
|
||
Mobile string `json:"mobile" binding:"required"`
|
||
}
|
||
|
||
// UpdateUserMobile 更新用户手机号
|
||
// @Summary 更新用户手机号
|
||
// @Description 管理员修改用户手机号
|
||
// @Tags 管理端.用户
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Param user_id path integer true "用户ID"
|
||
// @Param body body updateUserMobileRequest true "手机号信息"
|
||
// @Success 200 {object} map[string]any
|
||
// @Failure 400 {object} code.Failure
|
||
// @Router /api/admin/users/{user_id}/mobile [put]
|
||
// @Security LoginVerifyToken
|
||
func (h *handler) UpdateUserMobile() core.HandlerFunc {
|
||
return func(ctx core.Context) {
|
||
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
|
||
if err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "用户ID无效"))
|
||
return
|
||
}
|
||
|
||
req := new(updateUserMobileRequest)
|
||
if err := ctx.ShouldBindJSON(req); err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||
return
|
||
}
|
||
|
||
// 验证手机号格式(11位数字,1开头)
|
||
matched, _ := regexp.MatchString(`^1[3-9]\d{9}$`, req.Mobile)
|
||
if !matched {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "手机号格式不正确"))
|
||
return
|
||
}
|
||
|
||
// 检查手机号是否被其他用户占用
|
||
existedUser, _ := h.readDB.Users.WithContext(ctx.RequestContext()).
|
||
Where(h.readDB.Users.Mobile.Eq(req.Mobile)).
|
||
Where(h.readDB.Users.ID.Neq(userID)).
|
||
First()
|
||
|
||
if existedUser != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "该手机号已被其他用户使用"))
|
||
return
|
||
}
|
||
|
||
// 更新用户手机号
|
||
_, err = h.writeDB.Users.WithContext(ctx.RequestContext()).
|
||
Where(h.writeDB.Users.ID.Eq(userID)).
|
||
Update(h.writeDB.Users.Mobile, req.Mobile)
|
||
|
||
if err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20303, "更新失败: "+err.Error()))
|
||
return
|
||
}
|
||
|
||
ctx.Payload(map[string]any{
|
||
"success": true,
|
||
"message": "手机号更新成功",
|
||
})
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 步骤 2:添加导入依赖
|
||
|
||
**文件**:`internal/api/admin/users_admin.go`
|
||
**位置**:文件顶部 import 区域
|
||
|
||
**修改**:在现有 import 中添加 `regexp` 包
|
||
|
||
```go
|
||
import (
|
||
"fmt"
|
||
"math"
|
||
"net/http"
|
||
"regexp" // ← 添加这一行
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"bindbox-game/internal/code"
|
||
// ... 其他导入保持不变
|
||
)
|
||
```
|
||
|
||
---
|
||
|
||
### 步骤 3:注册路由
|
||
|
||
**文件**:`internal/router/router.go`
|
||
**位置**:在 adminAuthApiRouter 用户管理路由组中
|
||
|
||
**查找位置参考**:
|
||
```go
|
||
// 找到类似这样的路由组(用户管理相关)
|
||
adminAuthApiRouter.PUT("/users/:user_id/douyin_user_id", ...)
|
||
adminAuthApiRouter.PUT("/users/:user_id/remark", ...)
|
||
adminAuthApiRouter.PUT("/users/:user_id/status", ...)
|
||
```
|
||
|
||
**添加路由**:
|
||
```go
|
||
// 在上述路由附近添加
|
||
adminAuthApiRouter.PUT("/users/:user_id/mobile",
|
||
intc.RequireAdminAction("user:edit"),
|
||
adminHandler.UpdateUserMobile())
|
||
```
|
||
|
||
---
|
||
|
||
### 步骤 4:更新 Swagger 文档
|
||
|
||
**文件**:运行命令生成文档
|
||
|
||
```bash
|
||
make gen-swagger
|
||
```
|
||
|
||
---
|
||
|
||
## 📋 关键文件清单
|
||
|
||
| 文件路径 | 操作 | 说明 |
|
||
|---------|------|------|
|
||
| `internal/api/admin/users_admin.go` | 新增函数 | 添加 `UpdateUserMobile()` 及请求结构体 |
|
||
| `internal/api/admin/users_admin.go` | 修改导入 | 添加 `regexp` 包 |
|
||
| `internal/router/router.go` | 新增路由 | 注册 `PUT /users/:user_id/mobile` |
|
||
|
||
---
|
||
|
||
## ✅ 验证清单
|
||
|
||
完成后请验证:
|
||
|
||
- [ ] API 接口可正常访问
|
||
- [ ] 手机号格式验证生效(非法格式返回错误)
|
||
- [ ] 手机号唯一性检查生效(重复手机号返回错误)
|
||
- [ ] 更新成功后数据库中手机号已变更
|
||
- [ ] Swagger 文档已更新
|
||
- [ ] 权限验证生效(非管理员或无 `user:edit` 权限无法访问)
|
||
|
||
---
|
||
|
||
## ⚠️ 风险与缓解
|
||
|
||
| 风险 | 缓解措施 |
|
||
|------|----------|
|
||
| 手机号格式错误 | 正则验证 `^1[3-9]\d{9}$` |
|
||
| 手机号被占用 | 查询检查 `WHERE mobile = ? AND id != ?` |
|
||
| 权限滥用 | RBAC 中间件 `RequireAdminAction("user:edit")` |
|
||
| 更新失败 | 捕获异常并返回错误码 20303 |
|
||
|
||
---
|
||
|
||
## 📝 测试用例
|
||
|
||
### 正常流程
|
||
```bash
|
||
curl -X PUT http://localhost:9991/api/admin/users/123/mobile \
|
||
-H "Authorization: Bearer {admin_token}" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"mobile":"13800138000"}'
|
||
|
||
# 预期响应
|
||
{
|
||
"success": true,
|
||
"message": "手机号更新成功"
|
||
}
|
||
```
|
||
|
||
### 异常流程
|
||
|
||
**1. 手机号格式错误**
|
||
```json
|
||
{"mobile": "123"}
|
||
→ {"code": 10001, "message": "手机号格式不正确"}
|
||
```
|
||
|
||
**2. 手机号已被占用**
|
||
```json
|
||
{"mobile": "13800138000"} // 已被用户456使用
|
||
→ {"code": 10001, "message": "该手机号已被其他用户使用"}
|
||
```
|
||
|
||
**3. 用户ID不存在**
|
||
```bash
|
||
PUT /api/admin/users/999999/mobile
|
||
→ 更新成功但影响行数为0(GORM特性,不会报错)
|
||
```
|
||
|
||
---
|
||
|
||
## 🚀 后续优化建议(可选)
|
||
|
||
1. **操作日志记录**:记录管理员修改手机号的操作到审计日志
|
||
2. **短信验证**:要求新手机号验证码确认(防止恶意修改)
|
||
3. **旧手机号通知**:向旧手机号发送变更通知短信
|
||
4. **限制修改频率**:同一用户手机号修改间隔限制(如7天)
|
||
|
||
---
|
||
|
||
## 📌 备注
|
||
|
||
- 本方案仅实现管理端修改功能,不包含用户端自助修改
|
||
- 遵循现有代码风格(参考 `UpdateUserRemark()` 和 `UpdateUserDouyinID()`)
|
||
- 使用 GORM Gen 生成的 DAO 进行数据库操作
|
||
- 错误码 20303 用于手机号更新失败(延续现有错误码序列 20301, 20302)
|