Skip to content

Commit 3905a38

Browse files
committed
```
feat(users): 添加批量注册与批量修改用户功能 新增支持通过 CSV 文件批量注册用户的功能,以及批量修改用户账号类型和角色的功能。 - 添加 `/api/users/batch` 接口用于批量注册用户 - 添加 `/api/users/category` 和 `/api/users/role` 接口用于批量修改用户属性 - 引入 207 Multi-Status 响应机制处理部分失败的批量操作 - 更新错误处理逻辑,细化多种业务错误类型 - 完善 Swagger 文档,补充新接口的定义和说明 - README 中增加新功能使用说明及快速上手示例 ```
1 parent 4d9a56d commit 3905a38

13 files changed

Lines changed: 1273 additions & 67 deletions

File tree

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,47 @@
11
# asynx
22

33
> 核心Restful API
4+
5+
## 新功能
6+
7+
### 批量用户注册
8+
系统现已支持通过CSV文件批量注册用户功能。详细使用说明请参考 [批量注册文档](docs/batch_register.md)
9+
10+
**主要特性:**
11+
- 支持CSV文件批量上传用户信息
12+
- 详细的错误报告和成功统计
13+
- 数据验证和格式检查
14+
- 安全的权限控制(仅ADMIN可用)
15+
16+
**快速使用:**
17+
1. 使用 `templates/batch_register_template.csv` 作为模板
18+
2.`POST /api/users/batch` 上传CSV文件
19+
3. 查看批量注册结果和错误详情
20+
21+
### 批量修改用户信息
22+
系统现已支持批量修改用户账号类型和角色功能。详细使用说明请参考 [批量修改文档](docs/batch_modify.md)
23+
24+
**主要特性:**
25+
- 支持批量修改用户账号类型(system|member|external)
26+
- 支持批量修改用户角色(admin|default|restricted)
27+
- 详细的操作结果统计和错误报告
28+
- 安全的权限控制(仅ADMIN可用)
29+
30+
**API端点:**
31+
- `PATCH /api/users/category` - 批量修改账号类型
32+
- `PATCH /api/users/role` - 批量修改账号角色
33+
34+
**快速使用:**
35+
```bash
36+
# 批量修改账号类型
37+
curl -X PATCH http://localhost:8080/api/users/category \
38+
-H 'Authorization: Bearer TOKEN' \
39+
-H 'Content-Type: application/json' \
40+
-d '{"userIds":["user1","user2"],"category":"member"}'
41+
42+
# 批量修改账号角色
43+
curl -X PATCH http://localhost:8080/api/users/role \
44+
-H 'Authorization: Bearer TOKEN' \
45+
-H 'Content-Type: application/json' \
46+
-d '{"userIds":["user1","user2"],"role":"default"}'
47+
```

backend/pkg/controller/errors.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package controller
22

