li.shaoyi 1 ngày trước cách đây
mục cha
commit
dc269a4a98

+ 165 - 0
public/proto/mtp.js

@@ -4206,6 +4206,38 @@ var $root = ($protobuf.roots["default"] || ($protobuf.roots["default"] = new $pr
       }
     }
   },
+  EnumDicItemChangeNtf: {
+    fields: {
+      Header: {
+        type: "MessageHead",
+        id: 1
+      },
+      NtfHeader: {
+        type: "NotifyHead",
+        id: 2
+      },
+      EnumDicCode: {
+        type: "string",
+        id: 3
+      },
+      EnumItemName: {
+        type: "uint32",
+        id: 4
+      },
+      EnumItemStatus: {
+        type: "uint32",
+        id: 5
+      },
+      NotifyTime: {
+        type: "string",
+        id: 6
+      },
+      Timestamp: {
+        type: "uint64",
+        id: 7
+      }
+    }
+  },
   OrderReq: {
     fields: {
       Header: {
@@ -52849,6 +52881,139 @@ var $root = ($protobuf.roots["default"] || ($protobuf.roots["default"] = new $pr
         id: 11
       }
     }
+  },
+  UpdateDigitalAccountStatusReq: {
+    fields: {
+      Header: {
+        type: "MessageHead",
+        id: 1
+      },
+      DigitalAccountID: {
+        rule: "required",
+        type: "uint64",
+        id: 2
+      },
+      TradeStatus: {
+        rule: "required",
+        type: "uint32",
+        id: 3
+      }
+    }
+  },
+  UpdateDigitalAccountStatusRsp: {
+    fields: {
+      Header: {
+        type: "MessageHead",
+        id: 1
+      },
+      RetCode: {
+        type: "int32",
+        id: 2
+      },
+      RetDesc: {
+        type: "string",
+        id: 3
+      },
+      DigitalAccountID: {
+        type: "uint64",
+        id: 4
+      }
+    }
+  },
+  DigitalAccountRefundApplyReq: {
+    fields: {
+      Header: {
+        type: "MessageHead",
+        id: 1
+      },
+      DigitalAccountID: {
+        rule: "required",
+        type: "uint64",
+        id: 2
+      },
+      Amount: {
+        rule: "required",
+        type: "double",
+        id: 3
+      },
+      RefundType: {
+        rule: "required",
+        type: "uint32",
+        id: 4
+      },
+      Remark: {
+        type: "string",
+        id: 5
+      }
+    }
+  },
+  DigitalAccountRefundApplyRsp: {
+    fields: {
+      Header: {
+        type: "MessageHead",
+        id: 1
+      },
+      RetCode: {
+        type: "int32",
+        id: 2
+      },
+      RetDesc: {
+        type: "string",
+        id: 3
+      },
+      ApplyID: {
+        type: "uint64",
+        id: 4
+      }
+    }
+  },
+  DigitalAccountRefundApplyAuditReq: {
+    fields: {
+      Header: {
+        type: "MessageHead",
+        id: 1
+      },
+      ApplyID: {
+        rule: "required",
+        type: "uint64",
+        id: 2
+      },
+      Status: {
+        rule: "required",
+        type: "uint32",
+        id: 3
+      },
+      AuditorID: {
+        rule: "required",
+        type: "uint64",
+        id: 4
+      },
+      Remark: {
+        type: "string",
+        id: 5
+      }
+    }
+  },
+  DigitalAccountRefundApplyAuditRsp: {
+    fields: {
+      Header: {
+        type: "MessageHead",
+        id: 1
+      },
+      RetCode: {
+        type: "int32",
+        id: 2
+      },
+      RetDesc: {
+        type: "string",
+        id: 3
+      },
+      ApplyID: {
+        rule: "required",
+        type: "uint64",
+        id: 4
+      }
+    }
   }
 });
 

+ 53 - 0
public/proto/mtp.proto

@@ -1250,6 +1250,16 @@ message TaAccountDigitalChange {
 		optional uint32 TradeStatus = 22; // 交易状态-1:正常
 		optional uint64 Timestamp = 23; // 时间戳
 }
