11package controller
22
33import (
4+ "encoding/csv"
5+ "io"
46 "net/http"
7+ "strings"
58
69 "asynclab.club/asynx/backend/pkg/security"
710 "asynclab.club/asynx/backend/pkg/service"
@@ -22,6 +25,8 @@ func NewControllerUser(g *gin.RouterGroup, serviceManager *service.ServiceManage
2225 g .PUT ("/:uid/password" , security .GuardMiddleware (security .RoleRestricted ), gggin .ToGinHandler (ctl .HandleChangePassword ))
2326 g .PUT ("/:uid/category" , security .GuardMiddleware (security .RoleAdmin ), gggin .ToGinHandler (ctl .HandleModifyCategory ))
2427 g .PUT ("/:uid/role" , security .GuardMiddleware (security .RoleAdmin ), gggin .ToGinHandler (ctl .HandleModifyRole ))
28+ g .PATCH ("/category" , security .GuardMiddleware (security .RoleAdmin ), gggin .ToGinHandler (ctl .HandleBatchModifyCategory ))
29+ g .PATCH ("/role" , security .GuardMiddleware (security .RoleAdmin ), gggin .ToGinHandler (ctl .HandleBatchModifyRole ))
2530
2631 // Deprecated
2732 g .GET ("/:uid/category" , security .GuardMiddleware (security .RoleRestricted ), gggin .ToGinHandler (ctl .HandleGetCategory ))
@@ -128,7 +133,6 @@ func (ctl *ControllerUser) HandleChangePassword(c *gin.Context) (*gggin.Response
128133 err = ctl .serviceManager .ChangePassword (uid , req .Password )
129134 if err != nil {
130135 return nil , service .MapErrorToHttp (err )
131-
132136 }
133137
134138 return gggin .Ok , nil
@@ -230,24 +234,52 @@ type RequestRegister struct {
230234}
231235
232236// @Summary 注册新用户
233- // @Description 创建新用户账号。需要 ADMIN 角色权限。
237+ // @Description 创建新用户账号。支持两种方式:1) JSON格式单个注册 2) CSV文件批量注册。需要 ADMIN 角色权限。
238+ // @Description
239+ // @Description **单个注册 (application/json)**
240+ // @Description 发送JSON格式的单个用户数据
241+ // @Description
242+ // @Description **批量注册 (multipart/form-data 或 text/csv)**
243+ // @Description 上传CSV文件,CSV格式为:username,surName,givenName,mail,category,role
244+ // @Description 示例:user001,张,三,zhangsan@example.com,member,default
234245// @Tags users
235- // @Accept json
246+ // @Accept json,multipart/form-data,text/csv
236247// @Produce json
237- // @Param body body RequestRegister true "注册用户请求"
238- // @Success 200 {object} object{data=string} "成功注册用户,返回 'ok'"
248+ // @Param body body RequestRegister false "单个注册请求 (Content-Type: application/json)"
249+ // @Param file formData file false "批量注册CSV文件 (Content-Type: multipart/form-data)"
250+ // @Success 200 {object} object{data=string} "单个注册成功,返回 'ok'"
251+ // @Success 200 {object} object{data=BatchRegisterResult} "批量注册全部成功"
252+ // @Success 207 {object} object{data=BatchRegisterResult} "批量注册部分失败"
239253// @Failure 400 {object} object{data=string} "请求参数错误"
240254// @Failure 401 {object} object{data=string} "未授权访问"
241255// @Failure 403 {object} object{data=string} "权限不足"
242256// @Failure 409 {object} object{data=string} "用户已存在"
257+ // @Failure 415 {object} object{data=string} "不支持的媒体类型"
243258// @Failure 500 {object} object{data=string} "服务器内部错误"
244259// @Router /users [post]
245260// @Security BearerAuth
246- func (ctl * ControllerUser ) HandleRegister (c * gin.Context ) (* gggin.Response [string ], * gggin.HttpError ) {
261+ func (ctl * ControllerUser ) HandleRegister (c * gin.Context ) (* gggin.Response [any ], * gggin.HttpError ) {
247262 _ , ok := gggin .Get [* security.GuardResult ](c , "guard" )
248263 if ! ok {
249264 return nil , ErrHttpGuardFail
250265 }
266+
267+ contentType := c .ContentType ()
268+
269+ // 根据 Content-Type 区分单个注册还是批量注册
270+ if contentType == "application/json" {
271+ // 单个用户注册
272+ return ctl .handleSingleRegister (c )
273+ } else if strings .HasPrefix (contentType , "multipart/form-data" ) || contentType == "text/csv" {
274+ // CSV 批量注册
275+ return ctl .handleBatchRegisterFromCSV (c )
276+ }
277+
278+ return nil , gggin .NewHttpError (http .StatusUnsupportedMediaType , "不支持的 Content-Type,请使用 application/json (单个注册) 或 multipart/form-data (批量注册)" )
279+ }
280+
281+ // handleSingleRegister 处理单个用户注册(JSON格式)
282+ func (ctl * ControllerUser ) handleSingleRegister (c * gin.Context ) (* gggin.Response [any ], * gggin.HttpError ) {
251283 req , err := gggin.ShouldBindJSON [RequestRegister ](c )
252284 if err != nil {
253285 return nil , gggin .NewHttpError (http .StatusBadRequest , err .Error ())
@@ -258,7 +290,126 @@ func (ctl *ControllerUser) HandleRegister(c *gin.Context) (*gggin.Response[strin
258290 return nil , service .MapErrorToHttp (err )
259291 }
260292
261- return gggin .Ok , nil
293+ return gggin.NewResponse [any ]("ok" ), nil
294+ }
295+
296+ // handleBatchRegisterFromCSV 处理CSV文件批量注册
297+ func (ctl * ControllerUser ) handleBatchRegisterFromCSV (c * gin.Context ) (* gggin.Response [any ], * gggin.HttpError ) {
298+ file , err := c .FormFile ("file" )
299+ if err != nil {
300+ return nil , gggin .NewHttpError (http .StatusBadRequest , "未找到上传的文件,请使用 'file' 字段上传CSV文件" )
301+ }
302+
303+ src , err := file .Open ()
304+ if err != nil {
305+ return nil , gggin .NewHttpError (http .StatusInternalServerError , "无法读取上传的文件" )
306+ }
307+ defer src .Close ()
308+
309+ reader := csv .NewReader (src )
310+
311+ header , err := reader .Read ()
312+ if err != nil {
313+ return nil , gggin .NewHttpError (http .StatusBadRequest , "CSV文件格式错误:无法读取表头" )
314+ }
315+
316+ // 验证表头格式
317+ expectedHeaders := []string {"username" , "surName" , "givenName" , "mail" , "category" , "role" }
318+ hasHeader := false
319+ if len (header ) == len (expectedHeaders ) {
320+ // 检查前3个关键字段来判断是否有表头,避免误判
321+ h0 := strings .ToLower (header [0 ])
322+ h3 := strings .ToLower (header [3 ])
323+ h4 := strings .ToLower (header [4 ])
324+ if h0 == "username" && h3 == "mail" && h4 == "category" {
325+ hasHeader = true
326+ }
327+ }
328+
329+ var result BatchRegisterResult
330+ rowNum := 1
331+
332+ if ! hasHeader {
333+ if err := ctl .processCSVRow (header , & result , rowNum ); err != nil {
334+ return nil , err
335+ }
336+ rowNum ++
337+ }
338+
339+ for {
340+ record , err := reader .Read ()
341+ if err == io .EOF {
342+ break
343+ }
344+ if err != nil {
345+ result .Failed ++
346+ result .Total ++
347+ result .Failures = append (result .Failures , BatchFailureInfo {
348+ Row : rowNum ,
349+ Username : "" ,
350+ Error : "CSV行格式错误: " + err .Error (),
351+ })
352+ rowNum ++
353+ continue
354+ }
355+
356+ if err := ctl .processCSVRow (record , & result , rowNum ); err != nil {
357+ return nil , err
358+ }
359+ rowNum ++
360+ }
361+
362+ if result .Failed > 0 && result .Success > 0 {
363+ // 部分失败:使用207 Multi-Status
364+ return gggin .NewResponseWithStatusCode [any ](http .StatusMultiStatus , result ), nil
365+ }
366+
367+ return gggin.NewResponse [any ](result ), nil
368+ }
369+
370+ func (ctl * ControllerUser ) processCSVRow (record []string , result * BatchRegisterResult , rowNum int ) * gggin.HttpError {
371+ result .Total ++
372+
373+ if len (record ) != 6 {
374+ result .Failed ++
375+ result .Failures = append (result .Failures , BatchFailureInfo {
376+ Row : rowNum ,
377+ Username : "" ,
378+ Error : "CSV格式错误:应包含6个字段 (username,surName,givenName,mail,category,role)" ,
379+ })
380+ return nil
381+ }
382+
383+ username := strings .TrimSpace (record [0 ])
384+ surName := strings .TrimSpace (record [1 ])
385+ givenName := strings .TrimSpace (record [2 ])
386+ mail := strings .TrimSpace (record [3 ])
387+ category := strings .TrimSpace (record [4 ])
388+ role := strings .TrimSpace (record [5 ])
389+
390+ if username == "" || mail == "" || category == "" || role == "" {
391+ result .Failed ++
392+ result .Failures = append (result .Failures , BatchFailureInfo {
393+ Row : rowNum ,
394+ Username : username ,
395+ Error : "必填字段不能为空 (username, mail, category, role)" ,
396+ })
397+ return nil
398+ }
399+
400+ err := ctl .serviceManager .Register (username , surName , givenName , mail , category , role )
401+ if err != nil {
402+ result .Failed ++
403+ result .Failures = append (result .Failures , BatchFailureInfo {
404+ Row : rowNum ,
405+ Username : username ,
406+ Error : err .Error (),
407+ })
408+ } else {
409+ result .Success ++
410+ }
411+
412+ return nil
262413}
263414
264415// @Summary 删除用户
@@ -372,3 +523,150 @@ func (ctl *ControllerUser) HandleGetCategory(c *gin.Context) (*gggin.Response[se
372523
373524 return gggin .NewResponse (category ), nil
374525}
526+
527+ // ======== 批量操作相关类型定义 ========
528+
529+ // BatchRegisterResult 批量注册结果
530+ type BatchRegisterResult struct {
531+ Success int `json:"success"`
532+ Failed int `json:"failed"`
533+ Total int `json:"total"`
534+ Failures []BatchFailureInfo `json:"failures"`
535+ }
536+
537+ // BatchFailureInfo 批量操作失败信息
538+ type BatchFailureInfo struct {
539+ Row int `json:"row"`
540+ Username string `json:"username"`
541+ Error string `json:"error"`
542+ }
543+
544+ // RequestBatchModifyCategory 批量修改类别请求体
545+ type RequestBatchModifyCategory struct {
546+ UserIds []string `json:"userIds" binding:"required"`
547+ Category string `json:"category" binding:"required"`
548+ }
549+
550+ // RequestBatchModifyRole 批量修改角色请求体
551+ type RequestBatchModifyRole struct {
552+ UserIds []string `json:"userIds" binding:"required"`
553+ Role string `json:"role" binding:"required"`
554+ }
555+
556+ // BatchModifyResult 批量修改结果
557+ type BatchModifyResult struct {
558+ Success int `json:"success"`
559+ Failed int `json:"failed"`
560+ Total int `json:"total"`
561+ Failures []BatchModifyFailure `json:"failures"`
562+ }
563+
564+ // BatchModifyFailure 批量修改失败信息
565+ type BatchModifyFailure struct {
566+ UserId string `json:"userId"`
567+ Error string `json:"error"`
568+ }
569+
570+ // ======== 批量操作功能实现 ========
571+
572+ // @Summary 批量修改账号类型
573+ // @Description 批量修改指定用户的账号类型。需要 ADMIN 角色权限。
574+ // @Tags users
575+ // @Accept json
576+ // @Produce json
577+ // @Param body body RequestBatchModifyCategory true "批量修改账号类型请求"
578+ // @Success 200 {object} object{data=BatchModifyResult} "全部成功:批量修改结果"
579+ // @Success 207 {object} object{data=BatchModifyResult} "部分失败:批量修改结果"
580+ // @Failure 400 {object} object{data=string} "请求参数错误"
581+ // @Failure 401 {object} object{data=string} "未授权访问"
582+ // @Failure 403 {object} object{data=string} "权限不足"
583+ // @Failure 500 {object} object{data=string} "服务器内部错误"
584+ // @Router /users/category [patch]
585+ // @Security BearerAuth
586+ func (ctl * ControllerUser ) HandleBatchModifyCategory (c * gin.Context ) (* gggin.Response [BatchModifyResult ], * gggin.HttpError ) {
587+ _ , ok := gggin .Get [* security.GuardResult ](c , "guard" )
588+ if ! ok {
589+ return nil , ErrHttpGuardFail
590+ }
591+
592+ req , err := gggin.ShouldBindJSON [RequestBatchModifyCategory ](c )
593+ if err != nil {
594+ return nil , gggin .NewHttpError (http .StatusBadRequest , err .Error ())
595+ }
596+
597+ var result BatchModifyResult
598+ result .Total = len (req .UserIds )
599+
600+ for _ , uid := range req .UserIds {
601+ err := ctl .serviceManager .ModifyCategory (uid , req .Category )
602+ if err != nil {
603+ result .Failed ++
604+ result .Failures = append (result .Failures , BatchModifyFailure {
605+ UserId : uid ,
606+ Error : err .Error (),
607+ })
608+ } else {
609+ result .Success ++
610+ }
611+ }
612+
613+ // 根据结果决定返回方式:全部成功返回200,部分失败返回207
614+ if result .Failed > 0 && result .Success > 0 {
615+ // 部分失败:使用207 Multi-Status
616+ return gggin .NewResponseWithStatusCode (http .StatusMultiStatus , result ), nil
617+ }
618+
619+ // 全部成功或全部失败:使用200
620+ return gggin .NewResponse (result ), nil
621+ }
622+
623+ // @Summary 批量修改角色权限
624+ // @Description 批量修改指定用户的角色权限。需要 ADMIN 角色权限。
625+ // @Tags users
626+ // @Accept json
627+ // @Produce json
628+ // @Param body body RequestBatchModifyRole true "批量修改角色权限请求"
629+ // @Success 200 {object} object{data=BatchModifyResult} "全部成功:批量修改结果"
630+ // @Success 207 {object} object{data=BatchModifyResult} "部分失败:批量修改结果"
631+ // @Failure 400 {object} object{data=string} "请求参数错误"
632+ // @Failure 401 {object} object{data=string} "未授权访问"
633+ // @Failure 403 {object} object{data=string} "权限不足"
634+ // @Failure 500 {object} object{data=string} "服务器内部错误"
635+ // @Router /users/role [patch]
636+ // @Security BearerAuth
637+ func (ctl * ControllerUser ) HandleBatchModifyRole (c * gin.Context ) (* gggin.Response [BatchModifyResult ], * gggin.HttpError ) {
638+ _ , ok := gggin .Get [* security.GuardResult ](c , "guard" )
639+ if ! ok {
640+ return nil , ErrHttpGuardFail
641+ }
642+
643+ req , err := gggin.ShouldBindJSON [RequestBatchModifyRole ](c )
644+ if err != nil {
645+ return nil , gggin .NewHttpError (http .StatusBadRequest , err .Error ())
646+ }
647+
648+ var result BatchModifyResult
649+ result .Total = len (req .UserIds )
650+
651+ for _ , uid := range req .UserIds {
652+ err := ctl .serviceManager .GrantRoleByUidAndRoleName (uid , req .Role )
653+ if err != nil {
654+ result .Failed ++
655+ result .Failures = append (result .Failures , BatchModifyFailure {
656+ UserId : uid ,
657+ Error : err .Error (),
658+ })
659+ } else {
660+ result .Success ++
661+ }
662+ }
663+
664+ // 根据结果决定返回方式:全部成功返回200,部分失败返回207
665+ if result .Failed > 0 && result .Success > 0 {
666+ // 部分失败:使用207 Multi-Status
667+ return gggin .NewResponseWithStatusCode (http .StatusMultiStatus , result ), nil
668+ }
669+
670+ // 全部成功或全部失败:使用200
671+ return gggin .NewResponse (result ), nil
672+ }
0 commit comments