281 lines
8.0 KiB
Go
281 lines
8.0 KiB
Go
package services
|
||
|
||
import (
|
||
"ai_scheduler/internal/config"
|
||
"ai_scheduler/internal/data/constants"
|
||
errorcode "ai_scheduler/internal/data/error"
|
||
"ai_scheduler/internal/entitys"
|
||
"ai_scheduler/internal/gateway"
|
||
"ai_scheduler/internal/pkg"
|
||
"ai_scheduler/internal/pkg/dingtalk"
|
||
"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
|
||
dingtalkOldClient *dingtalk.OldClient
|
||
dingtalkContactClient *dingtalk.ContactClient
|
||
dingtalkNotableClient *dingtalk.NotableClient
|
||
botTool *tools_bot.BotTool
|
||
}
|
||
|
||
func NewCallbackService(cfg *config.Config, gateway *gateway.Gateway, dingtalkOldClient *dingtalk.OldClient, dingtalkContactClient *dingtalk.ContactClient, dingtalkNotableClient *dingtalk.NotableClient, botTool *tools_bot.BotTool) *CallbackService {
|
||
return &CallbackService{
|
||
cfg: cfg,
|
||
gateway: gateway,
|
||
dingtalkOldClient: dingtalkOldClient,
|
||
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 {
|
||
// 校验taskId
|
||
sessionID, ok := s.botTool.GetSessionByTaskID(env.TaskID)
|
||
if !ok {
|
||
return errorcode.ParamErr("missing session_id for task_id: %s", env.TaskID)
|
||
}
|
||
ctx := c.Context()
|
||
|
||
switch env.Action {
|
||
// bug/优化完成回调
|
||
case ActionBugOptimizationSubmitDone:
|
||
// 业务处理
|
||
msg, businessErr := s.handleBugOptimizationSubmitDone(ctx, env.Data)
|
||
if businessErr != nil {
|
||
return businessErr
|
||
}
|
||
|
||
// 发送日志
|
||
s.sendStreamLog(sessionID, msg)
|
||
|
||
// 删除映射
|
||
s.botTool.DelTaskMapping(env.TaskID)
|
||
|
||
return c.JSON(fiber.Map{"code": 0, "message": "ok"})
|
||
case ActionBugOptimizationSubmitUpdate:
|
||
// 业务处理
|
||
msg, businessErr := s.handleBugOptimizationSubmitUpdate(ctx, env.Data)
|
||
if businessErr != nil {
|
||
return businessErr
|
||
}
|
||
|
||
s.sendStreamLog(sessionID, msg)
|
||
|
||
return c.JSON(fiber.Map{"code": 0, "message": "ok"})
|
||
default:
|
||
return errorcode.ParamErr("unknown action: %s", env.Action)
|
||
}
|
||
}
|
||
|
||
// handleBugOptimizationSubmitDone 处理 bug 优化提交完成回调
|
||
func (s *CallbackService) handleBugOptimizationSubmitDone(ctx context.Context, taskData json.RawMessage) (string, *errorcode.BusinessErr) {
|
||
var data BugOptimizationSubmitDoneData
|
||
if err := json.Unmarshal(taskData, &data); err != nil {
|
||
return "", errorcode.ParamErr("invalid data type: %v", err)
|
||
}
|
||
|
||
if len(data.Receivers) == 0 {
|
||
return "", errorcode.ParamErr("empty receivers")
|
||
}
|
||
// 构建接收者
|
||
receivers := s.getDingtalkReceivers(ctx, data.Receivers)
|
||
if receivers == "" {
|
||
return "", errorcode.ParamErr("invalid 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))
|
||
|
||
return msg, nil
|
||
}
|
||
|
||
// getDingtalkReceivers 解析接收者字符串为 DingTalk 用户 ID 列表
|
||
func (s *CallbackService) getDingtalkReceivers(ctx context.Context, receiverIds []string) string {
|
||
var receiverNames []string
|
||
for _, receiverId := range receiverIds {
|
||
userDetails, err := s.dingtalkOldClient.QueryUserDetails(ctx, receiverId)
|
||
if err != nil {
|
||
return ""
|
||
}
|
||
if userDetails == nil {
|
||
return ""
|
||
}
|
||
receiverNames = append(receiverNames, "@"+userDetails.Name)
|
||
}
|
||
|
||
receivers := strings.Join(receiverNames, " ")
|
||
|
||
return receivers
|
||
}
|
||
|
||
// sendStreamLog 发送流式日志
|
||
func (s *CallbackService) sendStreamLog(sessionID string, content string) {
|
||
streamLog := entitys.Response{
|
||
Index: constants.BotToolsBugOptimizationSubmit,
|
||
Content: content,
|
||
Type: entitys.ResponseLog,
|
||
}
|
||
streamLogBytes := pkg.JsonByteIgonErr(streamLog)
|
||
s.gateway.SendToUid(sessionID, streamLogBytes)
|
||
}
|
||
|
||
// handleBugOptimizationSubmitUpdate 处理 bug 优化提交更新回调
|
||
func (s *CallbackService) handleBugOptimizationSubmitUpdate(ctx context.Context, taskData json.RawMessage) (string, *errorcode.BusinessErr) {
|
||
var data BugOptimizationSubmitUpdateData
|
||
if err := json.Unmarshal(taskData, &data); err != nil {
|
||
return "", errorcode.ParamErr("invalid data type: %v", err)
|
||
}
|
||
|
||
if data.Creator == "" {
|
||
return "", errorcode.ParamErr("empty creator")
|
||
}
|
||
|
||
// 获取创建者uid
|
||
accessToken, _ := s.dingtalkOldClient.GetAccessToken()
|
||
creatorId, err := s.dingtalkContactClient.SearchUserOne(accessToken, data.Creator)
|
||
if err != nil {
|
||
return "", errorcode.ParamErr("invalid data type: %v", err)
|
||
}
|
||
|
||
// 获取用户详情
|
||
userDetails, err := s.dingtalkOldClient.QueryUserDetails(ctx, 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.UpdateRecordReq{
|
||
BaseId: data.BaseId,
|
||
SheetId: data.SheetId,
|
||
RecordId: data.RecordId,
|
||
OperatorId: tools_bot.BotBugOptimizationSubmitAdminUnionId,
|
||
CreatorUnionId: unionId,
|
||
})
|
||
if err != nil {
|
||
return "", errorcode.ParamErr("invalid data type: %v", err)
|
||
}
|
||
if !ok {
|
||
return "", errorcode.ParamErr("update record failed")
|
||
}
|
||
|
||
return "问题记录即将完成", nil
|
||
}
|