836 lines
25 KiB
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)
|
|
}
|