package main import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "net/http/cookiejar" "net/url" "sort" "strings" "sync" "time" ) const ( defaultKingdeePollIntervalSeconds = 60 defaultKingdeeFormID = "SAL_SaleOrder" defaultKingdeeCompletedValue = "排产已完成" defaultKingdeeNotifyTemplate = "您好,您的订单 {{billNo}} 已完成排产,后续我们会继续跟进生产进度。" defaultKingdeeLCID = 2052 kingdeeMonitorStatusStopped = "stopped" kingdeeMonitorStatusRunning = "running" kingdeeMonitorStatusPolling = "polling" kingdeeMonitorStatusError = "error" ) type KingdeeMonitorConfig struct { Enabled bool `json:"enabled"` BaseURL string `json:"baseUrl"` AcctID string `json:"acctId"` Username string `json:"username"` Password string `json:"password"` LCID int `json:"lcid"` PollIntervalSeconds int `json:"pollIntervalSeconds"` FormID string `json:"formId"` BillNoFieldKey string `json:"billNoFieldKey"` OrderIDFieldKey string `json:"orderIdFieldKey"` CustomerFieldKey string `json:"customerFieldKey"` StatusFieldKey string `json:"statusFieldKey"` CompletedValue string `json:"completedValue"` ModifyTimeFieldKey string `json:"modifyTimeFieldKey"` NotifyTemplate string `json:"notifyTemplate"` CustomerMappings map[string]KingdeeCustomerMapping `json:"customerMappings"` } type KingdeeCustomerMapping struct { RobotID string `json:"robotId"` ConversationID string `json:"conversationId"` Remark string `json:"remark"` } type KingdeeMonitorState struct { Running bool `json:"running"` Status string `json:"status"` LastPollAt int64 `json:"lastPollAt"` LastCursorTime string `json:"lastCursorTime"` LastError string `json:"lastError"` LastErrorAt int64 `json:"lastErrorAt"` TotalPolled int `json:"totalPolled"` TotalNotified int `json:"totalNotified"` TotalUnmapped int `json:"totalUnmapped"` NotifiedOrders map[string]KingdeeNotifiedLog `json:"notifiedOrders"` RecentNotices []KingdeeNoticeRecord `json:"recentNotices"` RecentErrors []KingdeeErrorRecord `json:"recentErrors"` } type KingdeeNotifiedLog struct { OrderKey string `json:"orderKey"` BillNo string `json:"billNo"` CustomerNumber string `json:"customerNumber"` StatusValue string `json:"statusValue"` NotifiedAt int64 `json:"notifiedAt"` } type KingdeeNoticeRecord struct { OrderKey string `json:"orderKey"` BillNo string `json:"billNo"` CustomerNumber string `json:"customerNumber"` RobotID string `json:"robotId"` ConversationID string `json:"conversationId"` Message string `json:"message"` NotifiedAt int64 `json:"notifiedAt"` } type KingdeeErrorRecord struct { OrderKey string `json:"orderKey,omitempty"` BillNo string `json:"billNo,omitempty"` CustomerNumber string `json:"customerNumber,omitempty"` Message string `json:"message"` CreatedAt int64 `json:"createdAt"` } type KingdeeOrder struct { OrderID string `json:"orderId"` BillNo string `json:"billNo"` CustomerNumber string `json:"customerNumber"` StatusValue string `json:"statusValue"` ModifyTime string `json:"modifyTime"` } type KingdeeRunResult struct { Success bool `json:"success"` Message string `json:"message"` Polled int `json:"polled"` Matched int `json:"matched"` Notified int `json:"notified"` Skipped int `json:"skipped"` Unmapped int `json:"unmapped"` Failed int `json:"failed"` Orders []KingdeeOrder `json:"orders"` State KingdeeMonitorState `json:"state"` } type KingdeeMonitor struct { mu sync.Mutex stopCh chan struct{} running bool polling bool client *http.Client loggedIn bool } var ( kingdeeMonitor *KingdeeMonitor kingdeeMonitorOnce sync.Once ) func getKingdeeMonitor() *KingdeeMonitor { kingdeeMonitorOnce.Do(func() { jar, _ := cookiejar.New(nil) kingdeeMonitor = &KingdeeMonitor{ client: &http.Client{ Timeout: 30 * time.Second, Jar: jar, }, } }) return kingdeeMonitor } func startKingdeeMonitorFromConfig() { cfg, err := readKingdeeMonitorConfig() if err != nil { if globalLogger != nil { globalLogger.Error("[金蝶监听] 读取配置失败: %v", err) } return } if cfg.Enabled { getKingdeeMonitor().Start() } } func kingdeeMonitorConfigPath() string { return resolveAutoReplyPath("config/kingdee_monitor.json") } func kingdeeMonitorStatePath() string { return resolveAutoReplyPath("config/kingdee_monitor_state.json") } func defaultKingdeeMonitorConfig() KingdeeMonitorConfig { return KingdeeMonitorConfig{ Enabled: false, LCID: defaultKingdeeLCID, PollIntervalSeconds: defaultKingdeePollIntervalSeconds, FormID: defaultKingdeeFormID, BillNoFieldKey: "FBillNo", OrderIDFieldKey: "FID", CustomerFieldKey: "FCustId.FNumber", StatusFieldKey: "", CompletedValue: defaultKingdeeCompletedValue, ModifyTimeFieldKey: "FModifyDate", NotifyTemplate: defaultKingdeeNotifyTemplate, CustomerMappings: map[string]KingdeeCustomerMapping{}, } } func defaultKingdeeMonitorState() KingdeeMonitorState { return KingdeeMonitorState{ Status: kingdeeMonitorStatusStopped, NotifiedOrders: map[string]KingdeeNotifiedLog{}, RecentNotices: []KingdeeNoticeRecord{}, RecentErrors: []KingdeeErrorRecord{}, } } func readKingdeeMonitorConfig() (KingdeeMonitorConfig, error) { cfg := defaultKingdeeMonitorConfig() if err := readJSONFile(kingdeeMonitorConfigPath(), &cfg); err != nil { return cfg, err } normalizeKingdeeMonitorConfig(&cfg) return cfg, nil } func saveKingdeeMonitorConfig(cfg KingdeeMonitorConfig) error { if strings.TrimSpace(cfg.Password) == "******" { existing, err := readKingdeeMonitorConfig() if err == nil { cfg.Password = existing.Password } } normalizeKingdeeMonitorConfig(&cfg) if err := atomicWriteJSON(kingdeeMonitorConfigPath(), cfg); err != nil { return err } monitor := getKingdeeMonitor() if cfg.Enabled { monitor.Start() } else { monitor.Stop() } return nil } func readKingdeeMonitorState() (KingdeeMonitorState, error) { state := defaultKingdeeMonitorState() if err := readJSONFile(kingdeeMonitorStatePath(), &state); err != nil { return state, err } normalizeKingdeeMonitorState(&state) return state, nil } func saveKingdeeMonitorState(state KingdeeMonitorState) error { normalizeKingdeeMonitorState(&state) return atomicWriteJSON(kingdeeMonitorStatePath(), state) } func normalizeKingdeeMonitorConfig(cfg *KingdeeMonitorConfig) { if cfg == nil { return } cfg.BaseURL = strings.TrimRight(strings.TrimSpace(cfg.BaseURL), "/") cfg.AcctID = strings.TrimSpace(cfg.AcctID) cfg.Username = strings.TrimSpace(cfg.Username) if cfg.LCID <= 0 { cfg.LCID = defaultKingdeeLCID } if cfg.PollIntervalSeconds <= 0 { cfg.PollIntervalSeconds = defaultKingdeePollIntervalSeconds } if cfg.PollIntervalSeconds < 10 { cfg.PollIntervalSeconds = 10 } cfg.FormID = strings.TrimSpace(cfg.FormID) if cfg.FormID == "" { cfg.FormID = defaultKingdeeFormID } cfg.BillNoFieldKey = strings.TrimSpace(cfg.BillNoFieldKey) if cfg.BillNoFieldKey == "" { cfg.BillNoFieldKey = "FBillNo" } cfg.OrderIDFieldKey = strings.TrimSpace(cfg.OrderIDFieldKey) if cfg.OrderIDFieldKey == "" { cfg.OrderIDFieldKey = "FID" } cfg.CustomerFieldKey = strings.TrimSpace(cfg.CustomerFieldKey) if cfg.CustomerFieldKey == "" { cfg.CustomerFieldKey = "FCustId.FNumber" } cfg.StatusFieldKey = strings.TrimSpace(cfg.StatusFieldKey) cfg.CompletedValue = strings.TrimSpace(cfg.CompletedValue) if cfg.CompletedValue == "" { cfg.CompletedValue = defaultKingdeeCompletedValue } cfg.ModifyTimeFieldKey = strings.TrimSpace(cfg.ModifyTimeFieldKey) if cfg.ModifyTimeFieldKey == "" { cfg.ModifyTimeFieldKey = "FModifyDate" } cfg.NotifyTemplate = strings.TrimSpace(cfg.NotifyTemplate) if cfg.NotifyTemplate == "" { cfg.NotifyTemplate = defaultKingdeeNotifyTemplate } if cfg.CustomerMappings == nil { cfg.CustomerMappings = map[string]KingdeeCustomerMapping{} } normalized := make(map[string]KingdeeCustomerMapping, len(cfg.CustomerMappings)) for key, mapping := range cfg.CustomerMappings { customerNumber := strings.TrimSpace(key) if customerNumber == "" { continue } mapping.RobotID = strings.TrimSpace(mapping.RobotID) mapping.ConversationID = strings.TrimSpace(mapping.ConversationID) mapping.Remark = strings.TrimSpace(mapping.Remark) normalized[customerNumber] = mapping } cfg.CustomerMappings = normalized } func normalizeKingdeeMonitorState(state *KingdeeMonitorState) { if state == nil { return } if state.Status == "" { state.Status = kingdeeMonitorStatusStopped } if state.NotifiedOrders == nil { state.NotifiedOrders = map[string]KingdeeNotifiedLog{} } if state.RecentNotices == nil { state.RecentNotices = []KingdeeNoticeRecord{} } if state.RecentErrors == nil { state.RecentErrors = []KingdeeErrorRecord{} } if len(state.RecentNotices) > 30 { state.RecentNotices = state.RecentNotices[:30] } if len(state.RecentErrors) > 30 { state.RecentErrors = state.RecentErrors[:30] } } func maskedKingdeeConfig(cfg KingdeeMonitorConfig) KingdeeMonitorConfig { if strings.TrimSpace(cfg.Password) != "" { cfg.Password = "******" } return cfg } func (m *KingdeeMonitor) Start() { m.mu.Lock() if m.running { m.mu.Unlock() return } m.stopCh = make(chan struct{}) m.running = true m.mu.Unlock() m.setRuntimeStatus(kingdeeMonitorStatusRunning, "") go m.loop() if globalLogger != nil { globalLogger.Info("[金蝶监听] 已启动") } } func (m *KingdeeMonitor) Stop() { m.mu.Lock() if !m.running { m.mu.Unlock() m.setRuntimeStatus(kingdeeMonitorStatusStopped, "") return } close(m.stopCh) m.running = false m.polling = false m.loggedIn = false m.mu.Unlock() m.setRuntimeStatus(kingdeeMonitorStatusStopped, "") if globalLogger != nil { globalLogger.Info("[金蝶监听] 已停止") } } func (m *KingdeeMonitor) IsRunning() bool { m.mu.Lock() defer m.mu.Unlock() return m.running } func (m *KingdeeMonitor) loop() { for { cfg, err := readKingdeeMonitorConfig() if err != nil { m.recordError("", "", "", fmt.Sprintf("读取金蝶监听配置失败: %v", err)) } else if cfg.Enabled { _, _ = m.RunOnce(false) } interval := time.Duration(defaultKingdeePollIntervalSeconds) * time.Second if cfg.PollIntervalSeconds > 0 { interval = time.Duration(cfg.PollIntervalSeconds) * time.Second } timer := time.NewTimer(interval) select { case <-timer.C: case <-m.stopCh: timer.Stop() return } } } func (m *KingdeeMonitor) RunOnce(manual bool) (KingdeeRunResult, error) { m.mu.Lock() if m.polling { m.mu.Unlock() return KingdeeRunResult{Success: false, Message: "金蝶监听正在执行,请稍后再试"}, errors.New("kingdee poll already running") } m.polling = true m.mu.Unlock() defer func() { m.mu.Lock() m.polling = false if m.running { m.mu.Unlock() m.setRuntimeStatus(kingdeeMonitorStatusRunning, "") } else { m.mu.Unlock() } }() m.setRuntimeStatus(kingdeeMonitorStatusPolling, "") cfg, err := readKingdeeMonitorConfig() if err != nil { m.recordError("", "", "", fmt.Sprintf("读取金蝶监听配置失败: %v", err)) return KingdeeRunResult{Success: false, Message: err.Error()}, err } if !manual && !cfg.Enabled { state, _ := readKingdeeMonitorState() return KingdeeRunResult{Success: true, Message: "金蝶监听未开启", State: state}, nil } if err := validateKingdeeMonitorConfig(cfg, true); err != nil { m.recordError("", "", "", err.Error()) return KingdeeRunResult{Success: false, Message: err.Error()}, err } state, err := readKingdeeMonitorState() if err != nil { m.recordError("", "", "", fmt.Sprintf("读取金蝶监听状态失败: %v", err)) return KingdeeRunResult{Success: false, Message: err.Error()}, err } orders, err := m.fetchCompletedOrders(cfg, state.LastCursorTime) if err != nil { m.recordError("", "", "", fmt.Sprintf("查询金蝶订单失败: %v", err)) return KingdeeRunResult{Success: false, Message: err.Error()}, err } result := KingdeeRunResult{Success: true, Message: "扫描完成", Polled: len(orders), Orders: orders} now := time.Now().Unix() state.LastPollAt = now state.Running = m.IsRunning() if state.Running { state.Status = kingdeeMonitorStatusRunning } else { state.Status = kingdeeMonitorStatusStopped } state.TotalPolled += len(orders) for _, order := range orders { if !kingdeeOrderCompleted(order, cfg.CompletedValue) { continue } result.Matched++ orderKey := kingdeeOrderNotifyKey(order, cfg.CompletedValue) if _, exists := state.NotifiedOrders[orderKey]; exists { result.Skipped++ continue } mapping, ok := cfg.CustomerMappings[strings.TrimSpace(order.CustomerNumber)] if !ok || strings.TrimSpace(mapping.ConversationID) == "" || strings.TrimSpace(mapping.RobotID) == "" { result.Unmapped++ state.TotalUnmapped++ appendKingdeeError(&state, order, fmt.Sprintf("ERP客户编码 %s 未配置通知映射", order.CustomerNumber)) continue } message := renderKingdeeNotifyMessage(cfg.NotifyTemplate, order) if err := sendKingdeeOrderNotice(mapping, message); err != nil { result.Failed++ appendKingdeeError(&state, order, fmt.Sprintf("发送企微通知失败: %v", err)) continue } result.Notified++ state.TotalNotified++ notified := KingdeeNotifiedLog{ OrderKey: orderKey, BillNo: order.BillNo, CustomerNumber: order.CustomerNumber, StatusValue: order.StatusValue, NotifiedAt: now, } state.NotifiedOrders[orderKey] = notified state.RecentNotices = append([]KingdeeNoticeRecord{{ OrderKey: orderKey, BillNo: order.BillNo, CustomerNumber: order.CustomerNumber, RobotID: mapping.RobotID, ConversationID: mapping.ConversationID, Message: message, NotifiedAt: now, }}, state.RecentNotices...) } if cursor := newestKingdeeModifyTime(orders, state.LastCursorTime); cursor != "" { state.LastCursorTime = cursor } if result.Failed == 0 && result.Unmapped == 0 { state.LastError = "" } if state.LastError != "" { state.Status = kingdeeMonitorStatusError } normalizeKingdeeMonitorState(&state) if err := saveKingdeeMonitorState(state); err != nil { return result, err } result.State = state return result, nil } func validateKingdeeMonitorConfig(cfg KingdeeMonitorConfig, requireRule bool) error { if strings.TrimSpace(cfg.BaseURL) == "" { return errors.New("请填写金蝶服务地址") } if strings.TrimSpace(cfg.AcctID) == "" { return errors.New("请填写金蝶账套ID") } if strings.TrimSpace(cfg.Username) == "" { return errors.New("请填写金蝶用户名") } if strings.TrimSpace(cfg.Password) == "" { return errors.New("请填写金蝶密码") } if requireRule { if strings.TrimSpace(cfg.StatusFieldKey) == "" { return errors.New("请填写排产状态字段") } if strings.TrimSpace(cfg.CompletedValue) == "" { return errors.New("请填写完成状态值") } } if _, err := url.ParseRequestURI(cfg.BaseURL); err != nil { return fmt.Errorf("金蝶服务地址不正确: %w", err) } return nil } func (m *KingdeeMonitor) TestConnection(cfg KingdeeMonitorConfig) error { if strings.TrimSpace(cfg.Password) == "******" { existing, err := readKingdeeMonitorConfig() if err == nil { cfg.Password = existing.Password } } normalizeKingdeeMonitorConfig(&cfg) if err := validateKingdeeMonitorConfig(cfg, false); err != nil { return err } return m.login(cfg) } func (m *KingdeeMonitor) login(cfg KingdeeMonitorConfig) error { payload := map[string]interface{}{ "acctID": cfg.AcctID, "username": cfg.Username, "password": cfg.Password, "lcid": cfg.LCID, } var result map[string]interface{} if err := m.postKingdeeJSON(cfg, "/K3Cloud/Kingdee.BOS.WebApi.ServicesStub.AuthService.ValidateUser.common.kdsvc", payload, &result); err != nil { return err } if loginOK(result) { m.mu.Lock() m.loggedIn = true m.mu.Unlock() return nil } return fmt.Errorf("金蝶登录失败: %s", kingdeeResponseMessage(result)) } func loginOK(result map[string]interface{}) bool { if result == nil { return false } if v, ok := result["LoginResultType"].(float64); ok && int(v) == 1 { return true } if v, ok := result["LoginResultType"].(int); ok && v == 1 { return true } if v, ok := result["Result"].(map[string]interface{}); ok { if t, ok := v["LoginResultType"].(float64); ok && int(t) == 1 { return true } if responseStatus, ok := v["ResponseStatus"].(map[string]interface{}); ok { if isSuccess, ok := responseStatus["IsSuccess"].(bool); ok && isSuccess { return true } } } return false } func (m *KingdeeMonitor) fetchCompletedOrders(cfg KingdeeMonitorConfig, cursor string) ([]KingdeeOrder, error) { if err := m.ensureLogin(cfg); err != nil { return nil, err } fieldKeys := kingdeeFieldKeys(cfg) filter := buildKingdeeFilter(cfg, cursor) const limit = 200 orders := []KingdeeOrder{} for startRow := 0; ; startRow += limit { payload := map[string]interface{}{ "data": map[string]interface{}{ "FormId": cfg.FormID, "FieldKeys": strings.Join(fieldKeys, ","), "FilterString": filter, "OrderString": cfg.ModifyTimeFieldKey + " ASC", "TopRowCount": 0, "StartRow": startRow, "Limit": limit, }, } var rows [][]interface{} if err := m.postKingdeeJSON(cfg, "/K3Cloud/Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.ExecuteBillQuery.common.kdsvc", payload, &rows); err != nil { m.mu.Lock() m.loggedIn = false m.mu.Unlock() if loginErr := m.ensureLogin(cfg); loginErr == nil { err = m.postKingdeeJSON(cfg, "/K3Cloud/Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.ExecuteBillQuery.common.kdsvc", payload, &rows) } if err != nil { return nil, err } } for _, row := range rows { order := parseKingdeeOrderRow(row) if strings.TrimSpace(order.StatusValue) == "" && cfg.StatusFieldKey != "" { continue } orders = append(orders, order) } if len(rows) < limit { break } } return orders, nil } func (m *KingdeeMonitor) ensureLogin(cfg KingdeeMonitorConfig) error { m.mu.Lock() loggedIn := m.loggedIn m.mu.Unlock() if loggedIn { return nil } return m.login(cfg) } func (m *KingdeeMonitor) postKingdeeJSON(cfg KingdeeMonitorConfig, path string, payload interface{}, target interface{}) error { body, err := json.Marshal(payload) if err != nil { return err } req, err := http.NewRequest(http.MethodPost, cfg.BaseURL+path, bytes.NewReader(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") resp, err := m.client.Do(req) if err != nil { return err } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return err } if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("金蝶接口状态码 %d: %s", resp.StatusCode, string(respBody)) } if len(strings.TrimSpace(string(respBody))) == 0 { return nil } if err := json.Unmarshal(respBody, target); err != nil { return fmt.Errorf("解析金蝶响应失败: %w, body=%s", err, string(respBody)) } return nil } func kingdeeFieldKeys(cfg KingdeeMonitorConfig) []string { return []string{ cfg.OrderIDFieldKey, cfg.BillNoFieldKey, cfg.CustomerFieldKey, cfg.StatusFieldKey, cfg.ModifyTimeFieldKey, } } func buildKingdeeFilter(cfg KingdeeMonitorConfig, cursor string) string { parts := []string{} if strings.TrimSpace(cursor) != "" { parts = append(parts, fmt.Sprintf("%s > '%s'", cfg.ModifyTimeFieldKey, strings.ReplaceAll(cursor, "'", "''"))) } if strings.TrimSpace(cfg.StatusFieldKey) != "" && strings.TrimSpace(cfg.CompletedValue) != "" { parts = append(parts, fmt.Sprintf("%s = '%s'", cfg.StatusFieldKey, strings.ReplaceAll(cfg.CompletedValue, "'", "''"))) } return strings.Join(parts, " AND ") } func parseKingdeeOrderRow(row []interface{}) KingdeeOrder { return KingdeeOrder{ OrderID: valueToStringAt(row, 0), BillNo: valueToStringAt(row, 1), CustomerNumber: valueToStringAt(row, 2), StatusValue: valueToStringAt(row, 3), ModifyTime: valueToStringAt(row, 4), } } func valueToStringAt(row []interface{}, index int) string { if index < 0 || index >= len(row) { return "" } return strings.TrimSpace(fmt.Sprint(row[index])) } func kingdeeOrderCompleted(order KingdeeOrder, completedValue string) bool { return strings.TrimSpace(order.StatusValue) == strings.TrimSpace(completedValue) } func kingdeeOrderNotifyKey(order KingdeeOrder, completedValue string) string { id := strings.TrimSpace(order.OrderID) if id == "" { id = strings.TrimSpace(order.BillNo) } return id + "|" + strings.TrimSpace(completedValue) } func newestKingdeeModifyTime(orders []KingdeeOrder, current string) string { candidates := make([]string, 0, len(orders)+1) if strings.TrimSpace(current) != "" { candidates = append(candidates, strings.TrimSpace(current)) } for _, order := range orders { if strings.TrimSpace(order.ModifyTime) != "" { candidates = append(candidates, strings.TrimSpace(order.ModifyTime)) } } sort.Strings(candidates) if len(candidates) == 0 { return "" } return candidates[len(candidates)-1] } func renderKingdeeNotifyMessage(template string, order KingdeeOrder) string { replacements := map[string]string{ "{{orderId}}": order.OrderID, "{{billNo}}": order.BillNo, "{{customerNumber}}": order.CustomerNumber, "{{statusValue}}": order.StatusValue, "{{modifyTime}}": order.ModifyTime, } result := template for old, newValue := range replacements { result = strings.ReplaceAll(result, old, newValue) } return result } func sendKingdeeOrderNotice(mapping KingdeeCustomerMapping, message string) error { params := map[string]interface{}{ "robotId": mapping.RobotID, "conversationId": mapping.ConversationID, } clientID := GetClientIdFromRequestParams(params) if clientID == 0 { return fmt.Errorf("未找到在线企微账号: %s", mapping.RobotID) } return sendAutoReplyText(clientID, mapping.ConversationID, message) } func appendKingdeeError(state *KingdeeMonitorState, order KingdeeOrder, message string) { now := time.Now().Unix() state.LastError = message state.LastErrorAt = now state.Status = kingdeeMonitorStatusError state.RecentErrors = append([]KingdeeErrorRecord{{ OrderKey: kingdeeOrderNotifyKey(order, order.StatusValue), BillNo: order.BillNo, CustomerNumber: order.CustomerNumber, Message: message, CreatedAt: now, }}, state.RecentErrors...) normalizeKingdeeMonitorState(state) } func (m *KingdeeMonitor) recordError(orderKey string, billNo string, customerNumber string, message string) { state, _ := readKingdeeMonitorState() now := time.Now().Unix() state.LastError = message state.LastErrorAt = now state.Status = kingdeeMonitorStatusError state.RecentErrors = append([]KingdeeErrorRecord{{ OrderKey: orderKey, BillNo: billNo, CustomerNumber: customerNumber, Message: message, CreatedAt: now, }}, state.RecentErrors...) _ = saveKingdeeMonitorState(state) } func (m *KingdeeMonitor) setRuntimeStatus(status string, lastError string) { state, _ := readKingdeeMonitorState() state.Running = status == kingdeeMonitorStatusRunning || status == kingdeeMonitorStatusPolling state.Status = status if lastError != "" { state.LastError = lastError state.LastErrorAt = time.Now().Unix() } _ = saveKingdeeMonitorState(state) } func kingdeeResponseMessage(result map[string]interface{}) string { if result == nil { return "空响应" } if msg := strings.TrimSpace(fmt.Sprint(result["Message"])); msg != "" && msg != "" { return msg } if v, ok := result["Result"].(map[string]interface{}); ok { if responseStatus, ok := v["ResponseStatus"].(map[string]interface{}); ok { if errorsValue, ok := responseStatus["Errors"].([]interface{}); ok && len(errorsValue) > 0 { return fmt.Sprint(errorsValue[0]) } if msg := strings.TrimSpace(fmt.Sprint(responseStatus["Message"])); msg != "" && msg != "" { return msg } } } data, _ := json.Marshal(result) return string(data) }