248 lines
7.0 KiB
Go
248 lines
7.0 KiB
Go
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
|
||
}
|