Initial qiwei secondary development handoff
This commit is contained in:
835
helper/kingdee_monitor.go
Normal file
835
helper/kingdee_monitor.go
Normal file
@@ -0,0 +1,835 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user