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"}) }