+// 枚举项变更通知
+message EnumDicItemChangeNtf {
+	optional MessageHead Header = 1; // 消息头
+		optional NotifyHead NtfHeader = 2; // NotifyHead 公共消息头
+		optional string EnumDicCode = 3; // string 枚举代码
+		optional uint32 EnumItemName = 4; // uint32 枚举项值
+		optional uint32 EnumItemStatus = 5; // uint32 枚举项状态
+		optional string NotifyTime = 6; // string 通知发送时间
+		optional uint64 Timestamp = 7; // uint64 时间戳
+}
 // 交易委托请求
 message OrderReq {
 	optional MessageHead Header = 1;
@@ -15917,3 +15927,46 @@ message CreateDigitalWalletAddressRsp {
 			optional string Memo = 10; // 地址备注(某些链需要)
 		required uint64 SerialNumber = 11; // 流水号
 }
+// 修改数字账户状态请求
+message UpdateDigitalAccountStatusReq {
+	optional MessageHead Header = 1;
+		required uint64 DigitalAccountID = 2; // 数字账户ID
+		required uint32 TradeStatus = 3; // 交易状态-1:正常
+}
+// 修改数字账户状态应答
+message UpdateDigitalAccountStatusRsp {
+	optional MessageHead Header = 1; // 消息头
+	optional int32 RetCode = 2; // 返回码
+	optional string RetDesc = 3; // 描述信息
+		optional uint64 DigitalAccountID = 4; // 数字账户ID
+}
+// 数字账户退票申请请求
+message DigitalAccountRefundApplyReq {
+	optional MessageHead Header = 1;
+		required uint64 DigitalAccountID = 2; // 数字账户ID
+		required double Amount = 3; // 金额(正值)
+		required uint32 RefundType = 4; // 类型:5-充值退票;6-提现退票
+		optional string Remark = 5; // 备注
+}
+// 数字账户退票申请应答
+message DigitalAccountRefundApplyRsp {
+	optional MessageHead Header = 1; // 消息头
+	optional int32 RetCode = 2; // 返回码
+	optional string RetDesc = 3; // 描述信息
+		optional uint64 ApplyID = 4; // 申请ID
+}
+// 数字账户退票申请审核请求
+message DigitalAccountRefundApplyAuditReq {
+	optional MessageHead Header = 1;
+		required uint64 ApplyID = 2; // 申请ID
+		required uint32 Status = 3; // 申请状态(3:审核通过,4:审核拒绝)
+		required uint64 AuditorID = 4; // 审核人
+		optional string Remark = 5; // 备注
+}
+// 数字账户退票申请审核应答
+message DigitalAccountRefundApplyAuditRsp {
+	optional MessageHead Header = 1; // 消息头
+	optional int32 RetCode = 2; // 返回码
+	optional string RetDesc = 3; // 描述信息
+		required uint64 ApplyID = 4; // 申请ID
+}

+ 71 - 0
src/business/account/index.ts

@@ -0,0 +1,71 @@
+import { shallowRef } from 'vue'
+import { getTodayAccountConfigInfo } from '@/services/api/account'
+import { wordArrayToUint8Array } from '@/services/websocket/package/crypto'
+import { decodeProto } from '@/services/websocket/package/package50/proto'
+
+export function useAccountConfig() {
+    const riskRatioConfig = shallowRef<Model.RiskRatioType>()
+    const margin = shallowRef<Proto.MarginInfoStruct>()
+    const tradeRule = shallowRef<Proto.TradeRuleInfoStruct>()
+    const tradeFee = shallowRef<Proto.TradeFeeInfoStruct>()
+
+    // 获取账户配置
+    const fetchAccountConfig = async (accountId: number, goodsId: number) => {
+        const res = await getTodayAccountConfigInfo({
+            data: {
+                accountid: accountId
+            }
+        })
+
+        riskRatioConfig.value = res.data.riskRatioType
+        const { todayAccountMargins = [], todayAccountTradeRules = [], todayAccountTradefees = [] } = res.data
+
+        // 保证金
+        const marginConfig = todayAccountMargins.find((e) => e.accountid === accountId && e.goodsid === goodsId)
+        const foundMargin = marginConfig ?? todayAccountMargins.find((e) => e.accountid === 0 && e.goodsid === goodsId)
+        if (foundMargin) {
+            const wordArray = CryptoJS.enc.Base64.parse(foundMargin.infocontent)
+            const uint8Array = wordArrayToUint8Array(wordArray)
+
+            await decodeProto<Proto.MarginInfoStruct>('MarginInfoStruct', uint8Array).then((data) => margin.value = data)
+        }
+
+        // 交易规则
+        const ruleConfig = todayAccountTradeRules.find((e) => e.accountid === accountId && e.goodsid === goodsId)
+        const foundRule = ruleConfig ?? todayAccountTradeRules.find((e) => e.accountid === 0 && e.goodsid === goodsId)
+        if (foundRule) {
+            const wordArray = CryptoJS.enc.Base64.parse(foundRule.infocontent)
+            const uint8Array = wordArrayToUint8Array(wordArray)
+
+            await decodeProto<Proto.TradeRuleInfoStruct>('TradeRuleInfoStruct', uint8Array).then((data) => tradeRule.value = data)
+        }
+
+        // 交易费用
+        const freeConfig = todayAccountTradefees.find((e) => e.accountid === accountId && e.goodsid === goodsId)
+        const foundFree = freeConfig ?? todayAccountTradefees.find((e) => e.accountid === 0 && e.goodsid === goodsId)
+        if (foundFree) {
+            const wordArray = CryptoJS.enc.Base64.parse(foundFree.infocontent)
+            const uint8Array = wordArrayToUint8Array(wordArray)
+
+            await decodeProto<Proto.TradeFeeInfoStruct>('TradeFeeInfoStruct', uint8Array).then((data) => tradeFee.value = data)
+        }
+    }
+
+    const getTradeRuleById = (ruleId: number) => {
+        return tradeRule.value?.TradeRules.find((e) => e.RuleID === ruleId)
+    }
+
+    const getTradeFeeById = (freeId: number) => {
+        return tradeFee.value?.TradeFees.find((e) => e.FeeID === freeId)
+    }
+
+    return {
+        riskRatioConfig,
+        margin,
+        tradeRule,
+        tradeFee,
+        fetchAccountConfig,
+        getTradeRuleById,
+        getTradeFeeById
+    }
+}

+ 1 - 0
src/constants/funcode.ts

@@ -60,6 +60,7 @@ export enum FunCode {
     OrderCanceledNtf = 131084, /// 委托单撤单通知(0, 2, 12)
     DigitalAccountChangedNtf = 131180, // 数字账户变化通知
     DigitalAccountFundsChangedNtf = 131181, // 数字账户资金变化通知
+    EnumDicItemChangeNtf = 131183, // 枚举项变更通知
     
     // 行情内容
     QuoteBeat = 0x12, // 心跳

+ 1 - 0
src/services/bus/types.ts

@@ -21,6 +21,7 @@ export enum EventName {
     RiskToWebNtf, // 风控消息管理端通知客户端
     DigitalAccountChangedNtf, // 数字账户变化通知
     DigitalAccountFundsChangedNtf, // 数字账户资金变化通知
+    EnumDicItemChangeNtf, // 枚举项变更通知
 }
 
 /**

+ 9 - 0
src/services/websocket/message.ts

@@ -132,6 +132,15 @@ export async function pushMessage50(pkg: Package50, contentType: 'encrypted' | '
             }, delay, funCode.toString())
             break
         }
+        case FunCode.EnumDicItemChangeNtf: {
+            timerInterceptor.debounce(() => {
+                decodeProto<Proto.EnumDicItemChangeNtf>('EnumDicItemChangeNtf', content).then((data) => {
+                    // 枚举项变更通知
+                    eventBus.$emit('OrderDealedNtf', data)
+                })
+            }, delay, funCode.toString())
+            break
+        }
         default: {
             if (funCode) {
                 console.warn('接收到未定义的通知', funCode)

+ 17 - 17
src/services/worker/quote/cache.ts → src/services/worker/market/cache.ts

@@ -1,32 +1,32 @@
 import moment from 'moment'
 
 export default class {
-    loading = false
     goodsMap = new Map<string, Model.GoodsRsp>()
     quoteMap = new Map<string, Model.QuoteDayRsp>()
 
-    updateGoodsList(data: Map<string, Model.GoodsRsp>) {
-        this.goodsMap = data
+    updateGoods(data: Model.GoodsRsp[]) {
+        this.goodsMap = new Map(data.map((item) => [item.goodscode, item]))
+    }
+
+    updateQuotes(data: Model.QuoteDayRsp[]) {
+        this.quoteMap = new Map(data.map((item) => [item.goodscode, item]))
     }
 
     // 处理订阅行情
-    subscribeToQuotes(quotes: Proto.Quote[]) {
+    processSubscribedQuotes(quotes: Proto.Quote[]) {
         const result: Model.QuoteDayRsp[] = []
 
         quotes.forEach((item) => {
-            const processedQuote = this.processQuote(item)
-
-            if (!this.loading) {
-                const updatedQuote = this.updateGoodsQuote(processedQuote)
-                result.push(updatedQuote)
-            }
+            const normalizedQuote = this.normalizeQuote(item)
+            const mergedQuote = this.mergeQuote(normalizedQuote)
+            result.push(mergedQuote)
         })
 
         return result
     }
 
-    // 处理行情报价
-    private processQuote(quote: Proto.Quote): Partial<Model.QuoteDayRsp> {
+    // 标准化行情数据
+    private normalizeQuote(quote: Proto.Quote): Partial<Model.QuoteDayRsp> {
         const goodsCode = quote.goodscode ?? ''
         const goodsItem = this.goodsMap.get(goodsCode)
 
@@ -93,13 +93,13 @@ export default class {
         }
     }
 
-    // 处理商品报价
-    private updateGoodsQuote(quote: Partial<Model.QuoteDayRsp>) {
+    // 合并处理行情数据
+    private mergeQuote(quote: Partial<Model.QuoteDayRsp>) {
         const goodsCode = quote.goodscode ?? ''
         const goodsItem = this.goodsMap.get(goodsCode)
-        const goodsQuoteItem = this.quoteMap.get(goodsCode)
+        const quoteItem = this.quoteMap.get(goodsCode)
 
-        const updatedQuote: Model.QuoteDayRsp = goodsQuoteItem ?? {
+        const updatedQuote: Model.QuoteDayRsp = quoteItem ?? {
             ask: quote.ask ?? 0,
             ask10: quote.ask10 ?? 0,
             ask2: quote.ask2 ?? 0,
@@ -225,7 +225,7 @@ export default class {
             utclasttime: quote.utclasttime ?? ''
         }
 
-        if (goodsQuoteItem) {
+        if (quoteItem) {
             // 更新对象属性
             for (const [key, value] of Object.entries(quote)) {
                 if (value !== undefined) {

+ 252 - 0
src/services/worker/market/index.ts

@@ -0,0 +1,252 @@
+import { reactive, computed, onUnmounted } from 'vue'
+import { defineStore } from 'pinia'
+import { isMatch } from 'lodash'
+import { TaskQueue } from '@/utils/queue'
+import { queryMemberGoodsLimitConfig } from '@/services/api/common'
+import { queryErmcpGoods, queryQuoteDay } from '@/services/api/goods'
+import Worker from 'worker-loader!./thread'
+import Service from '../service'
+
+const worker = new Worker()
+
+// 初始化 Worker
+Service.onReady(() => {
+  worker.postMessage({
+    type: 'init',
+    data: {
+      url: Service.getServiceConfig('quoteUrl'),
+      protocols: []
+    }
+  })
+})
+
+export const useGoodsStore = defineStore('goods', () => {
+  const state = reactive({
+    loading: false,
+    goodsMap: new Map<number, Model.GoodsRsp>(), // 商品合集
+  })
+
+  const goodsList = computed(() => Array.from(state.goodsMap.values()))
+
+  const getGoodsById = (id: number) => state.goodsMap.get(id)
+
+  // 获取商品对象
+  const getGoodsItem = (query: Partial<Model.GoodsQuote>) => {
+    return goodsList.value.find((item) => isMatch(item, query))
+  }
+
+  // 获取会员商品列表
+  const fetchGoods = async () => {
+    if (state.loading) return
+    state.loading = true
+
+    try {
+      // 待优化,缓存到 userStore
+      const { data: configs } = await queryMemberGoodsLimitConfig({
+        data: {
+          roletype: 7
+        }
+      })
+
+      const { data: goodsItems } = await queryErmcpGoods()
+
+      const configMap = new Map(configs.map(config => [config.goodsid, config]))
+
+      const filteredItems = goodsItems.filter((item) => configMap.get(item.goodsid)?.isnodisplay !== 1)
+
+      // 根据 goodscode 字母升序排序
+      const sortedItems = filteredItems.sort((a, b) => a.goodscode.localeCompare(b.goodscode))
+
+      state.goodsMap = new Map(sortedItems.map((item) => [item.goodsid, item]))
+
+      worker.postMessage({
+        type: 'updateGoods',
+        data: sortedItems,
+      })
+    } finally {
+      state.loading = false
+    }
+  }
+
+  return {
+    goodsList,
+    fetchGoods,
+    getGoodsById,
+    getGoodsItem
+  }
+})
+
+export const useQuoteStore = defineStore('quote', () => {
+  const goodsStore = useGoodsStore()
+  const taskQueue = new TaskQueue()
+  const subscribeMap = new Map<symbol, string[]>()
+
+  const state = reactive({
+    loading: false,
+    quoteServerConnected: false,
+    quoteMap: new Map<string, Model.QuoteDayRsp>(), // 盘面合集
+  })
+
+  // 行情服务连接状态
+  const quoteServerConnected = computed(() => state.quoteServerConnected)
+
+  // 商品行情
+  const quoteList = computed(() => {
+    const result = []
+    for (const goods of goodsStore.goodsList) {
+      const quote = state.quoteMap.get(goods.goodscode)
+      if (quote) {
+        result.push({ goods, quote })
+      }
+    }
+    return result
+  })
+
+  const getQuoteByCode = (code: string) => state.quoteMap.get(code)
+
+  // 获取行情对象
+  const getQuoteItem = (query: Partial<Model.GoodsQuote>) => {
+    return quoteList.value.find(({ goods }) => isMatch(goods, query))
+  }
+
+  // 获取盘面列表
+  const fetchQuotes = async () => {
+    if (state.loading || !goodsStore.goodsList.length) return
+    state.loading = true
+
+    try {
+      const res = await queryQuoteDay({
+        data: {
+          goodsCodes: goodsStore.goodsList.map((item) => item.goodscode).join(',')
+        }
+      })
+
+      state.quoteMap = new Map(res.data.map((item) => [item.goodscode, item]))
+
+      worker.postMessage({
+        type: 'updateQuotes',
+        data: res.data
+      })
+
+      taskQueue.run()
+    } finally {
+      state.loading = false
+    }
+  }
+
+  // 行情订阅
+  const subscribe = (goodsCodes: string | string[], key = Symbol()) => {
+    const values = Array.isArray(goodsCodes) ? goodsCodes : [goodsCodes]
+    subscribeMap.set(key, values)
+
+    const codes = Array.from(subscribeMap.values()).flat() // 扁平化
+    const uniqueCodes = Array.from(new Set(codes)) // 去重处理
+
+    worker.postMessage({
+      type: 'subscribe',
+      data: uniqueCodes.map(code => ({
+        exchangeCode: 250,
+        goodsCode: code,
+        subState: 0
+      }))
+    })
+
+    const cancel = () => {
+      if (subscribeMap.has(key)) {
+        subscribeMap.delete(key)
+      }
+    }
+
+    onUnmounted(() => cancel())
+
+    return cancel
+  }
+
+  const onReady = (callback: () => void) => {
+    if (state.quoteMap.size) {
+      setTimeout(() => callback(), 0)
+    } else {
+      const cancel = taskQueue.add(callback)
+      onUnmounted(() => cancel())
+    }
+  }
+
+  // 监听来自 Worker 的消息
+  worker.onmessage = (event) => {
+    const message = event.data
+
+    if (typeof message === 'object') {
+      switch (message.type) {
+        case 'ready': {
+          console.log('QuoteSocket is ready')
+          state.quoteServerConnected = true
+          break
+        }
+        case 'closed': {
+          console.log('QuoteSocket is closed')
+          state.quoteServerConnected = false
+          break
+        }
+        case 'publish': {
+          if (!state.loading) {
+            const item = message.data
+            state.quoteMap.set(item.goodscode, item)
+          }
+          break
+        }
+      }
+    } else {
+      console.warn('Error from worker:', event.data)
+    }
+  }
+
+  return {
+    quoteServerConnected,
+    quoteList,
+    onReady,
+    fetchQuotes,
+    getQuoteByCode,
+    getQuoteItem,
+    subscribe
+  }
+})
+
+export const useMarketStore = defineStore('market', () => {
+  const quoteStore = useQuoteStore()
+
+  const state = reactive({
+    selectedMarketIdsSet: new Set<number>(),
+    selectedGoodsId: 0, // 当前选中的商品ID
+  })
+
+  const marketQuoteList = computed(() => quoteStore.quoteList.filter((e) => state.selectedMarketIdsSet.has(e.goods.marketid)))
+
+  // const getMarketIdByTradeMode = (values: number | number[]) => {
+  //   const tradeMode = Array.isArray(values) ? values : [values]
+  // }
+
+  const selectMarkets = (values: number | number[]) => {
+    const ids = new Set(Array.isArray(values) ? values : [values])
+    state.selectedMarketIdsSet = ids
+
+    const list = marketQuoteList.value
+    if (list.length) {
+      // 查找当前选中的商品ID
+      if (!list.every((e) => e.goods.goodsid === state.selectedGoodsId)) {
+        state.selectedGoodsId = list[0].goods.goodsid
+      }
+    } else {
+      state.selectedGoodsId = 0
+    }
+  }
+
+  const selectGoods = (id: number) => {
+    state.selectedGoodsId = id
+  }
+
+  return {
+    marketQuoteList,
+    selectMarkets,
+    selectGoods,
+  }
+})

+ 197 - 0
src/services/worker/market/thread.ts

@@ -0,0 +1,197 @@
+import { debounce, throttle } from 'lodash'
+import { Package40 } from '@/services/websocket/package'
+import { subscribeListToByteArrary } from '@/services/websocket/package/package40/encode'
+import { parseReceivePush } from '@/services/websocket/package/package40/decode'
+import WebSocketManager from '@/utils/websocket'
+import Long from 'long'
+import CacheData from './cache'
+
+// 错误处理
+const handleError = (error: unknown, context: string) => {
+    console.error(`[${context}] 错误:`, error)
+    self.postMessage({
+        type: 'error',
+        data: {
+            context,
+            message: error instanceof Error ? error.message : String(error)
+        }
+    })
+}
+
+const socketWorker = new (class {
+    private ws?: WebSocketManager
+    private cacheData = new CacheData()
+    private subscribers: Proto.QuoteReq[] = []
+    private throttledUpdates = new Map<string, (item: Model.QuoteDayRsp) => void>()
+
+    // 初始化
+    init(data: { url: string; protocols: string | string[]; }) {
+        this.ws?.disconnect()
+
+        this.ws = new WebSocketManager({
+            url: data.url,
+            protocols: data.protocols,
+            onOpen: () => {
+                self.postMessage({ type: 'ready' })
+            },
+            onClose: () => {
+                self.postMessage({ type: 'closed' })
+            },
+            onReconnect: () => {
+                this.runSubscriptions() // 重新订阅
+            },
+            onMessage: (data) => {
+                if (data instanceof Blob) {
+                    data.arrayBuffer().then((res) => {
+                        const arr = new Uint8Array(res)
+                        const raw = this.parseDataPackage(arr)
+
+                        if (raw?.content) {
+                            const quotes = parseReceivePush(raw.content)
+                            const items = this.cacheData.processSubscribedQuotes(quotes)
+                            this.publish(items)
+                        }
+                    })
+                }
+            }
+        })
+    }
+
+    // 解析数据包
+    private parseDataPackage(bytes: Uint8Array) {
+        if (bytes.length < 5) return // 最小长度检查
+
+        // 首字节检查
+        if (bytes[0] !== 0xff) {
+            handleError('接收到首字节不是0xFF', 'parseDataPackage')
+            return
+        }
+
+        // 提取包长度
+        const lengthView = new DataView(bytes.buffer, 1, 4)
+        const packageLength = lengthView.getUint32(0, false)
+
+        if (packageLength > 65535) {
+            handleError('接收到长度超过65535的行情包', 'parseDataPackage')
+            return
+        }
+
+        // 完整性检查
+        if (bytes.length < packageLength || bytes[packageLength - 1] !== 0x0) {
+            handleError('数据包不完整或尾字节错误', 'parseDataPackage')
+            return
+        }
+
+        try {
+            const content = bytes.slice(0, packageLength)
+            const result = new Package40(content)
+
+            if (result.packageLength === 0) {
+                handleError('报文装箱失败', 'parseDataPackage')
+                return
+            }
+
+            return result
+        } catch (error) {
+            handleError(error, 'parseDataPackage')
+        }
+    }
+
+    // 推送行情
+    private publish(items: Model.QuoteDayRsp[]) {
+        items.forEach((item) => {
+            const publisher = this.throttledUpdates.get(item.goodscode)
+
+            if (publisher) {
+                publisher(item)
+            } else {
+                // 节流处理,避免频繁推送同一商品的行情数据
+                const throttledPublisher = throttle((data) => {
+                    self.postMessage({ type: 'publish', data })
+                }, 200)
+
+                this.throttledUpdates.set(item.goodscode, throttledPublisher)
+                throttledPublisher(item)
+            }
+        })
+    }
+
+    // 防抖处理,避免短时间内同时订阅
+    private runSubscriptions = debounce(() => {
+        if (this.subscribers.length) {
+            this.ws?.connection({
+                onSuccess: () => {
+                    const content = subscribeListToByteArrary(this.subscribers, '2_TOKEN_NEKOT_', Long.fromNumber(2))
+                    const package40 = new Package40(32, content)
+                    this.ws?.send(package40.data())
+                }
+            })
+        } else {
+            handleError('订阅商品为空', 'runSubscriptions')
+            this.disconnect()
+        }
+    }, 300)
+
+    // 订阅行情
+    subscribe(data: Proto.QuoteReq[]) {
+        this.subscribers = data
+        this.runSubscriptions()
+    }
+
+    // 更新商品列表
+    updateGoods(data: Model.GoodsRsp[]) {
+        this.cacheData.updateGoods(data)
+    }
+
+    // 更新行情列表
+    updateQuotes(data: Model.QuoteDayRsp[]) {
+        this.cacheData.updateQuotes(data)
+    }
+
+    // 断开连接
+    disconnect() {
+        this.ws?.disconnect()
+        this.throttledUpdates.clear()
+        this.subscribers = []
+    }
+})
+
+// 主线程消息
+self.onmessage = (e) => {
+    const message = e.data
+
+    try {
+        switch (message.type) {
+            case 'init': {
+                socketWorker.init(message.data)
+                break
+            }
+            case 'updateGoods': {
+                socketWorker.updateGoods(message.data)
+                break
+            }
+            case 'updateQuotes': {
+                socketWorker.updateQuotes(message.data)
+                break
+            }
+            case 'subscribe': {
+                socketWorker.subscribe(message.data)
+                break
+            }
+            case 'close': {
+                socketWorker.disconnect()
+                break
+            }
+            default: {
+                handleError(`未知消息类型: ${message.type}`, 'onmessage')
+            }
+        }
+    } catch (error) {
+        handleError(error, 'onmessage')
+    }
+}
+
+// 错误处理
+self.onerror = (error) => {
+    handleError(error, 'onerror')
+}

+ 9 - 0
src/services/worker/market/types.ts

@@ -0,0 +1,9 @@
+export interface InitOptions {
+    url: string;
+    protocols: string | string[];
+}
+
+export interface UpdateData {
+    goodsList: Model.GoodsRsp[];
+    quoteList: Model.QuoteDayRsp[];
+}

+ 0 - 118
src/services/worker/quote/index.ts

@@ -1,118 +0,0 @@
-import { reactive, toRefs, toRaw } from 'vue'
-import { queryErmcpGoods, queryQuoteDay } from '@/services/api/goods'
-import Worker from 'worker-loader!./thread'
-import Service from '../service'
-
-const useService = (function () {
-  const state = reactive({
-    loading: false,
-    goodsMap: [] as Model.GoodsRsp[], // 商品列表
-    quoteMap: [] as Model.QuoteDayRsp[], // 盘面列表
-  })
-
-  const worker = new Worker()
-
-  const updateData = () => {
-    queryErmcpGoods().then((res) => {
-      state.goodsMap = res.data
-      queryQuoteDay().then((res) => {
-        state.quoteMap = res.data
-      })
-    })
-  }
-
-  Service.onReady(() => {
-    worker.postMessage({
-      type: 'init',
-      data: {
-        socketUrl: Service.getServiceConfig('quoteUrl'),
-        socketProtocols: []
-      }
-    })
-  })
-
-  return {
-    ...toRefs(state),
-    worker
-  }
-})()
-
-
-export default new (class {
-  private readonly worker = new Worker()
-
-  private state = reactive({
-    loading: false,
-    goodsMap: new Map<string, Model.GoodsRsp>(), // 商品列表
-    quoteMap: new Map<string, Model.QuoteDayRsp>(), // 盘面列表
-  })
-
-  constructor() {
-    // 监听来自Worker的消息
-    this.worker.onmessage = (event) => {
-      const message = event.data
-
-      if (typeof message === 'object') {
-        switch (message.type) {
-          case 'push': {
-            const quote = message.data
-            this.state.quoteMap.set(quote.goodscode, quote)
-            break
-          }
-        }
-      }
-
-      console.log('接收工作线程消息:', event.data)
-    }
-
-    // 监听来自Worker的错误
-    this.worker.onerror = (error) => {
-      console.error('Error from worker:', error.message)
-    }
-
-    // 发送WebSocket URL给Worker
-    this.worker.postMessage({
-      type: 'connect',
-      url: 'ws://192.168.31.204:18891'
-    })
-
-    setTimeout(() => {
-      this.worker.postMessage({
-        type: 'send',
-        data: [{
-          exchangeCode: 250,
-          goodsCode: 'XAUUSD',
-          subState: 0,
-        }]
-      })
-
-      setTimeout(() => {
-        this.worker.postMessage({
-          type: 'close',
-        })
-      }, 5000);
-    }, 1000)
-  }
-
-  goodsMapToList() {
-    return [...this.state.goodsMap.values()]
-  }
-
-  async updateData() {
-    try {
-      this.state.loading = true
-
-      const res = await queryErmcpGoods()
-      res.data.forEach((item) => {
-        this.state.goodsMap.set(item.goodscode, item)
-      })
-
-      this.worker.postMessage({
-        type: 'update',
-        data: toRaw(this.state)
-      })
-    } finally {
-      this.state.loading = false
-    }
-  }
-})

+ 0 - 125
src/services/worker/quote/thread.ts

@@ -1,125 +0,0 @@
-import Long from 'long'
-import CacheData from './cache'
-import WebSocketManager from '@/utils/websocket'
-import { Package40 } from '@/services/websocket/package'
-import { subscribeListToByteArrary } from '@/services/websocket/package/package40/encode'
-import { parseReceivePush } from '@/services/websocket/package/package40/decode'
-
-const cacheData = new CacheData()
-
-const webSocket = new WebSocketManager({
-    url: '',
-    //heartbeatMessage: () => JSON.stringify({ type: 'ping' }),
-    onMessage: (data) => {
-        new Response(data as Uint8Array).arrayBuffer().then((res) => {
-            const arr = new Uint8Array(res)
-            const raw = disposeReceiveDatas(arr)
-
-            if (raw?.content) {
-                const quotes = parseReceivePush(raw.content)
-                const result = cacheData.subscribeToQuotes(quotes)
-
-                result.forEach((item) => {
-                    self.postMessage({
-                        type: 'push',
-                        data: item
-                    })
-                })
-            }
-        })
-    }
-})
-
-const disposeReceiveDatas = (bytes: Uint8Array) => {
-    const cache: number[] = [];
-    let cachePackageLength = 0;
-
-    for (let i = 0; i < bytes.length; i++) {
-        const byte = bytes[i];
-        cache.push(byte);
-
-        if (i === 0 && byte !== 0xff) {
-            console.error('接收到首字节不是0xFF');
-            return;
-        } else {
-            // 取报文长度
-            if (cache.length === 5) {
-                const uint8View = new Uint8Array(cache.slice(1, 5));
-                cachePackageLength = new DataView(uint8View.buffer).getUint32(0, false);
-                if (cachePackageLength > 65535) {
-                    console.error('接收到长度超过65535的行情包');
-                    return;
-                }
-            }
-            // 判断是否已经到包尾
-            if (cache.length === cachePackageLength) {
-                if (byte !== 0x0) {
-                    console.error('接收到尾字节不是0x0的错误数据包');
-                    return;
-                }
-
-                const content = new Uint8Array(cache);
-                const result = new Package40(content);
-
-                if (result.packageLength === 0) {
-                    console.error('报文装箱失败');
-                    return;
-                }
-                return result
-            }
-        }
-    }
-}
-
-let ws: WebSocketManager
-
-// 监听来自主线程的消息
-self.onmessage = (e) => {
-    const message = e.data
-
-    if (typeof message === 'object') {
-        switch (message.type) {
-            case 'init': {
-                ws = new WebSocketManager({
-                    url: message.data,
-                    onMessage: (data) => {
-                        console.log(data)
-                    }
-                })
-
-                // 消息队列
-                const messageQueue = new Map()
-
-
-
-
-
-                break
-            }
-            case 'connect': {
-                const { url, protocols } = message
-                webSocket.connection()
-                break
-            }
-            case 'update': {
-                cacheData.updateGoodsList(message.data)
-                break
-            }
-            case 'send': {
-                const content = subscribeListToByteArrary(message.data, '2_TOKEN_NEKOT_', Long.fromNumber(2))
-                const package40 = new Package40(32, content)
-                webSocket.send(package40.data())
-                break
-            }
-            case 'close': {
-                webSocket.disconnect()
-                break
-            }
-        }
-    }
-}
-
-// 错误处理
-self.onerror = (error) => {
-    console.error('工作线程错误:', error)
-}

+ 23 - 22
src/services/worker/service.ts

@@ -1,4 +1,5 @@
 import plus from '@/utils/h5plus'
+import { TaskQueue } from '@/utils/queue'
 
 // 本地配置
 interface AppConfig {
@@ -11,11 +12,15 @@ interface AppConfig {
     modules: ('register' | 'delivery')[]; // 应用包含的模块
     quotationProperties: (keyof Model.QuoteDayRsp)[]; // 盘面可显示的属性
     forcedPasswordChange: boolean; // 首次登录是否强制修改密码
+    strongPassword: boolean; // 是否使用强密码规则
     registrationCodeRule: -1 | 0 | 1; // 注册编码规则,-1=隐藏,0=非必填,1=必填
     riskType: 0 | 1 | 2; // 风控类型,1=按单风控,2=按账户风控
     i18nEnabled: boolean; // 是否启用多语言设置
     allCloseEnabled: boolean; // 是否启用全部平仓
     allDeliveryEnabled: boolean; // 是否启用全部交收
+    metaPixelId: string; // 是否启用像素追踪
+    appsFlyerId: string; // 是否启用像素追踪
+    appsFlyerKey: string; // 是否启用像素追踪
 }
 
 // 服务配置
@@ -46,12 +51,12 @@ export default new (class {
 
     isReady = false
 
+    private taskQueue = new TaskQueue()
+
     /** 限制重试次数,0 = 无限制 */
     private retryLimit = 5
 
-    private readyPromise: Promise<void> | undefined = undefined
-
-    private readyCallbacks = new Map<symbol, () => void>()
+    private initPromise: Promise<void> | null = null
 
     private appConfigAsync
 
@@ -65,11 +70,15 @@ export default new (class {
         modules: [],
         quotationProperties: [],
         forcedPasswordChange: false,
-        registrationCodeRule: 1,
+        strongPassword: true,
+        registrationCodeRule: 0,
         riskType: 0,
         i18nEnabled: true,
         allCloseEnabled: false,
-        allDeliveryEnabled: false
+        allDeliveryEnabled: false,
+        metaPixelId: '',
+        appsFlyerId: '',
+        appsFlyerKey: ''
     }
 
     private serviceConfig: ServiceConfig = {
@@ -100,9 +109,7 @@ export default new (class {
                 plus.httpRequest({ url }).then((res) => {
                     this.serviceConfig = res.data.data
                     this.isReady = true
-
-                    this.readyCallbacks.forEach((callback) => callback())
-                    this.readyCallbacks.clear()
+                    this.taskQueue.run()
                     resolve()
                 }).catch(() => {
                     setTimeout(() => {
@@ -111,35 +118,31 @@ export default new (class {
                     }, 3000)
                 })
             } else {
-                this.readyPromise = undefined
+                this.initPromise = null
                 reject('服务加载失败,请稍后再试')
             }
         })
     }
 
     async init() {
-        if (!this.readyPromise) {
+        if (!this.initPromise) {
             try {
                 await this.appConfigAsync
             } catch {
                 this.appConfigAsync = this.loadAppConfig()
             }
-            this.readyPromise = new Promise<void>((resolve, reject) => {
+            this.initPromise = new Promise<void>((resolve, reject) => {
                 this.appConfigAsync.then((data) => {
                     Object.assign(this.appConfig, data)
                     this.loadServiceConfig(data.apiUrl).then(resolve).catch(reject)
                 }).catch(() => {
-                    this.readyPromise = undefined
+                    this.initPromise = null
                     reject('配置文件加载失败,请稍后再试')
                 })
             })
         }
         // 确保当前只有一个初始化实例
-        return this.readyPromise
-    }
-
-    removeReadyCallback(key: symbol) {
-        this.readyCallbacks.delete(key)
+        return this.initPromise
     }
 
     getAppConfig<K extends keyof AppConfig>(key: K) {
@@ -151,12 +154,10 @@ export default new (class {
     }
 
     onReady(callback: () => void) {
-        const key = Symbol()
+        const cancel = this.taskQueue.add(callback)
         if (this.isReady) {
-            callback()
-        } else {
-            this.readyCallbacks.set(key, callback)
+            this.taskQueue.run()
         }
-        return key
+        return cancel
     }
 })

+ 2 - 1
src/services/worker/trade/thread.ts

@@ -1,6 +1,6 @@
 import WebSocketManager from '@/utils/websocket'
 import { Package50 } from '@/services/websocket/package'
-import { encodeProto, decodeProto } from '@/services/websocket/package/package50/proto'
+import { encodeProto } from '@/services/websocket/package/package50/proto'
 import { FunCode } from '@/constants/funcode'
 
 interface SendMessageEvent {
@@ -63,6 +63,7 @@ const initWebSocket = (url: string, protocols?: string | string[]) => {
     return new WebSocketManager({
         url,
         protocols,
+        //heartbeatMessage: () => JSON.stringify({ type: 'ping' }),
         onMessage: (uint8Array) => {
             // 接收数据
             new Response(uint8Array).arrayBuffer().then((res) => {

+ 3 - 9
src/stores/modules/account.ts

@@ -22,22 +22,16 @@ export const useAccountStore = defineStore(() => {
         loading: boolean;
         accountList: Model.TaAccountsRsp[];
         currentAccountId: number;
-        currentAccountConfig: Model.TodayAccountConfigInfoRsp;
+        currentAccountConfig?: Model.TodayAccountConfigInfoRsp;
     }>({
         loading: false,
         accountList: [],
-        currentAccountId: 0,
-        currentAccountConfig: {
-            todayAccountMargins: [],
-            todayAccountTradeRules: [],
-            todayAccountTradefees: []
-        }
+        currentAccountId: 0
     })
 
     // 资金账户计算列表
     const accountComputedList = computed(() => {
-        const { riskRatioType } = state.currentAccountConfig
-        const { addmarginriskratio = 0, notemarginriskratio = 0, cutriskratio = 0 } = riskRatioType ?? {}
+        const { addmarginriskratio = 0, notemarginriskratio = 0, cutriskratio = 0 } = state.currentAccountConfig?.riskRatioType ?? {}
 
         const result: (Model.TaAccountsRsp & {
             freezeMargin: number; // 冻结资金

+ 19 - 0
src/stores/modules/enum.ts

@@ -3,6 +3,7 @@ import { queryAllEnums } from '@/services/api/common'
 import { defineStore } from '../store'
 import { sessionData } from '../storage'
 import { i18n } from '@/stores'
+import eventBus from '@/services/bus'
 
 /**
  * 枚举类型
@@ -136,6 +137,24 @@ export const useEnumStore = defineStore(() => {
         return item?.bankmappedvalue ?? ''
     }
 
+    // 枚举项变更通知
+    eventBus.$on('EnumDicItemChangeNtf', (res) => {
+        const data = res as Proto.EnumDicItemChangeNtf
+
+        queryAllEnums({
+            data: {
+                enumdiccode: data.EnumDicCode
+            }
+        }).then((res) => {
+            const enums = enumMap.get(data.EnumDicCode)
+            if (enums) {
+                enums.value = res.data
+            } else {
+                enumMap.set(data.EnumDicCode, shallowRef(res.data))
+            }
+        })
+    })
+
     return {
         loading,
         allEnums,

+ 0 - 1
src/stores/modules/global.ts

@@ -52,7 +52,6 @@ export const useGlobalStore = defineStore(() => {
             i18nEnabled: true,
             allCloseEnabled: false,
             allDeliveryEnabled: false,
-            metaPixelEnabled: false,
             metaPixelId: '',
             appsFlyerId: '',
             appsFlyerKey: ''

+ 1 - 0
src/types/model/enum.d.ts

@@ -2,6 +2,7 @@ declare namespace Model {
     /** 枚举信息 请求 */
     interface EnumReq {
         autoid?: number; // 起始自增ID
+        enumdiccode?: string; // 枚举代码
     }
 
     /** 枚举信息 响应 */

+ 1 - 1
src/types/model/user.d.ts

@@ -167,7 +167,7 @@ declare namespace Model {
 
     /** 获取资金账户今日配置信息 响应 */
     interface TodayAccountConfigInfoRsp {
-        riskRatioType?: RiskRatioType;
+        riskRatioType: RiskRatioType;
         todayAccountMargins: TodayAccountMargin[]; // 今日账户保证金表
         todayAccountTradeRules: TodayAccountTradeRule[]; // 今日账户交易规则信表
         todayAccountTradefees: TodayAccountTradefee[]; // 今日账户交易费用表

+ 11 - 0
src/types/proto/notify.d.ts

@@ -91,5 +91,16 @@ declare global {
             MatchAccountID: number; // 对手方资金账号代码
             RealClosePL: number; // 实际盈亏
         }
+
+        /** 枚举项变更通知 */
+        interface EnumDicItemChangeNtf {
+            Header: IMessageHead;
+            NtfHeader: NotifyHead; // NotifyHead 公共消息头
+            EnumDicCode: string; // string 枚举代码
+            EnumItemName: number; // uint32 枚举项值
+            EnumItemStatus: number; // uint32 枚举项状态
+            NotifyTime: string; // string 通知发送时间
+            Timestamp: number; // uint64 时间戳
+        }
     }
 }

+ 48 - 0
src/utils/queue/index.ts

@@ -0,0 +1,48 @@
+export class TaskQueue<T = void> {
+    private currentQueue = new Map<symbol, () => T | Promise<T>>() // 当前队列
+    private pendingQueue = new Map<symbol, () => T | Promise<T>>() // 等待队列
+    private isRunning = false
+
+    add(fn: () => T | Promise<T>) {
+        const key = Symbol()
+
+        const targetQueue = this.isRunning ? this.pendingQueue : this.currentQueue
+        targetQueue.set(key, fn)
+
+        return () => {
+            this.currentQueue.delete(key)
+            this.pendingQueue.delete(key)
+        }
+    }
+
+    async run() {
+        if (this.isRunning) return []
+        if (!this.size) return []
+
+        this.isRunning = true
+
+        try {
+            // 合并队列
+            this.currentQueue = new Map([...this.currentQueue, ...this.pendingQueue])
+            this.pendingQueue.clear()
+
+            const tasks = Array.from(this.currentQueue.values())
+            this.currentQueue.clear()
+
+            const results = await Promise.all(
+                tasks.map((task) => Promise.resolve(task()).catch((e) => Promise.reject(e)))
+            )
+            return results
+        } finally {
+            this.isRunning = false
+            // 检查是否有新队列
+            if (this.pendingQueue.size > 0) {
+                await this.run()
+            }
+        }
+    }
+
+    get size() {
+        return this.currentQueue.size + this.pendingQueue.size
+    }
+}

+ 8 - 23
src/utils/websocket/index.ts

@@ -1,19 +1,4 @@
-interface WebSocketOptions {
-    url: string,
-    protocols?: string | string[]
-    heartbeatMessage?: () => MessageEvent['data'];
-    onOpen?: () => void;
-    onMessage: (data: MessageEvent['data']) => void;
-    onClose?: () => void;
-    onError?: (err: Event) => void;
-    onBeforeReconnect?: (count: number) => void;
-    onReconnect?: () => void;
-}
-
-interface ConnectionOptions {
-    onSuccess?: () => void;
-    onFail?: () => void;
-}
+import { WebSocketOptions, ConnectionOptions } from './types'
 
 export default class {
     private socket: WebSocket | null = null; // WebSocket 对象
@@ -37,6 +22,9 @@ export default class {
     private onBeforeReconnect?: (count: number) => void; // 重连之前的事件
     private onReconnect?: () => void; // 重连成功之后的事件
 
+    private isConnecting = false
+    private connectionCallbacks = new Map<symbol, ConnectionOptions>()
+
     constructor(options: WebSocketOptions) {
         this.url = options.url
         this.protocols = options.protocols
@@ -49,11 +37,8 @@ export default class {
         this.onReconnect = options.onReconnect
     }
 
-    private isConnecting = false
-    private connectionCallbacks = new Map<symbol, ConnectionOptions>()
-
     // 查询连接状态
-    isConnected() {
+    get isConnected() {
         return !this.isReconnecting && this.socket?.readyState === WebSocket.OPEN
     }
 
@@ -61,7 +46,7 @@ export default class {
     connection(options?: ConnectionOptions) {
         const key = Symbol()
 
-        if (this.isConnected()) {
+        if (this.isConnected) {
             options?.onSuccess?.()
         } else {
             if (options) {
@@ -179,8 +164,8 @@ export default class {
 
     // 发送消息
     send(data: string | ArrayBufferLike | Blob | ArrayBufferView) {
-        if (this.socket) {
-            this.socket.send(data)
+        if (this.isConnected) {
+            this.socket?.send(data)
             return true
         }
         return false

+ 16 - 0
src/utils/websocket/types.ts

@@ -0,0 +1,16 @@
+export interface WebSocketOptions {
+    url: string,
+    protocols?: string | string[]
+    heartbeatMessage?: () => MessageEvent['data'];
+    onOpen?: () => void;
+    onMessage: (data: MessageEvent['data']) => void;
+    onClose?: () => void;
+    onError?: (err: Event) => void;
+    onBeforeReconnect?: (count: number) => void;
+    onReconnect?: () => void;
+}
+
+export interface ConnectionOptions {
+    onSuccess?: () => void;
+    onFail?: () => void;
+}