196 lines
6.3 KiB
Go
196 lines
6.3 KiB
Go
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"})
|
||
}
|