server: Go 服务器(REST+WSS 网关+JWT+Argon2+配对+素材直链+任务状态机);修复并发下线 send-on-closed-channel、下发查询 SQL 优先级、配对码原子占用、上传体积上限、JWT 默认密钥告警
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"everypublish/server/internal/api/response"
|
||||
"everypublish/server/internal/models"
|
||||
"everypublish/server/internal/ws"
|
||||
"everypublish/shared/proto"
|
||||
)
|
||||
|
||||
// AccountHandler 账号台账处理器(凭据仅在 Agent 本机保险库,服务器零凭据)
|
||||
type AccountHandler struct {
|
||||
DB *gorm.DB
|
||||
Hub *ws.Hub
|
||||
}
|
||||
|
||||
var platforms = map[string]bool{
|
||||
"douyin": true, "kuaishou": true, "xiaohongshu": true, "bilibili": true,
|
||||
"shipinhao": true, "x": true, "instagram": true, "whatsapp": true, "youtube": true,
|
||||
}
|
||||
|
||||
type accountReq struct {
|
||||
Platform string `json:"platform" binding:"required"`
|
||||
Remark string `json:"remark"`
|
||||
AgentDeviceID uint64 `json:"agentDeviceId"`
|
||||
AccountName string `json:"accountName"`
|
||||
}
|
||||
|
||||
// List 账号台账(筛选 platform/status)
|
||||
func (h *AccountHandler) List(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
size, _ := strconv.Atoi(c.DefaultQuery("size", "20"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if size < 1 || size > 100 {
|
||||
size = 20
|
||||
}
|
||||
q := h.DB.Model(&models.Account{}).Where("workspace_id = ?", c.GetUint64("wsid"))
|
||||
if p := c.Query("platform"); p != "" {
|
||||
q = q.Where("platform = ?", p)
|
||||
}
|
||||
if s := c.Query("status"); s != "" {
|
||||
q = q.Where("status = ?", s)
|
||||
}
|
||||
var total int64
|
||||
q.Count(&total)
|
||||
var list []models.Account
|
||||
q.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&list)
|
||||
response.OK(c, gin.H{"list": list, "total": total, "page": page, "size": size})
|
||||
}
|
||||
|
||||
// Create 新增账号(初始 unbound;凭据只登记元数据)
|
||||
func (h *AccountHandler) Create(c *gin.Context) {
|
||||
var req accountReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Fail(c, http.StatusBadRequest, 1001, "参数不合法")
|
||||
return
|
||||
}
|
||||
if !platforms[req.Platform] {
|
||||
response.Fail(c, http.StatusBadRequest, 1001, "暂不支持该平台")
|
||||
return
|
||||
}
|
||||
account := models.Account{
|
||||
WorkspaceID: c.GetUint64("wsid"),
|
||||
Platform: req.Platform,
|
||||
Remark: req.Remark,
|
||||
AgentDeviceID: req.AgentDeviceID,
|
||||
Status: "unbound",
|
||||
Health: 100,
|
||||
}
|
||||
if err := h.DB.Create(&account).Error; err != nil {
|
||||
response.Fail(c, http.StatusInternalServerError, 5000, "创建失败")
|
||||
return
|
||||
}
|
||||
response.Audit(c, "account.create", "account:"+itoa(account.ID), gin.H{"platform": req.Platform, "remark": req.Remark})
|
||||
response.OK(c, account)
|
||||
}
|
||||
|
||||
// Update 修改账号资料(名称/头像/IP画像)
|
||||
func (h *AccountHandler) Update(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.Fail(c, http.StatusBadRequest, 1001, "参数不合法")
|
||||
return
|
||||
}
|
||||
var req accountReq
|
||||
if err = c.ShouldBindJSON(&req); err != nil {
|
||||
response.Fail(c, http.StatusBadRequest, 1001, "参数不合法")
|
||||
return
|
||||
}
|
||||
updates := map[string]interface{}{"remark": req.Remark}
|
||||
if req.AccountName != "" {
|
||||
updates["account_name"] = req.AccountName
|
||||
}
|
||||
if req.AgentDeviceID > 0 {
|
||||
updates["agent_device_id"] = req.AgentDeviceID
|
||||
}
|
||||
if err = h.DB.Model(&models.Account{}).Where("id = ? and workspace_id = ?", id, c.GetUint64("wsid")).Updates(updates).Error; err != nil {
|
||||
response.Fail(c, http.StatusInternalServerError, 5000, "更新失败")
|
||||
return
|
||||
}
|
||||
response.Audit(c, "account.update", "account:"+itoa(id), gin.H{"remark": req.Remark})
|
||||
response.OK(c, nil)
|
||||
}
|
||||
|
||||
// Delete 删除账号(仅未绑定)
|
||||
func (h *AccountHandler) Delete(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.Fail(c, http.StatusBadRequest, 1001, "参数不合法")
|
||||
return
|
||||
}
|
||||
var account models.Account
|
||||
if err = h.DB.Where("id = ? and workspace_id = ?", id, c.GetUint64("wsid")).First(&account).Error; err != nil {
|
||||
response.Fail(c, http.StatusNotFound, 1004, "账号不存在")
|
||||
return
|
||||
}
|
||||
if account.Status == "active" || account.Status == "binding" {
|
||||
response.Fail(c, http.StatusConflict, 3001, "已绑定或绑定中的账号不可删除")
|
||||
return
|
||||
}
|
||||
if err = h.DB.Delete(&account).Error; err != nil {
|
||||
response.Fail(c, http.StatusInternalServerError, 5000, "删除失败")
|
||||
return
|
||||
}
|
||||
response.Audit(c, "account.delete", "account:"+itoa(id), nil)
|
||||
response.OK(c, nil)
|
||||
}
|
||||
|
||||
// Bind 发起绑定:创建挑战记录(pending),等待客户端扫码(D4 WSS 联动)
|
||||
func (h *AccountHandler) Bind(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.Fail(c, http.StatusBadRequest, 1001, "参数不合法")
|
||||
return
|
||||
}
|
||||
var account models.Account
|
||||
if err = h.DB.Where("id = ? and workspace_id = ?", id, c.GetUint64("wsid")).First(&account).Error; err != nil {
|
||||
response.Fail(c, http.StatusNotFound, 1004, "账号不存在")
|
||||
return
|
||||
}
|
||||
if account.Status == "active" {
|
||||
response.Fail(c, http.StatusConflict, 3002, "账号已绑定")
|
||||
return
|
||||
}
|
||||
challenge := models.Challenge{
|
||||
WorkspaceID: c.GetUint64("wsid"),
|
||||
AccountID: account.ID,
|
||||
Platform: account.Platform,
|
||||
Kind: "pending",
|
||||
Status: "active",
|
||||
Prompt: "等待客户端发起扫码,稍后此处展示二维码",
|
||||
ExpiresAt: time.Now().Add(30 * time.Minute),
|
||||
}
|
||||
err = h.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err = tx.Create(&challenge).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&account).Update("status", "binding").Error
|
||||
})
|
||||
if err != nil {
|
||||
response.Fail(c, http.StatusInternalServerError, 5000, "发起绑定失败")
|
||||
return
|
||||
}
|
||||
// 定向下发:优先账号指定设备,否则工作空间首个在线设备
|
||||
if h.Hub != nil {
|
||||
target := account.AgentDeviceID
|
||||
if target == 0 || !h.Hub.AgentOnline(target) {
|
||||
if online := h.Hub.OnlineDevices(c.GetUint64("wsid")); len(online) > 0 {
|
||||
target = online[0]
|
||||
}
|
||||
}
|
||||
if target != 0 {
|
||||
env := proto.NewEnvelope(uuid.NewString(), proto.TypeChallenge, proto.Challenge{
|
||||
ChallengeID: strconv.FormatUint(challenge.ID, 10),
|
||||
AccountID: strconv.FormatUint(account.ID, 10),
|
||||
Platform: account.Platform,
|
||||
Kind: "qr",
|
||||
Prompt: "请使用手机客户端扫码登录 " + account.Platform,
|
||||
ExpiresAt: challenge.ExpiresAt.UnixMilli(),
|
||||
})
|
||||
_ = h.Hub.SendToAgent(target, env)
|
||||
}
|
||||
}
|
||||
response.Audit(c, "account.bind", "account:"+itoa(account.ID), gin.H{"challengeId": challenge.ID})
|
||||
response.OK(c, gin.H{"challengeId": challenge.ID, "account": account.ID, "status": "binding"})
|
||||
}
|
||||
Reference in New Issue
Block a user