Files
qiweimanager-master/helper/kingdee_monitor.go

836 lines
25 KiB
Go

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 != "<nil>" {
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 != "<nil>" {
return msg
}
}
}
data, _ := json.Marshal(result)
return string(data)
}