server: Go 服务器(REST+WSS 网关+JWT+Argon2+配对+素材直链+任务状态机);修复并发下线 send-on-closed-channel、下发查询 SQL 优先级、配对码原子占用、上传体积上限、JWT 默认密钥告警

This commit is contained in:
Qiufeng
2026-08-20 20:38:21 +08:00
parent bf25d8beac
commit cc8845dcec
44 changed files with 4995 additions and 0 deletions
+190
View File
@@ -0,0 +1,190 @@
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)
}