ai_scheduler/internal/services/callback.go

248 lines
7.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package services
import (
"ai_scheduler/internal/config"
errorcode "ai_scheduler/internal/data/error"
"ai_scheduler/internal/gateway"
"ai_scheduler/internal/pkg/dingtalk"
"ai_scheduler/internal/pkg/dingtalk_contact"
"ai_scheduler/internal/pkg/dingtalk_notable"
"ai_scheduler/internal/pkg/util"
"ai_scheduler/internal/tools_bot"
"context"
"encoding/json"
"strings"
"time"
"github.com/gofiber/fiber/v2"
)
// CallbackService 统一回调入口
type CallbackService struct {
cfg *config.Config
gateway *gateway.Gateway
dingtalkClient *dingtalk.Client
dingtalkContactClient *dingtalk_contact.Client
dingtalkNotableClient *dingtalk_notable.Client
botTool *tools_bot.BotTool
}
func NewCallbackService(cfg *config.Config, gateway *gateway.Gateway, dingtalkClient *dingtalk.Client, dingtalkContactClient *dingtalk_contact.Client, dingtalkNotableClient *dingtalk_notable.Client, botTool *tools_bot.BotTool) *CallbackService {
return &CallbackService{
cfg: cfg,
gateway: gateway,
dingtalkClient: dingtalkClient,
dingtalkContactClient: dingtalkContactClient,
dingtalkNotableClient: dingtalkNotableClient,
botTool: botTool,
}
}
// Envelope 回调统一请求体
type Envelope struct {
Action string `json:"action"`
TaskID string `json:"task_id"`
Data json.RawMessage `json:"data"`
}
// bug_optimization_submit 工单回调
const (
ActionBugOptimizationSubmitDone = "bug_optimization_submit_done" // 工单完成回调
ActionBugOptimizationSubmitUpdate = "bug_optimization_submit_update" // 工单更新回调
)
// BugOptimizationSubmitDoneData 工单完成回调数据
type BugOptimizationSubmitDoneData struct {
Receivers []string `json:"receivers"`
DetailPage string `json:"detail_page"`
Msg string `json:"msg"`
}
// BugOptimizationSubmitUpdateData 工单更新回调数据
type BugOptimizationSubmitUpdateData struct {
BaseId string `json:"base_id"` // 表格ID
SheetId string `json:"sheet_id"` // 表单ID
RecordId string `json:"record_id"` // 记录ID
UnionId string `json:"union_id"` // 钉钉用户 UnionID
Creator string `json:"creator"` // 钉钉用户名称
}
// Callback 统一回调处理
// 头部X-Source-Key / X-Timestamp
func (s *CallbackService) Callback(c *fiber.Ctx) error {
// 读取头
sourceKey := strings.TrimSpace(c.Get("X-Source-Key"))
ts := strings.TrimSpace(c.Get("X-Timestamp"))
// 时间窗口(如果提供了 ts 则校验,否则跳过),窗口 5 分钟
if ts != "" && !validateTimestamp(ts, 5*time.Minute) {
// return errorcode.AuthNotFound
}
// 解析 Envelope
var env Envelope
if err := json.Unmarshal(c.Body(), &env); err != nil {
return errorcode.ParamErr("invalid json: %v", err)
}
if env.Action == "" || env.TaskID == "" {
return errorcode.ParamErr("missing action/task_id")
}
if env.Data == nil {
return errorcode.ParamErr("missing data")
}
switch sourceKey {
case "dingtalk":
return s.handleDingTalkCallback(c, env)
default:
return errorcode.AuthNotFound
}
}
func validateTimestamp(ts string, window time.Duration) bool {
// 期望毫秒时间戳或秒级,简单容错
// 尝试解析为整数
var n int64
for _, base := range []int64{1, 1000} { // 秒或毫秒
if v, ok := parseInt64(ts); ok {
n = v
// 归一为毫秒
if base == 1 && len(ts) <= 10 {
n = n * 1000
}
now := time.Now().UnixMilli()
diff := now - n
if diff < 0 {
diff = -diff
}
if diff <= window.Milliseconds() {
return true
}
}
}
return false
}
func parseInt64(s string) (int64, bool) {
var n int64
for _, ch := range s {
if ch < '0' || ch > '9' {
return 0, false
}
n = n*10 + int64(ch-'0')
}
return n, true
}
func (s *CallbackService) handleDingTalkCallback(c *fiber.Ctx, env Envelope) error {
switch env.Action {
// bug/优化完成回调
case ActionBugOptimizationSubmitDone:
// 获取 session_id
sessionID, ok := s.botTool.GetSessionByTaskID(env.TaskID)
if !ok {
return errorcode.ParamErr("missing session_id for task_id: %s", env.TaskID)
}
var data BugOptimizationSubmitDoneData
if err := json.Unmarshal(env.Data, &data); err != nil {
return errorcode.ParamErr("invalid data type: %v", err)
}
if len(data.Receivers) == 0 {
return errorcode.ParamErr("empty receivers")
}
// 构建接收者
receivers := s.getDingtalkReceivers(c.Context(), data.Receivers)
// 构建跳转链接
var detailPage string
if data.DetailPage != "" {
detailPage = util.BuildJumpLink(data.DetailPage, "去查看")
}
msg := data.Msg
msg = util.ReplacePlaceholder(msg, "receivers", receivers)
msg = util.ReplacePlaceholder(msg, "detail_page", util.EscapeJSONString(detailPage))
s.gateway.SendToUid(sessionID, []byte(msg))
// 删除映射
s.botTool.DelTaskMapping(env.TaskID)
return c.JSON(fiber.Map{"code": 0, "message": "ok"})
case ActionBugOptimizationSubmitUpdate:
// 获取 session_id
// sessionID, ok := s.botTool.GetSessionByTaskID(env.TaskID)
// if !ok {
// return errorcode.ParamErr("missing session_id for task_id: %s", env.TaskID)
// }
var data BugOptimizationSubmitUpdateData
if err := json.Unmarshal(env.Data, &data); err != nil {
return errorcode.ParamErr("invalid data type: %v", err)
}
if data.Creator == "" {
return errorcode.ParamErr("empty creator")
}
// 获取创建者uid
accessToken, _ := s.dingtalkClient.GetAccessToken()
creatorId, err := s.dingtalkContactClient.SearchUserOne(accessToken, data.Creator)
if err != nil {
return errorcode.ParamErr("invalid data type: %v", err)
}
// 获取用户详情
userDetails, err := s.dingtalkClient.QueryUserDetails(c.Context(), creatorId)
if err != nil {
return errorcode.ParamErr("invalid data type: %v", err)
}
if userDetails == nil {
return errorcode.ParamErr("user details not found")
}
unionId := userDetails.UnionID
// 更新记录
ok, err := s.dingtalkNotableClient.UpdateRecord(accessToken, &dingtalk_notable.UpdateRecordReq{
BaseId: data.BaseId,
SheetId: data.SheetId,
RecordId: data.RecordId,
UserId: creatorId,
UnionId: unionId,
})
if err != nil {
return errorcode.ParamErr("invalid data type: %v", err)
}
if !ok {
return errorcode.ParamErr("update record failed")
}
// s.gateway.SendToUid(sessionID, []byte("问题记录即将完成"))
return c.JSON(fiber.Map{"code": 0, "message": "ok"})
default:
return errorcode.ParamErr("unknown action: %s", env.Action)
}
}
// getDingtalkReceivers 解析接收者字符串为 DingTalk 用户 ID 列表
func (s *CallbackService) getDingtalkReceivers(ctx context.Context, receiverIds []string) string {
var receiverNames []string
for _, receiverId := range receiverIds {
userDetails, err := s.dingtalkClient.QueryUserDetails(ctx, receiverId)
if err != nil {
return ""
}
if userDetails == nil {
return ""
}
receiverNames = append(receiverNames, "@"+userDetails.Name)
}
receivers := strings.Join(receiverNames, " ")
return receivers
}