33
import (
4+
"encoding/json"
45
"net/http"
56

67
"github.com/dsx137/gg-gin/pkg/gggin"
@@ -10,3 +11,11 @@ var (
1011
ErrHttpForceForbidden = gggin.NewHttpError(http.StatusForbidden, "WHAT ARE YOU DOING?")
1112
ErrHttpGuardFail = gggin.NewHttpError(http.StatusInternalServerError, "获取用户信息失败")
1213
)
14+
15+
// NewMultiStatusResponse 创建207 Multi-Status响应
16+
// 这不是真正的错误,而是用来通过gggin框架返回207状态码的机制
17+
func NewMultiStatusResponse[T any](data T) *gggin.HttpError {
18+
// 将数据序列化为JSON
19+
jsonData, _ := json.Marshal(map[string]T{"data": data})
20+
return gggin.NewHttpError(http.StatusMultiStatus, string(jsonData))
21+
}

backend/pkg/controller/users.go

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,14 @@ func NewControllerUser(g *gin.RouterGroup, serviceManager *service.ServiceManage
1717
ctl := &ControllerUser{serviceManager: serviceManager}
1818
g.GET("", security.GuardMiddleware(security.RoleDefault), gggin.ToGinHandler(ctl.HandleListProfiles))
1919
g.POST("", security.GuardMiddleware(security.RoleAdmin), gggin.ToGinHandler(ctl.HandleRegister))
20+
g.POST("/batch", security.GuardMiddleware(security.RoleAdmin), gggin.ToGinHandler(ctl.HandleBatchRegister))
2021
g.GET("/:uid", security.GuardMiddleware(security.RoleRestricted), gggin.ToGinHandler(ctl.HandleGetProfile))
2122
g.DELETE("/:uid", security.GuardMiddleware(security.RoleAdmin), gggin.ToGinHandler(ctl.HandleUnregister))
2223
g.PUT("/:uid/password", security.GuardMiddleware(security.RoleRestricted), gggin.ToGinHandler(ctl.HandleChangePassword))
2324
g.PUT("/:uid/category", security.GuardMiddleware(security.RoleAdmin), gggin.ToGinHandler(ctl.HandleModifyCategory))
2425
g.PUT("/:uid/role", security.GuardMiddleware(security.RoleAdmin), gggin.ToGinHandler(ctl.HandleModifyRole))
26+
g.PATCH("/category", security.GuardMiddleware(security.RoleAdmin), gggin.ToGinHandler(ctl.HandleBatchModifyCategory))
27+
g.PATCH("/role", security.GuardMiddleware(security.RoleAdmin), gggin.ToGinHandler(ctl.HandleBatchModifyRole))
2528

2629
// Deprecated
2730
g.GET("/:uid/category", security.GuardMiddleware(security.RoleRestricted), gggin.ToGinHandler(ctl.HandleGetCategory))
@@ -372,3 +375,215 @@ func (ctl *ControllerUser) HandleGetCategory(c *gin.Context) (*gggin.Response[se
372375

373376
return gggin.NewResponse(category), nil
374377
}
378+
379+
// ======== 批量操作相关类型定义 ========
380+
381+
// RequestBatchRegister 批量注册请求体
382+
type RequestBatchRegister struct {
383+
Users []BatchRegisterUser `json:"users" binding:"required,dive"`
384+
}
385+
386+
// BatchRegisterUser 单个用户注册信息
387+
type BatchRegisterUser struct {
388+
Username string `json:"username" binding:"required"`
389+
Email string `json:"email" binding:"required,email"`
390+
Category string `json:"category" binding:"required"`
391+
Role string `json:"role" binding:"required"`
392+
}
393+
394+
// BatchRegisterResult 批量注册结果
395+
type BatchRegisterResult struct {
396+
Success int `json:"success"`
397+
Failed int `json:"failed"`
398+
Total int `json:"total"`
399+
Failures []BatchFailureInfo `json:"failures"`
400+
}
401+
402+
// BatchFailureInfo 批量操作失败信息
403+
type BatchFailureInfo struct {
404+
Row int `json:"row"`
405+
Username string `json:"username"`
406+
Error string `json:"error"`
407+
}
408+
409+
// RequestBatchModifyCategory 批量修改类别请求体
410+
type RequestBatchModifyCategory struct {
411+
UserIds []string `json:"userIds" binding:"required"`
412+
Category string `json:"category" binding:"required"`
413+
}
414+
415+
// RequestBatchModifyRole 批量修改角色请求体
416+
type RequestBatchModifyRole struct {
417+
UserIds []string `json:"userIds" binding:"required"`
418+
Role string `json:"role" binding:"required"`
419+
}
420+
421+
// BatchModifyResult 批量修改结果
422+
type BatchModifyResult struct {
423+
Success int `json:"success"`
424+
Failed int `json:"failed"`
425+
Total int `json:"total"`
426+
Failures []BatchModifyFailure `json:"failures"`
427+
}
428+
429+
// BatchModifyFailure 批量修改失败信息
430+
type BatchModifyFailure struct {
431+
UserId string `json:"userId"`
432+
Error string `json:"error"`
433+
}
434+
435+
// ======== 批量操作功能实现 ========
436+
437+
// @Summary 批量注册用户
438+
// @Description 通过CSV数据批量注册多个用户。需要 ADMIN 角色权限。
439+
// @Tags users
440+
// @Accept json
441+
// @Produce json
442+
// @Param body body RequestBatchRegister true "批量注册请求"
443+
// @Success 200 {object} object{data=BatchRegisterResult} "全部成功:批量注册结果"
444+
// @Success 207 {object} object{data=BatchRegisterResult} "部分失败:批量注册结果"
445+
// @Failure 400 {object} object{data=string} "请求参数错误"
446+
// @Failure 401 {object} object{data=string} "未授权访问"
447+
// @Failure 403 {object} object{data=string} "权限不足"
448+
// @Failure 500 {object} object{data=string} "服务器内部错误"
449+
// @Router /users/batch [post]
450+
// @Security BearerAuth
451+
func (ctl *ControllerUser) HandleBatchRegister(c *gin.Context) (*gggin.Response[BatchRegisterResult], *gggin.HttpError) {
452+
_, ok := gggin.Get[*security.GuardResult](c, "guard")
453+
if !ok {
454+
return nil, ErrHttpGuardFail
455+
}
456+
457+
req, err := gggin.ShouldBindJSON[RequestBatchRegister](c)
458+
if err != nil {
459+
return nil, gggin.NewHttpError(http.StatusBadRequest, err.Error())
460+
}
461+
462+
var result BatchRegisterResult
463+
result.Total = len(req.Users)
464+
465+
for idx, user := range req.Users {
466+
err := ctl.serviceManager.Register(user.Username, "", "", user.Email, user.Category, user.Role)
467+
if err != nil {
468+
result.Failed++
469+
result.Failures = append(result.Failures, BatchFailureInfo{
470+
Row: idx + 1,
471+
Username: user.Username,
472+
Error: err.Error(),
473+
})
474+
} else {
475+
result.Success++
476+
}
477+
}
478+
479+
// 根据结果决定返回方式:全部成功返回200,部分失败返回207
480+
if result.Failed > 0 && result.Success > 0 {
481+
// 部分失败:使用207 Multi-Status
482+
return nil, NewMultiStatusResponse(result)
483+
}
484+
485+
// 全部成功或全部失败:使用200
486+
return gggin.NewResponse(result), nil
487+
}
488+
489+
// @Summary 批量修改账号类型
490+
// @Description 批量修改指定用户的账号类型。需要 ADMIN 角色权限。
491+
// @Tags users
492+
// @Accept json
493+
// @Produce json
494+
// @Param body body RequestBatchModifyCategory true "批量修改账号类型请求"
495+
// @Success 200 {object} object{data=BatchModifyResult} "全部成功:批量修改结果"
496+
// @Success 207 {object} object{data=BatchModifyResult} "部分失败:批量修改结果"
497+
// @Failure 400 {object} object{data=string} "请求参数错误"
498+
// @Failure 401 {object} object{data=string} "未授权访问"
499+
// @Failure 403 {object} object{data=string} "权限不足"
500+
// @Failure 500 {object} object{data=string} "服务器内部错误"
501+
// @Router /users/category [patch]
502+
// @Security BearerAuth
503+
func (ctl *ControllerUser) HandleBatchModifyCategory(c *gin.Context) (*gggin.Response[BatchModifyResult], *gggin.HttpError) {
504+
_, ok := gggin.Get[*security.GuardResult](c, "guard")
505+
if !ok {
506+
return nil, ErrHttpGuardFail
507+
}
508+
509+
req, err := gggin.ShouldBindJSON[RequestBatchModifyCategory](c)
510+
if err != nil {
511+
return nil, gggin.NewHttpError(http.StatusBadRequest, err.Error())
512+
}
513+
514+
var result BatchModifyResult
515+
result.Total = len(req.UserIds)
516+
517+
for _, uid := range req.UserIds {
518+
err := ctl.serviceManager.ModifyCategory(uid, req.Category)
519+
if err != nil {
520+
result.Failed++
521+
result.Failures = append(result.Failures, BatchModifyFailure{
522+
UserId: uid,
523+
Error: err.Error(),
524+
})
525+
} else {
526+
result.Success++
527+
}
528+
}
529+
530+
// 根据结果决定返回方式:全部成功返回200,部分失败返回207
531+
if result.Failed > 0 && result.Success > 0 {
532+
// 部分失败:使用207 Multi-Status
533+
return nil, NewMultiStatusResponse(result)
534+
}
535+
536+
// 全部成功或全部失败:使用200
537+
return gggin.NewResponse(result), nil
538+
}
539+
540+
// @Summary 批量修改角色权限
541+
// @Description 批量修改指定用户的角色权限。需要 ADMIN 角色权限。
542+
// @Tags users
543+
// @Accept json
544+
// @Produce json
545+
// @Param body body RequestBatchModifyRole true "批量修改角色权限请求"
546+
// @Success 200 {object} object{data=BatchModifyResult} "全部成功:批量修改结果"
547+
// @Success 207 {object} object{data=BatchModifyResult} "部分失败:批量修改结果"
548+
// @Failure 400 {object} object{data=string} "请求参数错误"
549+
// @Failure 401 {object} object{data=string} "未授权访问"
550+
// @Failure 403 {object} object{data=string} "权限不足"
551+
// @Failure 500 {object} object{data=string} "服务器内部错误"
552+
// @Router /users/role [patch]
553+
// @Security BearerAuth
554+
func (ctl *ControllerUser) HandleBatchModifyRole(c *gin.Context) (*gggin.Response[BatchModifyResult], *gggin.HttpError) {
555+
_, ok := gggin.Get[*security.GuardResult](c, "guard")
556+
if !ok {
557+
return nil, ErrHttpGuardFail
558+
}
559+
560+
req, err := gggin.ShouldBindJSON[RequestBatchModifyRole](c)
561+
if err != nil {
562+
return nil, gggin.NewHttpError(http.StatusBadRequest, err.Error())
563+
}
564+
565+
var result BatchModifyResult
566+
result.Total = len(req.UserIds)
567+
568+
for _, uid := range req.UserIds {
569+
err := ctl.serviceManager.GrantRoleByUidAndRoleName(uid, req.Role)
570+
if err != nil {
571+
result.Failed++
572+
result.Failures = append(result.Failures, BatchModifyFailure{
573+
UserId: uid,
574+
Error: err.Error(),
575+
})
576+
} else {
577+
result.Success++
578+
}
579+
}
580+
581+
// 根据结果决定返回方式:全部成功返回200,部分失败返回207
582+
if result.Failed > 0 && result.Success > 0 {
583+
// 部分失败:使用207 Multi-Status
584+
return nil, NewMultiStatusResponse(result)
585+
}
586+
587+
// 全部成功或全部失败:使用200
588+
return gggin.NewResponse(result), nil
589+
}

backend/pkg/repository/user.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ import (
77
"asynclab.club/asynx/backend/pkg/config"
88
"asynclab.club/asynx/backend/pkg/entity"
99
"asynclab.club/asynx/backend/pkg/transfer"
10-
11-
1210
)
1311

1412
type RepositoryUser struct {

backend/pkg/service/errors.go

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,15 @@ import (
99
)
1010

1111
var (
12-
ErrNotFound = errors.New("not found")
13-
ErrExists = errors.New("already exists")
14-
ErrInvalid = errors.New("invalid objet")
12+
ErrNotFound = errors.New("not found")
13+
ErrExists = errors.New("already exists")
14+
ErrInvalidEmail = errors.New("invalid email format")
15+
ErrInvalidRole = errors.New("invalid role")
16+
ErrInvalidOu = errors.New("invalid organizational unit")
17+
ErrInvalidCreds = errors.New("invalid credentials")
18+
ErrConflict = errors.New("conflict")
19+
ErrIllegalPassword = errors.New("illegal password")
20+
ErrWeakPassword = errors.New("weak password")
1521
)
1622

1723
type ServiceError struct {
@@ -33,8 +39,18 @@ func MapErrorToHttp(err error) *gggin.HttpError {
3339
return gggin.NewHttpError(http.StatusNotFound, fmt.Sprintf("对象不存在: %s", err.Error()))
3440
case errors.Is(err, ErrExists):
3541
return gggin.NewHttpError(http.StatusConflict, fmt.Sprintf("对象已存在: %s", err.Error()))
36-
case errors.Is(err, ErrInvalid):
37-
return gggin.NewHttpError(http.StatusBadRequest, fmt.Sprintf("无效的对象: %s", err.Error()))
42+
case errors.Is(err, ErrInvalidEmail):
43+
return gggin.NewHttpError(http.StatusBadRequest, fmt.Sprintf("邮箱格式不合法: %s", err.Error()))
44+
case errors.Is(err, ErrInvalidOu):
45+
return gggin.NewHttpError(http.StatusBadRequest, fmt.Sprintf("账号类型不合法: %s", err.Error()))
46+
case errors.Is(err, ErrInvalidRole):
47+
return gggin.NewHttpError(http.StatusBadRequest, fmt.Sprintf("角色不合法: %s", err.Error()))
48+
case errors.Is(err, ErrInvalidCreds):
49+
return gggin.NewHttpError(http.StatusUnauthorized, fmt.Sprintf("无效的凭证: %s", err.Error()))
50+
case errors.Is(err, ErrIllegalPassword):
51+
return gggin.NewHttpError(http.StatusBadRequest, fmt.Sprintf("密码不符合要求: %s", err.Error()))
52+
case errors.Is(err, ErrWeakPassword):
53+
return gggin.NewHttpError(http.StatusBadRequest, fmt.Sprintf("密码强度不够: %s", err.Error()))
3854
default:
3955
return gggin.NewHttpError(http.StatusInternalServerError, err.Error())
4056
}

backend/pkg/service/group.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func (r *ServiceGroup) FindByOuAndCn(ou security.OuGroup, cn string) (*entity.Gr
2626
return nil, err
2727
}
2828
if group == nil {
29-
return nil, WrapError(ErrNotFound, fmt.Sprintf("group %s not found", cn))
29+
return nil, ErrNotFound
3030
}
3131
return group, nil
3232
}

0 commit comments

Comments
 (0)