package handlers import ( "net/http" "strconv" "strings" "github.com/gin-gonic/gin" "gorm.io/gorm" "everypublish/server/internal/api/response" "everypublish/server/internal/auth" "everypublish/server/internal/config" "everypublish/server/internal/models" ) // MemberHandler 成员处理器 type MemberHandler struct { DB *gorm.DB Cfg *config.Config } type inviteReq struct { Email string `json:"email" binding:"required"` Role string `json:"role" binding:"required"` } type joinReq struct { Token string `json:"token" binding:"required"` } type roleReq struct { Role string `json:"role" binding:"required"` } var memberRoles = map[string]bool{"admin": true, "operator": true, "reviewer": true, "viewer": true} func isAdminRole(mrole string) bool { return mrole == "owner" || mrole == "admin" } type memberRow struct { models.Member Email string `json:"email"` Nickname string `json:"nickname"` } // List 成员列表(含用户资料) func (h *MemberHandler) List(c *gin.Context) { wsid := c.GetUint64("wsid") var rows []memberRow h.DB.Table("members").Select("members.*, users.email as email, users.nickname as nickname"). Joins("join users on users.id = members.user_id"). Where("members.workspace_id = ?", wsid).Order("members.id asc").Scan(&rows) response.OK(c, gin.H{"list": rows}) } // Invite 生成邀请链接(owner/admin) func (h *MemberHandler) Invite(c *gin.Context) { if !isAdminRole(c.GetString("mrole")) { response.Fail(c, http.StatusForbidden, 1003, "仅管理员可邀请成员") return } var req inviteReq if err := c.ShouldBindJSON(&req); err != nil { response.Fail(c, http.StatusBadRequest, 1001, "参数不合法") return } req.Role = strings.ToLower(strings.TrimSpace(req.Role)) if !memberRoles[req.Role] { response.Fail(c, http.StatusBadRequest, 1001, "角色不合法") return } wsid := c.GetUint64("wsid") var existing memberRow err := h.DB.Table("members").Select("members.id"). Joins("join users on users.id = members.user_id"). Where("members.workspace_id = ? and users.email = ?", wsid, strings.ToLower(req.Email)). Scan(&existing).Error if err == nil && existing.ID > 0 { response.Fail(c, http.StatusConflict, 2004, "该邮箱已是成员") return } token, err := auth.IssueInvite(h.Cfg.JWTSecret, wsid, strings.ToLower(req.Email), req.Role) if err != nil { response.Fail(c, http.StatusInternalServerError, 5000, "生成邀请失败") return } response.Audit(c, "member.invite", "workspace:"+itoa(wsid), gin.H{"email": req.Email, "role": req.Role}) response.OK(c, gin.H{"token": token, "link": h.Cfg.PublicBaseURL + "/invite?t=" + token}) } // Join 接受邀请(需登录,邮箱须与邀请一致) func (h *MemberHandler) Join(c *gin.Context) { var req joinReq if err := c.ShouldBindJSON(&req); err != nil { response.Fail(c, http.StatusBadRequest, 1001, "参数不合法") return } claims, err := auth.ParseInvite(h.Cfg.JWTSecret, req.Token) if err != nil { response.Fail(c, http.StatusBadRequest, 2005, "邀请链接无效或已过期") return } var user models.User if err = h.DB.First(&user, c.GetUint64("uid")).Error; err != nil { response.Fail(c, http.StatusUnauthorized, 1002, "用户不存在") return } if !strings.EqualFold(user.Email, claims.Email) { response.Fail(c, http.StatusForbidden, 1003, "邀请链接与当前账号邮箱不匹配") return } member := models.Member{WorkspaceID: claims.WorkspaceID, UserID: user.ID, Role: claims.Role} err = h.DB.Where("workspace_id = ? and user_id = ?", claims.WorkspaceID, user.ID).First(&models.Member{}).Error if err == nil { response.Fail(c, http.StatusConflict, 2004, "你已是该工作空间成员") return } if err = h.DB.Create(&member).Error; err != nil { response.Fail(c, http.StatusInternalServerError, 5000, "加入失败") return } response.Audit(c, "member.join", "workspace:"+itoa(claims.WorkspaceID), gin.H{"role": claims.Role}) response.OK(c, nil) } // UpdateRole 修改成员角色(owner/admin;不可改 owner) func (h *MemberHandler) UpdateRole(c *gin.Context) { if !isAdminRole(c.GetString("mrole")) { response.Fail(c, http.StatusForbidden, 1003, "仅管理员可修改角色") return } id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { response.Fail(c, http.StatusBadRequest, 1001, "参数不合法") return } var req roleReq if err = c.ShouldBindJSON(&req); err != nil { response.Fail(c, http.StatusBadRequest, 1001, "参数不合法") return } req.Role = strings.ToLower(strings.TrimSpace(req.Role)) if !memberRoles[req.Role] && req.Role != "owner" { response.Fail(c, http.StatusBadRequest, 1001, "角色不合法") return } var member models.Member if err = h.DB.Where("id = ? and workspace_id = ?", id, c.GetUint64("wsid")).First(&member).Error; err != nil { response.Fail(c, http.StatusNotFound, 1004, "成员不存在") return } if member.Role == "owner" { response.Fail(c, http.StatusForbidden, 1003, "不可修改 owner 角色") return } if err = h.DB.Model(&member).Update("role", req.Role).Error; err != nil { response.Fail(c, http.StatusInternalServerError, 5000, "更新失败") return } response.Audit(c, "member.role", "member:"+itoa(id), gin.H{"role": req.Role}) response.OK(c, nil) } // Remove 移除成员(owner/admin;不可移除 owner) func (h *MemberHandler) Remove(c *gin.Context) { if !isAdminRole(c.GetString("mrole")) { response.Fail(c, http.StatusForbidden, 1003, "仅管理员可移除成员") return } id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { response.Fail(c, http.StatusBadRequest, 1001, "参数不合法") return } var member models.Member if err = h.DB.Where("id = ? and workspace_id = ?", id, c.GetUint64("wsid")).First(&member).Error; err != nil { response.Fail(c, http.StatusNotFound, 1004, "成员不存在") return } if member.Role == "owner" { response.Fail(c, http.StatusForbidden, 1003, "不可移除 owner") return } if err = h.DB.Delete(&member).Error; err != nil { response.Fail(c, http.StatusInternalServerError, 5000, "移除失败") return } response.Audit(c, "member.remove", "member:"+itoa(id), nil) response.OK(c, nil) }