@pisell/pisellos 0.0.568 → 0.0.569

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (413) hide show
  1. package/dist/model/strategy/adapter/promotion/evaluator.js +3 -2
  2. package/dist/model/strategy/adapter/walletPass/utils.js +7 -4
  3. package/dist/modules/BaseModule.d.ts +2 -2
  4. package/dist/modules/BaseModule.js +4 -4
  5. package/dist/modules/BookingContext/index.d.ts +48 -0
  6. package/dist/modules/BookingContext/index.js +566 -0
  7. package/dist/modules/BookingContext/types.d.ts +102 -0
  8. package/dist/modules/BookingContext/types.js +51 -0
  9. package/dist/modules/BookingContext/utils/buildCacheItemFromOrderLine.d.ts +16 -0
  10. package/dist/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +302 -0
  11. package/dist/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.d.ts +8 -0
  12. package/dist/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.js +149 -0
  13. package/dist/modules/BookingContext/utils/cacheItemToBookingInput.d.ts +76 -0
  14. package/dist/modules/BookingContext/utils/cacheItemToBookingInput.js +249 -0
  15. package/dist/modules/BookingContext/utils/flexible.d.ts +28 -0
  16. package/dist/modules/BookingContext/utils/flexible.js +56 -0
  17. package/dist/modules/BookingContext/utils/formatResourceList.d.ts +8 -0
  18. package/dist/modules/BookingContext/utils/formatResourceList.js +38 -0
  19. package/dist/modules/BookingContext/utils/index.d.ts +11 -0
  20. package/dist/modules/BookingContext/utils/index.js +11 -0
  21. package/dist/modules/BookingContext/utils/noResource.d.ts +13 -0
  22. package/dist/modules/BookingContext/utils/noResource.js +77 -0
  23. package/dist/modules/BookingContext/utils/orderLineDisplay.d.ts +1 -0
  24. package/dist/modules/BookingContext/utils/orderLineDisplay.js +36 -0
  25. package/dist/modules/BookingContext/utils/productExtend.d.ts +72 -0
  26. package/dist/modules/BookingContext/utils/productExtend.js +339 -0
  27. package/dist/modules/BookingContext/utils/resourceErrors.d.ts +41 -0
  28. package/dist/modules/BookingContext/utils/resourceErrors.js +74 -0
  29. package/dist/modules/BookingContext/utils/resourceSelection.d.ts +7 -0
  30. package/dist/modules/BookingContext/utils/resourceSelection.js +24 -0
  31. package/dist/modules/BookingContext/utils/resources.d.ts +80 -0
  32. package/dist/modules/BookingContext/utils/resources.js +486 -0
  33. package/dist/modules/BookingContext/utils/serviceTimes.d.ts +55 -0
  34. package/dist/modules/BookingContext/utils/serviceTimes.js +151 -0
  35. package/dist/modules/BookingContext/utils/timeSlices.d.ts +42 -0
  36. package/dist/modules/BookingContext/utils/timeSlices.js +100 -0
  37. package/dist/modules/Cart/utils/cartProduct.js +26 -5
  38. package/dist/modules/Cart/utils/changePrice.js +27 -20
  39. package/dist/modules/Customer/index.d.ts +20 -0
  40. package/dist/modules/Customer/index.js +172 -83
  41. package/dist/modules/Customer/types.d.ts +2 -0
  42. package/dist/modules/Discount/index.d.ts +2 -1
  43. package/dist/modules/Discount/index.js +37 -9
  44. package/dist/modules/Discount/types.d.ts +1 -0
  45. package/dist/modules/OpenData/index.d.ts +8 -0
  46. package/dist/modules/OpenData/index.js +36 -16
  47. package/dist/modules/Order/index.d.ts +448 -23
  48. package/dist/modules/Order/index.js +5417 -561
  49. package/dist/modules/Order/types.d.ts +693 -88
  50. package/dist/modules/Order/types.js +68 -1
  51. package/dist/modules/Order/utils/bundleDiscountHydration.d.ts +5 -0
  52. package/dist/modules/Order/utils/bundleDiscountHydration.js +144 -0
  53. package/dist/modules/Order/utils/manualProductDiscount.d.ts +106 -0
  54. package/dist/modules/Order/utils/manualProductDiscount.js +210 -0
  55. package/dist/modules/Order/utils/orderCollectionIdentity.d.ts +53 -0
  56. package/dist/modules/Order/utils/orderCollectionIdentity.js +405 -0
  57. package/dist/modules/Order/utils.d.ts +167 -18
  58. package/dist/modules/Order/utils.js +960 -165
  59. package/dist/modules/Payment/index.d.ts +10 -9
  60. package/dist/modules/Payment/index.js +465 -703
  61. package/dist/modules/Payment/types.d.ts +40 -16
  62. package/dist/modules/Payment/utils.d.ts +1 -1
  63. package/dist/modules/Payment/utils.js +4 -3
  64. package/dist/modules/Payment/walletpass.js +44 -29
  65. package/dist/modules/Product/types.d.ts +29 -7
  66. package/dist/modules/ProductList/index.d.ts +17 -13
  67. package/dist/modules/ProductList/index.js +184 -74
  68. package/dist/modules/ProductList/types.d.ts +17 -0
  69. package/dist/modules/Quotation/index.d.ts +22 -2
  70. package/dist/modules/Quotation/index.js +108 -42
  71. package/dist/modules/Quotation/types.d.ts +19 -0
  72. package/dist/modules/ResourcePlanner/index.d.ts +24 -0
  73. package/dist/modules/ResourcePlanner/index.js +181 -0
  74. package/dist/modules/ResourcePlanner/planner.d.ts +14 -0
  75. package/dist/modules/ResourcePlanner/planner.js +1069 -0
  76. package/dist/modules/ResourcePlanner/types.d.ts +227 -0
  77. package/dist/modules/ResourcePlanner/types.js +1 -0
  78. package/dist/modules/Rules/index.d.ts +8 -10
  79. package/dist/modules/Rules/index.js +221 -109
  80. package/dist/modules/Rules/types.d.ts +14 -3
  81. package/dist/modules/SalesSummary/index.d.ts +23 -26
  82. package/dist/modules/SalesSummary/index.js +152 -46
  83. package/dist/modules/SalesSummary/types.d.ts +9 -1
  84. package/dist/modules/SalesSummary/utils.d.ts +3 -22
  85. package/dist/modules/SalesSummary/utils.js +803 -163
  86. package/dist/modules/Schedule/getDateIsInSchedule.js +14 -7
  87. package/dist/modules/Schedule/index.d.ts +18 -5
  88. package/dist/modules/Schedule/index.js +78 -24
  89. package/dist/modules/Schedule/types.d.ts +2 -0
  90. package/dist/modules/Summary/index.js +4 -3
  91. package/dist/modules/SurchargeList/index.d.ts +18 -0
  92. package/dist/modules/SurchargeList/index.js +185 -0
  93. package/dist/modules/SurchargeList/types.d.ts +15 -0
  94. package/dist/modules/SurchargeList/types.js +1 -0
  95. package/dist/modules/index.d.ts +3 -0
  96. package/dist/modules/index.js +4 -1
  97. package/dist/plugins/app.d.ts +2 -0
  98. package/dist/plugins/request.d.ts +1 -0
  99. package/dist/server/index.d.ts +462 -8
  100. package/dist/server/index.js +6112 -656
  101. package/dist/server/modules/floor-plan/index.d.ts +39 -0
  102. package/dist/server/modules/floor-plan/index.js +595 -0
  103. package/dist/server/modules/floor-plan/types.d.ts +43 -0
  104. package/dist/server/modules/floor-plan/types.js +13 -0
  105. package/dist/server/modules/index.d.ts +7 -2
  106. package/dist/server/modules/index.js +6 -0
  107. package/dist/server/modules/order/index.d.ts +207 -14
  108. package/dist/server/modules/order/index.js +2326 -379
  109. package/dist/server/modules/order/types.d.ts +76 -5
  110. package/dist/server/modules/order/types.js +14 -1
  111. package/dist/server/modules/order/utils/filterBookings.d.ts +7 -1
  112. package/dist/server/modules/order/utils/filterBookings.js +118 -11
  113. package/dist/server/modules/order/utils/filterOrders.js +242 -14
  114. package/dist/server/modules/payment/index.d.ts +28 -0
  115. package/dist/server/modules/payment/index.js +305 -0
  116. package/dist/server/modules/payment/types.d.ts +9 -0
  117. package/dist/server/modules/payment/types.js +1 -0
  118. package/dist/server/modules/products/index.d.ts +105 -14
  119. package/dist/server/modules/products/index.js +1393 -516
  120. package/dist/server/modules/products/types.d.ts +34 -1
  121. package/dist/server/modules/products/types.js +4 -0
  122. package/dist/server/modules/quotation/index.d.ts +39 -0
  123. package/dist/server/modules/quotation/index.js +278 -66
  124. package/dist/server/modules/resource/index.d.ts +2 -3
  125. package/dist/server/modules/resource/index.js +108 -86
  126. package/dist/server/modules/schedule/index.d.ts +6 -4
  127. package/dist/server/modules/schedule/index.js +116 -85
  128. package/dist/server/test.json +713 -0
  129. package/dist/server/types.d.ts +2 -0
  130. package/dist/server/utils/product.js +2 -2
  131. package/dist/server/utils/small-ticket.d.ts +71 -0
  132. package/dist/server/utils/small-ticket.js +963 -0
  133. package/dist/solution/BaseSales/index.d.ts +378 -0
  134. package/dist/solution/BaseSales/index.js +4006 -0
  135. package/dist/solution/BaseSales/types.d.ts +394 -0
  136. package/dist/solution/BaseSales/types.js +40 -0
  137. package/dist/solution/BaseSales/utils/cartPromotion.d.ts +287 -0
  138. package/dist/solution/BaseSales/utils/cartPromotion.js +1450 -0
  139. package/dist/solution/BaseSales/utils/parseSalesResponse.d.ts +144 -0
  140. package/dist/solution/BaseSales/utils/parseSalesResponse.js +157 -0
  141. package/dist/solution/BaseSales/utils/quotationPrice.d.ts +18 -0
  142. package/dist/solution/BaseSales/utils/quotationPrice.js +250 -0
  143. package/dist/solution/BaseSales/utils/transformBaseProductToOrderProduct.d.ts +63 -0
  144. package/dist/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +218 -0
  145. package/dist/solution/BaseSales/utils.d.ts +40 -0
  146. package/dist/solution/BaseSales/utils.js +263 -0
  147. package/dist/solution/BookingByStep/index.d.ts +1 -1
  148. package/dist/solution/BookingTicket/index.d.ts +394 -25
  149. package/dist/solution/BookingTicket/index.js +2506 -443
  150. package/dist/solution/BookingTicket/types.d.ts +191 -6
  151. package/dist/solution/BookingTicket/types.js +83 -10
  152. package/dist/solution/BookingTicket/utils/addProductDecision.d.ts +117 -0
  153. package/dist/solution/BookingTicket/utils/addProductDecision.js +346 -0
  154. package/dist/solution/BookingTicket/utils/addTimeAvailability.d.ts +35 -0
  155. package/dist/solution/BookingTicket/utils/addTimeAvailability.js +95 -0
  156. package/dist/solution/BookingTicket/utils/bookingStatus.d.ts +77 -0
  157. package/dist/solution/BookingTicket/utils/bookingStatus.js +136 -0
  158. package/dist/solution/BookingTicket/utils/cartView.d.ts +29 -0
  159. package/dist/solution/BookingTicket/utils/cartView.js +209 -0
  160. package/dist/solution/BookingTicket/utils/exampleData.d.ts +123 -0
  161. package/dist/solution/BookingTicket/utils/exampleData.js +183 -0
  162. package/dist/solution/BookingTicket/utils/orderCustomer.d.ts +4 -0
  163. package/dist/solution/BookingTicket/utils/orderCustomer.js +31 -0
  164. package/dist/solution/BookingTicket/utils/resolveBestAddTimePlan.d.ts +185 -0
  165. package/dist/solution/BookingTicket/utils/resolveBestAddTimePlan.js +438 -0
  166. package/dist/solution/BookingTicket/utils/scan/applyGlobalScan.d.ts +36 -0
  167. package/dist/solution/BookingTicket/utils/scan/applyGlobalScan.js +400 -0
  168. package/dist/solution/BookingTicket/utils/scan/cloudSearch.d.ts +4 -0
  169. package/dist/solution/BookingTicket/utils/scan/cloudSearch.js +41 -1
  170. package/dist/solution/BookingTicket/utils/scan/formatGlobalScan.d.ts +32 -0
  171. package/dist/solution/BookingTicket/utils/scan/formatGlobalScan.js +56 -0
  172. package/dist/solution/BookingTicket/utils/scan/handleScan.d.ts +3 -1
  173. package/dist/solution/BookingTicket/utils/scan/handleScan.js +11 -3
  174. package/dist/solution/BookingTicket/utils/scan/index.d.ts +5 -0
  175. package/dist/solution/BookingTicket/utils/scan/index.js +20 -8
  176. package/dist/solution/BookingTicket/utils/scan/scanProductValues.d.ts +21 -0
  177. package/dist/solution/BookingTicket/utils/scan/scanProductValues.js +150 -0
  178. package/dist/solution/BookingTicket/utils/sessionCatalogStale.d.ts +23 -0
  179. package/dist/solution/BookingTicket/utils/sessionCatalogStale.js +78 -0
  180. package/dist/solution/Checkout/index.d.ts +1 -0
  181. package/dist/solution/Checkout/index.js +25 -34
  182. package/dist/solution/Checkout/utils/index.js +4 -4
  183. package/dist/solution/RegisterAndLogin/config.js +2 -10
  184. package/dist/solution/Sales/index.d.ts +50 -5
  185. package/dist/solution/Sales/index.js +484 -92
  186. package/dist/solution/Sales/types.d.ts +26 -2
  187. package/dist/solution/ScanOrder/index.d.ts +27 -21
  188. package/dist/solution/ScanOrder/index.js +499 -434
  189. package/dist/solution/ScanOrder/types.d.ts +33 -11
  190. package/dist/solution/ScanOrder/types.js +4 -5
  191. package/dist/solution/ScanOrder/utils.d.ts +4 -1
  192. package/dist/solution/ScanOrder/utils.js +109 -34
  193. package/dist/solution/ShopDiscount/index.js +4 -4
  194. package/dist/solution/UnifiedBookingSales/index.d.ts +183 -0
  195. package/dist/solution/UnifiedBookingSales/index.js +1871 -0
  196. package/dist/solution/UnifiedBookingSales/types.d.ts +143 -0
  197. package/dist/solution/UnifiedBookingSales/types.js +11 -0
  198. package/dist/solution/VenueBooking/index.d.ts +29 -20
  199. package/dist/solution/VenueBooking/index.js +301 -213
  200. package/dist/solution/VenueBooking/sales-section-4-annotated.json +0 -2
  201. package/dist/solution/VenueBooking/utils/slotMerge.d.ts +6 -0
  202. package/dist/solution/VenueBooking/utils/slotMerge.js +9 -3
  203. package/dist/solution/index.d.ts +2 -0
  204. package/dist/solution/index.js +3 -1
  205. package/dist/types/index.d.ts +11 -1
  206. package/lib/model/strategy/adapter/promotion/evaluator.js +3 -2
  207. package/lib/model/strategy/adapter/walletPass/utils.js +6 -4
  208. package/lib/modules/BaseModule.d.ts +2 -2
  209. package/lib/modules/BaseModule.js +4 -4
  210. package/lib/modules/BookingContext/index.d.ts +48 -0
  211. package/lib/modules/BookingContext/index.js +390 -0
  212. package/lib/modules/BookingContext/types.d.ts +102 -0
  213. package/lib/modules/BookingContext/types.js +48 -0
  214. package/lib/modules/BookingContext/utils/buildCacheItemFromOrderLine.d.ts +16 -0
  215. package/lib/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +286 -0
  216. package/lib/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.d.ts +8 -0
  217. package/lib/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.js +148 -0
  218. package/lib/modules/BookingContext/utils/cacheItemToBookingInput.d.ts +76 -0
  219. package/lib/modules/BookingContext/utils/cacheItemToBookingInput.js +242 -0
  220. package/lib/modules/BookingContext/utils/flexible.d.ts +28 -0
  221. package/lib/modules/BookingContext/utils/flexible.js +63 -0
  222. package/lib/modules/BookingContext/utils/formatResourceList.d.ts +8 -0
  223. package/lib/modules/BookingContext/utils/formatResourceList.js +35 -0
  224. package/lib/modules/BookingContext/utils/index.d.ts +11 -0
  225. package/lib/modules/BookingContext/utils/index.js +126 -0
  226. package/lib/modules/BookingContext/utils/noResource.d.ts +13 -0
  227. package/lib/modules/BookingContext/utils/noResource.js +78 -0
  228. package/lib/modules/BookingContext/utils/orderLineDisplay.d.ts +1 -0
  229. package/lib/modules/BookingContext/utils/orderLineDisplay.js +29 -0
  230. package/lib/modules/BookingContext/utils/productExtend.d.ts +72 -0
  231. package/lib/modules/BookingContext/utils/productExtend.js +330 -0
  232. package/lib/modules/BookingContext/utils/resourceErrors.d.ts +41 -0
  233. package/lib/modules/BookingContext/utils/resourceErrors.js +80 -0
  234. package/lib/modules/BookingContext/utils/resourceSelection.d.ts +7 -0
  235. package/lib/modules/BookingContext/utils/resourceSelection.js +24 -0
  236. package/lib/modules/BookingContext/utils/resources.d.ts +80 -0
  237. package/lib/modules/BookingContext/utils/resources.js +419 -0
  238. package/lib/modules/BookingContext/utils/serviceTimes.d.ts +55 -0
  239. package/lib/modules/BookingContext/utils/serviceTimes.js +161 -0
  240. package/lib/modules/BookingContext/utils/timeSlices.d.ts +42 -0
  241. package/lib/modules/BookingContext/utils/timeSlices.js +114 -0
  242. package/lib/modules/Cart/utils/cartProduct.js +27 -5
  243. package/lib/modules/Cart/utils/changePrice.js +20 -14
  244. package/lib/modules/Customer/index.d.ts +20 -0
  245. package/lib/modules/Customer/index.js +63 -10
  246. package/lib/modules/Customer/types.d.ts +2 -0
  247. package/lib/modules/Discount/index.d.ts +2 -1
  248. package/lib/modules/Discount/index.js +18 -2
  249. package/lib/modules/Discount/types.d.ts +1 -0
  250. package/lib/modules/OpenData/index.d.ts +8 -0
  251. package/lib/modules/OpenData/index.js +21 -3
  252. package/lib/modules/Order/index.d.ts +448 -23
  253. package/lib/modules/Order/index.js +3641 -308
  254. package/lib/modules/Order/types.d.ts +693 -88
  255. package/lib/modules/Order/types.js +55 -1
  256. package/lib/modules/Order/utils/bundleDiscountHydration.d.ts +5 -0
  257. package/lib/modules/Order/utils/bundleDiscountHydration.js +106 -0
  258. package/lib/modules/Order/utils/manualProductDiscount.d.ts +106 -0
  259. package/lib/modules/Order/utils/manualProductDiscount.js +207 -0
  260. package/lib/modules/Order/utils/orderCollectionIdentity.d.ts +53 -0
  261. package/lib/modules/Order/utils/orderCollectionIdentity.js +353 -0
  262. package/lib/modules/Order/utils.d.ts +167 -18
  263. package/lib/modules/Order/utils.js +974 -138
  264. package/lib/modules/Payment/index.d.ts +10 -9
  265. package/lib/modules/Payment/index.js +30 -124
  266. package/lib/modules/Payment/types.d.ts +40 -16
  267. package/lib/modules/Payment/utils.d.ts +1 -1
  268. package/lib/modules/Payment/utils.js +4 -3
  269. package/lib/modules/Payment/walletpass.js +16 -6
  270. package/lib/modules/Product/types.d.ts +29 -7
  271. package/lib/modules/ProductList/index.d.ts +17 -13
  272. package/lib/modules/ProductList/index.js +92 -51
  273. package/lib/modules/ProductList/types.d.ts +17 -0
  274. package/lib/modules/Quotation/index.d.ts +22 -2
  275. package/lib/modules/Quotation/index.js +76 -20
  276. package/lib/modules/Quotation/types.d.ts +19 -0
  277. package/lib/modules/ResourcePlanner/index.d.ts +24 -0
  278. package/lib/modules/ResourcePlanner/index.js +147 -0
  279. package/lib/modules/ResourcePlanner/planner.d.ts +14 -0
  280. package/lib/modules/ResourcePlanner/planner.js +895 -0
  281. package/lib/modules/ResourcePlanner/types.d.ts +227 -0
  282. package/lib/modules/ResourcePlanner/types.js +5 -0
  283. package/lib/modules/Rules/index.d.ts +8 -10
  284. package/lib/modules/Rules/index.js +152 -60
  285. package/lib/modules/Rules/types.d.ts +14 -3
  286. package/lib/modules/SalesSummary/index.d.ts +23 -26
  287. package/lib/modules/SalesSummary/index.js +84 -16
  288. package/lib/modules/SalesSummary/types.d.ts +9 -1
  289. package/lib/modules/SalesSummary/utils.d.ts +3 -22
  290. package/lib/modules/SalesSummary/utils.js +616 -111
  291. package/lib/modules/Schedule/getDateIsInSchedule.js +14 -7
  292. package/lib/modules/Schedule/index.d.ts +18 -5
  293. package/lib/modules/Schedule/index.js +49 -14
  294. package/lib/modules/Schedule/types.d.ts +2 -0
  295. package/lib/modules/Summary/index.js +2 -2
  296. package/lib/modules/SurchargeList/index.d.ts +18 -0
  297. package/lib/modules/SurchargeList/index.js +95 -0
  298. package/lib/modules/SurchargeList/types.d.ts +15 -0
  299. package/lib/modules/SurchargeList/types.js +5 -0
  300. package/lib/modules/index.d.ts +3 -0
  301. package/lib/modules/index.js +33 -0
  302. package/lib/plugins/app.d.ts +2 -0
  303. package/lib/plugins/request.d.ts +1 -0
  304. package/lib/server/index.d.ts +462 -8
  305. package/lib/server/index.js +3901 -205
  306. package/lib/server/modules/floor-plan/index.d.ts +39 -0
  307. package/lib/server/modules/floor-plan/index.js +306 -0
  308. package/lib/server/modules/floor-plan/types.d.ts +43 -0
  309. package/lib/server/modules/floor-plan/types.js +19 -0
  310. package/lib/server/modules/index.d.ts +7 -2
  311. package/lib/server/modules/index.js +21 -0
  312. package/lib/server/modules/order/index.d.ts +207 -14
  313. package/lib/server/modules/order/index.js +1324 -141
  314. package/lib/server/modules/order/types.d.ts +76 -5
  315. package/lib/server/modules/order/types.js +11 -1
  316. package/lib/server/modules/order/utils/filterBookings.d.ts +7 -1
  317. package/lib/server/modules/order/utils/filterBookings.js +119 -11
  318. package/lib/server/modules/order/utils/filterOrders.js +238 -13
  319. package/lib/server/modules/payment/index.d.ts +28 -0
  320. package/lib/server/modules/payment/index.js +168 -0
  321. package/lib/server/modules/payment/types.d.ts +9 -0
  322. package/lib/server/modules/payment/types.js +5 -0
  323. package/lib/server/modules/products/index.d.ts +105 -14
  324. package/lib/server/modules/products/index.js +588 -97
  325. package/lib/server/modules/products/types.d.ts +34 -1
  326. package/lib/server/modules/products/types.js +3 -0
  327. package/lib/server/modules/quotation/index.d.ts +39 -0
  328. package/lib/server/modules/quotation/index.js +145 -1
  329. package/lib/server/modules/resource/index.d.ts +2 -3
  330. package/lib/server/modules/resource/index.js +71 -45
  331. package/lib/server/modules/schedule/index.d.ts +6 -4
  332. package/lib/server/modules/schedule/index.js +55 -44
  333. package/lib/server/test.json +713 -0
  334. package/lib/server/types.d.ts +2 -0
  335. package/lib/server/utils/product.js +2 -2
  336. package/lib/server/utils/small-ticket.d.ts +71 -0
  337. package/lib/server/utils/small-ticket.js +926 -0
  338. package/lib/solution/BaseSales/index.d.ts +378 -0
  339. package/lib/solution/BaseSales/index.js +2367 -0
  340. package/lib/solution/BaseSales/types.d.ts +394 -0
  341. package/lib/solution/BaseSales/types.js +47 -0
  342. package/lib/solution/BaseSales/utils/cartPromotion.d.ts +287 -0
  343. package/lib/solution/BaseSales/utils/cartPromotion.js +1155 -0
  344. package/lib/solution/BaseSales/utils/parseSalesResponse.d.ts +144 -0
  345. package/lib/solution/BaseSales/utils/parseSalesResponse.js +140 -0
  346. package/lib/solution/BaseSales/utils/quotationPrice.d.ts +18 -0
  347. package/lib/solution/BaseSales/utils/quotationPrice.js +231 -0
  348. package/lib/solution/BaseSales/utils/transformBaseProductToOrderProduct.d.ts +63 -0
  349. package/lib/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +215 -0
  350. package/lib/solution/BaseSales/utils.d.ts +40 -0
  351. package/lib/solution/BaseSales/utils.js +259 -0
  352. package/lib/solution/BookingByStep/index.d.ts +1 -1
  353. package/lib/solution/BookingTicket/index.d.ts +394 -25
  354. package/lib/solution/BookingTicket/index.js +1350 -76
  355. package/lib/solution/BookingTicket/types.d.ts +191 -6
  356. package/lib/solution/BookingTicket/types.js +71 -10
  357. package/lib/solution/BookingTicket/utils/addProductDecision.d.ts +117 -0
  358. package/lib/solution/BookingTicket/utils/addProductDecision.js +353 -0
  359. package/lib/solution/BookingTicket/utils/addTimeAvailability.d.ts +35 -0
  360. package/lib/solution/BookingTicket/utils/addTimeAvailability.js +94 -0
  361. package/lib/solution/BookingTicket/utils/bookingStatus.d.ts +77 -0
  362. package/lib/solution/BookingTicket/utils/bookingStatus.js +136 -0
  363. package/lib/solution/BookingTicket/utils/cartView.d.ts +29 -0
  364. package/lib/solution/BookingTicket/utils/cartView.js +200 -0
  365. package/lib/solution/BookingTicket/utils/exampleData.d.ts +123 -0
  366. package/lib/solution/BookingTicket/utils/exampleData.js +185 -0
  367. package/lib/solution/BookingTicket/utils/orderCustomer.d.ts +4 -0
  368. package/lib/solution/BookingTicket/utils/orderCustomer.js +35 -0
  369. package/lib/solution/BookingTicket/utils/resolveBestAddTimePlan.d.ts +185 -0
  370. package/lib/solution/BookingTicket/utils/resolveBestAddTimePlan.js +387 -0
  371. package/lib/solution/BookingTicket/utils/scan/applyGlobalScan.d.ts +36 -0
  372. package/lib/solution/BookingTicket/utils/scan/applyGlobalScan.js +220 -0
  373. package/lib/solution/BookingTicket/utils/scan/cloudSearch.d.ts +4 -0
  374. package/lib/solution/BookingTicket/utils/scan/cloudSearch.js +24 -3
  375. package/lib/solution/BookingTicket/utils/scan/formatGlobalScan.d.ts +32 -0
  376. package/lib/solution/BookingTicket/utils/scan/formatGlobalScan.js +64 -0
  377. package/lib/solution/BookingTicket/utils/scan/handleScan.d.ts +3 -1
  378. package/lib/solution/BookingTicket/utils/scan/handleScan.js +4 -2
  379. package/lib/solution/BookingTicket/utils/scan/index.d.ts +5 -0
  380. package/lib/solution/BookingTicket/utils/scan/index.js +14 -2
  381. package/lib/solution/BookingTicket/utils/scan/scanProductValues.d.ts +21 -0
  382. package/lib/solution/BookingTicket/utils/scan/scanProductValues.js +115 -0
  383. package/lib/solution/BookingTicket/utils/sessionCatalogStale.d.ts +23 -0
  384. package/lib/solution/BookingTicket/utils/sessionCatalogStale.js +83 -0
  385. package/lib/solution/Checkout/index.d.ts +1 -0
  386. package/lib/solution/Checkout/index.js +18 -19
  387. package/lib/solution/Checkout/utils/index.js +4 -4
  388. package/lib/solution/RegisterAndLogin/config.js +2 -10
  389. package/lib/solution/Sales/index.d.ts +50 -5
  390. package/lib/solution/Sales/index.js +296 -39
  391. package/lib/solution/Sales/types.d.ts +26 -2
  392. package/lib/solution/ScanOrder/index.d.ts +27 -21
  393. package/lib/solution/ScanOrder/index.js +144 -103
  394. package/lib/solution/ScanOrder/types.d.ts +33 -11
  395. package/lib/solution/ScanOrder/types.js +4 -5
  396. package/lib/solution/ScanOrder/utils.d.ts +4 -1
  397. package/lib/solution/ScanOrder/utils.js +98 -26
  398. package/lib/solution/ShopDiscount/index.js +3 -3
  399. package/lib/solution/UnifiedBookingSales/index.d.ts +183 -0
  400. package/lib/solution/UnifiedBookingSales/index.js +1050 -0
  401. package/lib/solution/UnifiedBookingSales/types.d.ts +143 -0
  402. package/lib/solution/UnifiedBookingSales/types.js +14 -0
  403. package/lib/solution/VenueBooking/index.d.ts +29 -20
  404. package/lib/solution/VenueBooking/index.js +169 -94
  405. package/lib/solution/VenueBooking/sales-section-4-annotated.json +0 -2
  406. package/lib/solution/VenueBooking/utils/slotMerge.d.ts +6 -0
  407. package/lib/solution/VenueBooking/utils/slotMerge.js +9 -3
  408. package/lib/solution/index.d.ts +2 -0
  409. package/lib/solution/index.js +22 -0
  410. package/lib/types/index.d.ts +11 -1
  411. package/package.json +1 -1
  412. package/dist/solution/Checkout/appointmentDemo.json +0 -1
  413. package/lib/solution/Checkout/appointmentDemo.json +0 -1
@@ -11,13 +11,17 @@ var _menu = require("./modules/menu");
11
11
  var _quotation = require("./modules/quotation");
12
12
  var _schedule = require("./modules/schedule");
13
13
  var _resource = require("./modules/resource");
14
+ var _floorPlan = require("./modules/floor-plan");
15
+ var _types = require("./modules/floor-plan/types");
16
+ var _payment = require("./modules/payment");
14
17
  var _schedule2 = require("./utils/schedule");
15
- var _types = require("./modules/products/types");
18
+ var _types2 = require("./modules/products/types");
16
19
  var _product = require("./utils/product");
17
20
  var _order = require("./modules/order");
18
- var _types2 = require("./modules/order/types");
21
+ var _types3 = require("./modules/order/types");
19
22
  var _filterOrders = require("./modules/order/utils/filterOrders");
20
23
  var _filterBookings = require("./modules/order/utils/filterBookings");
24
+ var _smallTicket = require("./utils/small-ticket");
21
25
  var _modules = require("./modules");
22
26
  Object.keys(_modules).forEach(function (key) {
23
27
  if (key === "default" || key === "__esModule") return;
@@ -39,6 +43,14 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
39
43
 
40
44
  /** 预约列表查询订阅者 */
41
45
 
46
+ /** 非当天(远端)预约查询订阅者 */
47
+
48
+ /** Server 运行时诊断快照,用于排查订阅者是否持续增长。 */
49
+
50
+ /** 平面图 GET 查询订阅者(列表 / id 详情 / code 详情) */
51
+
52
+ const PRODUCT_TITLE_SNAPSHOT_KEYS = ['en', 'ja', 'pt', 'auto', 'zh-CN', 'zh-HK', 'kitchen', 'original'];
53
+
42
54
  /**
43
55
  * Server 类
44
56
  * 用于注册和管理服务端模块
@@ -55,6 +67,15 @@ class Server {
55
67
  schedule;
56
68
  resource;
57
69
  order;
70
+ floor_plan;
71
+ payment;
72
+ paymentRouteModule;
73
+ paymentRouteModuleInFlight;
74
+ quotationAvailableCache = new Map();
75
+ quotationAvailableInFlight = new Map();
76
+
77
+ /** GET 前缀路由(最长前缀优先匹配) */
78
+ prefixRouterGet = [];
58
79
 
59
80
  // 路由注册表
60
81
  router = {
@@ -66,10 +87,23 @@ class Server {
66
87
 
67
88
  // ---- 商品查询订阅者 ----
68
89
  productQuerySubscribers = new Map();
90
+ /** subscriberId → 智能定价变价时间点定时器句柄(与 subscriber 分离存储,便于 clear) */
91
+ productQueryScheduleTimers = new Map();
92
+ /** 餐牌筛选为空时才触发的菜单/日程缓存自愈,避免每次商品查询都请求远端。 */
93
+ menuScheduleRefreshInFlight = null;
94
+ lastMenuScheduleRefreshAt = 0;
95
+ MENU_SCHEDULE_REFRESH_COOLDOWN_MS = 30 * 1000;
69
96
 
70
97
  // ---- 订单 / 预约列表查询订阅者 ----
71
98
  orderQuerySubscribers = new Map();
72
99
  bookingQuerySubscribers = new Map();
100
+ bookingRemoteQuerySubscribers = new Map();
101
+ bookingRemoteCache = new Map();
102
+ BOOKING_REMOTE_CACHE_TTL_MS = 5 * 60 * 1000;
103
+ BOOKING_REMOTE_CACHE_MAX_ENTRIES = 80;
104
+ salesSearchSubscribers = new Map();
105
+ deviceTaskActionsRegistered = false;
106
+ floorPlanQuerySubscribers = new Map();
73
107
 
74
108
  // 模块注册表 - 定义所有可用的模块配置
75
109
  moduleRegistry = {
@@ -121,6 +155,15 @@ class Server {
121
155
  list: []
122
156
  }
123
157
  },
158
+ payment: {
159
+ name: 'payment',
160
+ moduleClass: _payment.PaymentServerModule,
161
+ moduleName: 'server_payment',
162
+ version: '1.0.0',
163
+ defaultStore: {
164
+ payMethods: []
165
+ }
166
+ },
124
167
  resource: {
125
168
  name: 'resource',
126
169
  moduleClass: _resource.ResourceModule,
@@ -131,6 +174,17 @@ class Server {
131
174
  map: new Map(),
132
175
  bookings: []
133
176
  }
177
+ },
178
+ floor_plan: {
179
+ name: 'floor_plan',
180
+ moduleClass: _floorPlan.FloorPlanModule,
181
+ moduleName: 'server_floor_plan',
182
+ version: '1.0.0',
183
+ defaultStore: {
184
+ list: [],
185
+ map: new Map(),
186
+ codeMap: new Map()
187
+ }
134
188
  }
135
189
  };
136
190
  constructor(core) {
@@ -144,6 +198,28 @@ class Server {
144
198
  });
145
199
  }
146
200
 
201
+ /**
202
+ * 获取 Server 内部订阅者和缓存数量快照。
203
+ *
204
+ * @example
205
+ * const snapshot = pisellos.server?.getDiagnosticsSnapshot?.();
206
+ */
207
+ getDiagnosticsSnapshot() {
208
+ return {
209
+ subscribers: {
210
+ productQuerySubscribers: this.productQuerySubscribers.size,
211
+ orderQuerySubscribers: this.orderQuerySubscribers.size,
212
+ bookingQuerySubscribers: this.bookingQuerySubscribers.size,
213
+ bookingRemoteQuerySubscribers: this.bookingRemoteQuerySubscribers.size,
214
+ salesSearchSubscribers: this.salesSearchSubscribers.size,
215
+ floorPlanQuerySubscribers: this.floorPlanQuerySubscribers.size
216
+ },
217
+ caches: {
218
+ bookingRemoteCache: this.bookingRemoteCache.size
219
+ }
220
+ };
221
+ }
222
+
147
223
  /**
148
224
  * 注册路由
149
225
  * @param routes 路由定义数组
@@ -180,6 +256,29 @@ class Server {
180
256
  });
181
257
  }
182
258
 
259
+ /**
260
+ * 注册前缀路由(仅 GET)。匹配规则:path === prefix 或 path 以 prefix + '/' 开头;最长前缀优先。
261
+ */
262
+ registerPrefixRoutes(routes) {
263
+ for (const r of routes) {
264
+ if (r.method !== 'get') {
265
+ this.logWarning('registerPrefixRoutes: 当前仅支持 GET', {
266
+ method: r.method,
267
+ prefix: r.prefix
268
+ });
269
+ continue;
270
+ }
271
+ this.prefixRouterGet.push({
272
+ prefix: r.prefix,
273
+ handler: r.handler
274
+ });
275
+ }
276
+ this.prefixRouterGet.sort((a, b) => b.prefix.length - a.prefix.length);
277
+ this.logInfo('前缀 GET 路由已更新', {
278
+ count: this.prefixRouterGet.length
279
+ });
280
+ }
281
+
183
282
  /**
184
283
  * 注册单个模块并自动注册其路由
185
284
  * @param module 模块实例
@@ -386,6 +485,7 @@ class Server {
386
485
 
387
486
  // 注册 Server 层的业务路由
388
487
  this.registerServerRoutes();
488
+ this.registerDeviceTaskActions();
389
489
 
390
490
  // 注册模块
391
491
  const registeredModules = await this.registerModules(moduleConfigs);
@@ -398,18 +498,26 @@ class Server {
398
498
  autoPreload
399
499
  });
400
500
  }
501
+ await this.setupProductsQuotationPriceBridge();
401
502
 
402
503
  // 监听 ProductsModule 同步完成事件,重新计算并推送查询结果
403
- this.core.effects.on(_types.ProductsHooks.onProductsSyncCompleted, payload => {
504
+ this.core.effects.on(_types2.ProductsHooks.onProductsSyncCompleted, payload => {
404
505
  this.recomputeAndNotifyProductQuery({
405
506
  changedIds: payload?.changedIds
406
507
  });
407
508
  });
408
509
 
510
+ // 平面图:本地列表任意变更(预加载、pubsub 同步)时,向 GET 订阅者推送最新列表/详情
511
+ this.core.effects.on(_types.FloorPlanHooks.onFloorPlansChanged, () => {
512
+ void this.recomputeAndNotifyFloorPlanQuery();
513
+ });
514
+
409
515
  // 订单 / 预约本地数据变更时,向订阅者推送最新列表
410
- this.core.effects.on(_types2.OrderHooks.onOrdersChanged, () => {
516
+ this.core.effects.on(_types3.OrderHooks.onOrdersChanged, payload => {
411
517
  this.recomputeAndNotifyOrderQuery();
412
518
  this.recomputeAndNotifyBookingQuery();
519
+ this.syncBookingRemoteCacheFromOrderChanges();
520
+ this.notifySalesSearchSubscribers(payload);
413
521
  });
414
522
  const duration = Date.now() - startTime;
415
523
  this.logInfo('Server 初始化完成', {
@@ -418,6 +526,237 @@ class Server {
418
526
  });
419
527
  return registeredModules;
420
528
  }
529
+ registerDeviceTaskActions() {
530
+ if (this.deviceTaskActionsRegistered) return;
531
+ const deviceTask = this.getDeviceTaskPlugin();
532
+ if (!deviceTask?.registerAction) {
533
+ this.logWarning('registerDeviceTaskActions: deviceTask 插件不可用');
534
+ return;
535
+ }
536
+ const registeredActions = new Set(typeof deviceTask.getActions === 'function' ? (deviceTask.getActions() || []).map(action => action?.action).filter(Boolean) : []);
537
+ const register = definition => {
538
+ if (registeredActions.has(definition.action)) return;
539
+ deviceTask.registerAction?.(definition);
540
+ registeredActions.add(definition.action);
541
+ };
542
+ register({
543
+ action: 'sync_sales',
544
+ name: '同步订单',
545
+ validate: payload => payload?.backendPath && payload?.data ? null : '缺少 backendPath 或 data',
546
+ handler: ctx => this.handleSyncSalesTask(ctx)
547
+ });
548
+ this.deviceTaskActionsRegistered = true;
549
+ this.logInfo('DeviceTask actions 已注册', {
550
+ actions: ['sync_sales']
551
+ });
552
+ }
553
+ async handleSyncSalesTask(ctx) {
554
+ const payload = ctx.task?.payload || {};
555
+ const backendPath = payload.backendPath;
556
+ const data = payload.data;
557
+ const title = payload.title || 'sync_sales';
558
+ if (!backendPath || !data) {
559
+ return this.taskFail('INVALID_PAYLOAD', '缺少 backendPath 或 data');
560
+ }
561
+ if (!this.app?.request) {
562
+ this.logError(`${title}: sync_sales app.request 不可用`);
563
+ return this.taskTimeout('app.request 不可用');
564
+ }
565
+ try {
566
+ const syncResult = await this.runCheckoutSync({
567
+ backendPath,
568
+ data,
569
+ title,
570
+ externalSaleNumber: payload.external_sale_number,
571
+ deviceId: ctx.task?.device_id,
572
+ persistCheckoutDataWithResponse: false
573
+ });
574
+ if (syncResult.rejected) {
575
+ this.logError(`${title}: sync_sales 后端明确失败`, {
576
+ backendPath,
577
+ response: syncResult.errorResponse
578
+ });
579
+ return this.taskFail('BACKEND_REJECTED', syncResult.message || '后端明确拒绝 checkout', syncResult.errorResponse);
580
+ }
581
+ return this.taskOk({
582
+ order: syncResult.syncedOrder ?? null,
583
+ response: syncResult.normalizedResponse
584
+ });
585
+ } catch (error) {
586
+ const backendError = this.extractBackendErrorResponse(error);
587
+ if (backendError) {
588
+ const message = backendError?.message || '后端明确拒绝 checkout';
589
+ this.logError(`${title}: sync_sales 后端明确失败`, {
590
+ backendPath,
591
+ backendError
592
+ });
593
+ return this.taskFail('BACKEND_REJECTED', message, backendError);
594
+ }
595
+ const errorMessage = this.getUnknownErrorMessage(error);
596
+ this.logError(`${title}: sync_sales 请求未明确成功`, {
597
+ backendPath,
598
+ error: errorMessage,
599
+ error_detail: this.normalizeUnknownError(error)
600
+ });
601
+ return this.taskTimeout(errorMessage);
602
+ }
603
+ }
604
+ async runCheckoutSync(params) {
605
+ const {
606
+ backendPath,
607
+ title
608
+ } = params;
609
+ const normalizedData = await this.normalizeCheckoutSubmitData(params.data, title);
610
+ const checkoutData = await this.ensureCheckoutOrderNumbers(normalizedData, title);
611
+ const remoteCheckoutData = this.omitCheckoutSyncMode(checkoutData);
612
+ if (!this.app?.request?.post) {
613
+ throw new Error('app.request 不可用');
614
+ }
615
+ const response = await this.app.request.post(backendPath, remoteCheckoutData, {
616
+ isShopApi: true,
617
+ customToast: () => {}
618
+ });
619
+ if (this.isCheckoutResponseRejected(response)) {
620
+ return {
621
+ rejected: true,
622
+ message: response?.message || '后端明确拒绝 checkout',
623
+ errorResponse: response,
624
+ response,
625
+ normalizedResponse: this.normalizeCheckoutResponse(response),
626
+ checkoutData: remoteCheckoutData
627
+ };
628
+ }
629
+ const fresh = this.extractOrderDataFromCheckoutResponse(response);
630
+ const externalSaleNumber = this.getCheckoutExternalSaleNumber(remoteCheckoutData, {
631
+ external_sale_number: params.externalSaleNumber,
632
+ data: remoteCheckoutData
633
+ });
634
+ const syncedOrder = await this.persistSyncedCheckoutOrder({
635
+ backendPath,
636
+ checkoutData: remoteCheckoutData,
637
+ externalSaleNumber,
638
+ fresh,
639
+ title,
640
+ persistCheckoutDataWithResponse: params.persistCheckoutDataWithResponse === true
641
+ });
642
+ const printableSyncedOrder = syncedOrder ? this.buildSyncedOrderForPrint({
643
+ syncedOrder,
644
+ checkoutResponseOrder: fresh
645
+ }) : undefined;
646
+ if (printableSyncedOrder && this.shouldPrintSyncedOrder({
647
+ checkoutData: remoteCheckoutData,
648
+ syncedOrder: printableSyncedOrder
649
+ })) {
650
+ await this.dispatchPrintOtherReceiptTask({
651
+ checkoutData: remoteCheckoutData,
652
+ syncedOrder: printableSyncedOrder,
653
+ response,
654
+ deviceId: params.deviceId
655
+ });
656
+ }
657
+ return {
658
+ rejected: false,
659
+ response,
660
+ normalizedResponse: this.normalizeCheckoutResponse(response),
661
+ checkoutData: remoteCheckoutData,
662
+ syncedOrder,
663
+ fresh
664
+ };
665
+ }
666
+ omitCheckoutSyncMode(data) {
667
+ if (!data || typeof data !== 'object') return data;
668
+ const next = {
669
+ ...data
670
+ };
671
+ delete next.sync_mode;
672
+ delete next.syncMode;
673
+ return next;
674
+ }
675
+ async persistSyncedCheckoutOrder(params) {
676
+ const {
677
+ backendPath,
678
+ checkoutData,
679
+ externalSaleNumber,
680
+ fresh,
681
+ title,
682
+ persistCheckoutDataWithResponse
683
+ } = params;
684
+ const shouldMergeRemoteOrder = fresh ? this.isMergeableCheckoutOrder(fresh, externalSaleNumber) : false;
685
+ let syncedOrder;
686
+ if (this.order && fresh && (shouldMergeRemoteOrder || persistCheckoutDataWithResponse)) {
687
+ syncedOrder = persistCheckoutDataWithResponse ? {
688
+ ...checkoutData,
689
+ ...fresh,
690
+ external_sale_number: fresh.external_sale_number ?? externalSaleNumber,
691
+ need_sync: 0
692
+ } : {
693
+ ...fresh,
694
+ external_sale_number: fresh.external_sale_number ?? externalSaleNumber,
695
+ need_sync: 0
696
+ };
697
+ await this.order.upsertOrdersFromRemote([syncedOrder]);
698
+ this.logInfo(`${title}: sync_sales 完整订单已同步到本地`, {
699
+ order_id: fresh.order_id,
700
+ external_sale_number: syncedOrder.external_sale_number,
701
+ order_number: fresh.order_number,
702
+ mergedRemoteOrder: shouldMergeRemoteOrder,
703
+ persistedCheckoutDataWithResponse: persistCheckoutDataWithResponse
704
+ });
705
+ } else if (this.order && externalSaleNumber && typeof this.order.markOrderSyncedByExternalSaleNumber === 'function') {
706
+ const patch = this.buildCheckoutSyncSuccessPatch(fresh);
707
+ const result = await this.order.markOrderSyncedByExternalSaleNumber({
708
+ externalSaleNumber,
709
+ patch
710
+ });
711
+ syncedOrder = result?.order || {
712
+ ...checkoutData,
713
+ ...patch,
714
+ external_sale_number: externalSaleNumber,
715
+ need_sync: 0
716
+ };
717
+ this.logInfo(`${title}: sync_sales 已按 external_sale_number 标记本地订单`, {
718
+ order_id: syncedOrder?.order_id ?? null,
719
+ external_sale_number: externalSaleNumber,
720
+ hasRemoteOrder: !!fresh,
721
+ mergedRemoteOrder: false
722
+ });
723
+ } else {
724
+ this.logWarning(`${title}: sync_sales Order 模块未注册`, {
725
+ backendPath,
726
+ external_sale_number: externalSaleNumber,
727
+ order_id: fresh?.order_id
728
+ });
729
+ }
730
+ return syncedOrder;
731
+ }
732
+
733
+ /**
734
+ * 将普通层 QuotationModule 的报价单计算能力桥接到 Server Products 模块。
735
+ */
736
+ async setupProductsQuotationPriceBridge() {
737
+ if (!this.products) {
738
+ this.logInfo('setupProductsQuotationPriceBridge: Products 模块未注册');
739
+ return;
740
+ }
741
+ if (!this.schedule) {
742
+ this.logInfo('setupProductsQuotationPriceBridge: Schedule 模块未注册');
743
+ return;
744
+ }
745
+ try {
746
+ await this.schedule.loadAllSchedule();
747
+ this.products.clearPriceCache();
748
+ await this.products.setupQuotationPriceBridge({
749
+ scheduleModule: this.schedule,
750
+ channel: this.core.context?.channel
751
+ });
752
+ this.logInfo('Products 报价单价格桥接初始化完成');
753
+ } catch (error) {
754
+ const errorMessage = error instanceof Error ? error.message : String(error);
755
+ this.logError('Products 报价单价格桥接初始化失败', {
756
+ error: errorMessage
757
+ });
758
+ }
759
+ }
421
760
 
422
761
  /**
423
762
  * 获取 Products 模块实例
@@ -458,6 +797,7 @@ class Server {
458
797
  if (this.schedule) modules.push('schedule');
459
798
  if (this.resource) modules.push('resource');
460
799
  if (this.order) modules.push('order');
800
+ if (this.floor_plan) modules.push('floor_plan');
461
801
  return modules;
462
802
  }
463
803
 
@@ -490,6 +830,35 @@ class Server {
490
830
  }
491
831
  }
492
832
 
833
+ /**
834
+ * 后台静默刷新订单数据
835
+ * 重新拉取全量 SSE 接口,更新本地数据后触发 onOrdersSyncCompleted
836
+ * 不影响当前界面展示,适用于切回前台、定时刷新等场景
837
+ */
838
+ async refreshOrdersInBackground() {
839
+ if (!this.order) {
840
+ this.logWarning('refreshOrdersInBackground: Order 模块未注册');
841
+ return;
842
+ }
843
+ this.logInfo('refreshOrdersInBackground 开始');
844
+ const startTime = Date.now();
845
+ try {
846
+ await this.order.silentRefresh();
847
+ const duration = Date.now() - startTime;
848
+ this.logInfo('refreshOrdersInBackground 完成', {
849
+ duration: `${duration}ms`
850
+ });
851
+ } catch (error) {
852
+ const duration = Date.now() - startTime;
853
+ const errorMessage = error instanceof Error ? error.message : String(error);
854
+ console.error('[Server] refreshOrdersInBackground 失败:', error);
855
+ this.logError('refreshOrdersInBackground 失败', {
856
+ duration: `${duration}ms`,
857
+ error: errorMessage
858
+ });
859
+ }
860
+ }
861
+
493
862
  /**
494
863
  * 清空所有server模块的IndexedDB缓存
495
864
  * @returns Promise<void>
@@ -524,6 +893,10 @@ class Server {
524
893
  clearTasks.push(this.order.clear());
525
894
  moduleNames.push('Order');
526
895
  }
896
+ if (this.floor_plan) {
897
+ clearTasks.push(this.floor_plan.clear());
898
+ moduleNames.push('FloorPlan');
899
+ }
527
900
  if (clearTasks.length === 0) {
528
901
  console.warn('[Server] 没有找到已注册的模块,无需清空');
529
902
  this.logWarning('没有找到已注册的模块,无需清空 IndexedDB');
@@ -562,7 +935,25 @@ class Server {
562
935
  * @param path 路径
563
936
  */
564
937
  getRouteHandler(method, path) {
565
- return this.router[method]?.[path];
938
+ const exact = this.router[method]?.[path];
939
+ if (exact) {
940
+ return exact;
941
+ }
942
+ // GET /shop/product/query/:productId(纯数字 id),与 POST /shop/product/query、unsubscribe 区分
943
+ if (method === 'get' && /^\/shop\/product\/query\/\d+$/.test(path)) {
944
+ return this.handleProductQueryById;
945
+ }
946
+ if (method === 'get' && this.prefixRouterGet.length > 0) {
947
+ for (const {
948
+ prefix,
949
+ handler
950
+ } of this.prefixRouterGet) {
951
+ if (path === prefix || path.startsWith(`${prefix}/`)) {
952
+ return handler;
953
+ }
954
+ }
955
+ }
956
+ return undefined;
566
957
  }
567
958
 
568
959
  /**
@@ -571,7 +962,7 @@ class Server {
571
962
  * @param path 路径
572
963
  */
573
964
  hasRoute(method, path) {
574
- return !!this.router[method]?.[path];
965
+ return !!this.getRouteHandler(method, path);
575
966
  }
576
967
 
577
968
  /**
@@ -597,7 +988,10 @@ class Server {
597
988
  throw new Error(`Route not found: ${method.toUpperCase()} ${path}`);
598
989
  }
599
990
  try {
600
- const result = await handler(params);
991
+ const result = await handler({
992
+ ...params,
993
+ path
994
+ });
601
995
  const duration = Date.now() - startTime;
602
996
  this.logInfo(`路由请求完成: ${method.toUpperCase()} ${path}`, {
603
997
  method: method.toUpperCase(),
@@ -664,6 +1058,22 @@ class Server {
664
1058
  method: 'post',
665
1059
  path: '/shop/order/v2/list/unsubscribe',
666
1060
  handler: this.handleUnsubscribeOrderQuery.bind(this)
1061
+ }, {
1062
+ method: 'post',
1063
+ path: '/shop/es/search/sales',
1064
+ handler: this.handleSalesSearchModule.bind(this)
1065
+ }, {
1066
+ method: 'post',
1067
+ path: '/shop/es/search/sales/unsubscribe',
1068
+ handler: this.handleUnsubscribeSalesSearch.bind(this)
1069
+ }, {
1070
+ method: 'post',
1071
+ path: '/shop/es/search',
1072
+ handler: this.handleSalesSearchAggregate.bind(this)
1073
+ }, {
1074
+ method: 'post',
1075
+ path: '/shop/es/search/unsubscribe',
1076
+ handler: this.handleUnsubscribeSalesSearch.bind(this)
667
1077
  }, {
668
1078
  method: 'get',
669
1079
  path: '/shop/schedule/booking',
@@ -676,14 +1086,145 @@ class Server {
676
1086
  method: 'get',
677
1087
  path: '/shop/form/resource/page',
678
1088
  handler: this.handleResourceList.bind(this)
1089
+ }, {
1090
+ method: 'get',
1091
+ path: '/update/localOrder',
1092
+ handler: this.handleUpdateLocalOrder.bind(this)
1093
+ }, {
1094
+ method: 'get',
1095
+ path: '/shop/pay/custom-payment/all',
1096
+ handler: this.handlePaymentMethods.bind(this)
1097
+ }, {
1098
+ method: 'get',
1099
+ path: '/shop/quotation/available',
1100
+ handler: this.handleQuotationAvailable.bind(this)
1101
+ }, {
1102
+ method: 'get',
1103
+ path: '/quotation/available',
1104
+ handler: this.handleQuotationAvailable.bind(this)
1105
+ }, {
1106
+ method: 'post',
1107
+ path: '/shop/order/sales/checkout',
1108
+ handler: this.handleOrderSalesCheckout.bind(this)
1109
+ }, {
1110
+ method: 'post',
1111
+ path: '/order/checkout',
1112
+ handler: this.handleOrderCheckout.bind(this)
1113
+ }, {
1114
+ method: 'post',
1115
+ path: '/shop/local/print-order',
1116
+ handler: this.handleLocalPrintOrder.bind(this)
1117
+ }, {
1118
+ method: 'post',
1119
+ path: '/shop/machinecode/batch',
1120
+ handler: this.handleMachinecodeBatchMeta.bind(this)
1121
+ }, {
1122
+ method: 'post',
1123
+ path: '/machinecode/batch',
1124
+ handler: this.handleMachinecodeBatchMeta.bind(this)
1125
+ }]);
1126
+ this.registerPrefixRoutes([{
1127
+ method: 'get',
1128
+ prefix: '/shop/order/sales',
1129
+ handler: this.handleOrderSalesDetail.bind(this)
1130
+ }, {
1131
+ method: 'get',
1132
+ prefix: '/shop/schedule/floor-plan',
1133
+ handler: this.handleFloorPlanGet.bind(this)
679
1134
  }]);
680
1135
  }
1136
+ async getPaymentRouteModule() {
1137
+ if (this.payment) return this.payment;
1138
+ if (this.paymentRouteModule) return this.paymentRouteModule;
1139
+ if (this.paymentRouteModuleInFlight) return this.paymentRouteModuleInFlight;
1140
+ this.paymentRouteModuleInFlight = (async () => {
1141
+ const module = new _payment.PaymentServerModule('server_payment_route', '1.0.0');
1142
+ await module.initialize(this.core, {
1143
+ store: {
1144
+ payMethods: []
1145
+ }
1146
+ });
1147
+ this.paymentRouteModule = module;
1148
+ return module;
1149
+ })().finally(() => {
1150
+ this.paymentRouteModuleInFlight = undefined;
1151
+ });
1152
+ return this.paymentRouteModuleInFlight;
1153
+ }
1154
+ handlePaymentMethods = async ({
1155
+ data,
1156
+ config
1157
+ }) => {
1158
+ const payment = await this.getPaymentRouteModule();
1159
+ const payMethods = await payment.getPayMethodListAsync({
1160
+ shouldFilter: data?.filterPaymentMethods === true || config?.filterPaymentMethods === true
1161
+ });
1162
+ return {
1163
+ code: 200,
1164
+ status: true,
1165
+ data: payMethods
1166
+ };
1167
+ };
1168
+ handleQuotationAvailable = async ({
1169
+ data
1170
+ }) => {
1171
+ const queryPayload = data && typeof data === 'object' ? {
1172
+ ...data
1173
+ } : {};
1174
+ const cacheKey = this.stableSerialize(queryPayload);
1175
+ if (this.quotationAvailableCache.has(cacheKey)) {
1176
+ this.logInfo('handleQuotationAvailable: 命中运行时缓存', {
1177
+ cacheKey
1178
+ });
1179
+ return this.quotationAvailableCache.get(cacheKey);
1180
+ }
1181
+ const inFlight = this.quotationAvailableInFlight.get(cacheKey);
1182
+ if (inFlight) {
1183
+ this.logInfo('handleQuotationAvailable: 复用进行中的请求', {
1184
+ cacheKey
1185
+ });
1186
+ return inFlight;
1187
+ }
1188
+ const requestPromise = this.fetchQuotationAvailableFromAPI(queryPayload).then(response => {
1189
+ if (this.isSuccessfulRemoteResponse(response)) {
1190
+ this.quotationAvailableCache.set(cacheKey, response);
1191
+ }
1192
+ return response;
1193
+ }).finally(() => {
1194
+ this.quotationAvailableInFlight.delete(cacheKey);
1195
+ });
1196
+ this.quotationAvailableInFlight.set(cacheKey, requestPromise);
1197
+ return requestPromise;
1198
+ };
1199
+ async fetchQuotationAvailableFromAPI(data) {
1200
+ const requester = this.app?.request || this.core.getPlugin('request');
1201
+ if (!requester?.get) {
1202
+ this.logError('fetchQuotationAvailableFromAPI: request 不可用');
1203
+ return {
1204
+ code: 500,
1205
+ message: 'request 不可用',
1206
+ data: {
1207
+ list: []
1208
+ },
1209
+ status: false
1210
+ };
1211
+ }
1212
+ return requester.get('/shop/quotation/available', data);
1213
+ }
1214
+ isSuccessfulRemoteResponse(response) {
1215
+ if (!response) return false;
1216
+ if (response.status === false) return false;
1217
+ if (typeof response.status === 'number' && response.status >= 400) return false;
1218
+ if (response.code !== undefined && response.code !== 200 && response.code !== '200') return false;
1219
+ return true;
1220
+ }
681
1221
 
682
1222
  /**
683
1223
  * 根据 subscriberId 移除商品查询订阅者
684
1224
  */
685
1225
  removeProductQuerySubscriber(subscriberId) {
686
1226
  if (subscriberId) {
1227
+ this.clearSubscriberScheduleTimers(subscriberId);
687
1228
  this.productQuerySubscribers.delete(subscriberId);
688
1229
  this.logInfo('removeProductQuerySubscriber: 已移除订阅者', {
689
1230
  subscriberId,
@@ -692,6 +1233,157 @@ class Server {
692
1233
  }
693
1234
  }
694
1235
 
1236
+ /**
1237
+ * 构建商品 query 响应 data 段,附带智能定价 schedule_time_points 元数据。
1238
+ */
1239
+ buildProductQueryResultPayload(list, count) {
1240
+ return {
1241
+ list,
1242
+ count: count ?? list.length,
1243
+ schedule_time_points: this.products?.getLastScheduleTimePoints?.() ?? []
1244
+ };
1245
+ }
1246
+
1247
+ /**
1248
+ * 是否启用商品 query 订阅的 schedule 时间点定时重算。
1249
+ * strategy_context.enable_schedule_reload === false 时关闭。
1250
+ */
1251
+ shouldScheduleProductQueryReload(strategy_context) {
1252
+ return strategy_context?.enable_schedule_reload !== false;
1253
+ }
1254
+
1255
+ /**
1256
+ * 清除指定 subscriber 的全部 schedule 定时器,防止重复 query 或 unsubscribe 后泄漏。
1257
+ */
1258
+ clearSubscriberScheduleTimers(subscriberId) {
1259
+ const timers = this.productQueryScheduleTimers.get(subscriberId);
1260
+ if (timers?.length) {
1261
+ timers.forEach(timerId => clearTimeout(timerId));
1262
+ }
1263
+ this.productQueryScheduleTimers.delete(subscriberId);
1264
+ const subscriber = this.productQuerySubscribers.get(subscriberId);
1265
+ if (subscriber) {
1266
+ subscriber.scheduleTimerIds = [];
1267
+ }
1268
+ }
1269
+
1270
+ /**
1271
+ * 从 productQueryScheduleTimers 移除已触发的 timer 引用。
1272
+ */
1273
+ removeScheduleTimerRef(subscriberId, timerId) {
1274
+ const timers = this.productQueryScheduleTimers.get(subscriberId);
1275
+ if (!timers?.length) return;
1276
+ const next = timers.filter(id => id !== timerId);
1277
+ if (next.length === 0) {
1278
+ this.productQueryScheduleTimers.delete(subscriberId);
1279
+ } else {
1280
+ this.productQueryScheduleTimers.set(subscriberId, next);
1281
+ }
1282
+ const subscriber = this.productQuerySubscribers.get(subscriberId);
1283
+ if (subscriber) {
1284
+ subscriber.scheduleTimerIds = next;
1285
+ }
1286
+ }
1287
+
1288
+ /**
1289
+ * 计算 schedule 变价时间点到当前时刻的延迟(ms)。已过期返回 -1。
1290
+ */
1291
+ computeScheduleTimePointDelayMs(scheduleDate, timePoint) {
1292
+ const parts = timePoint.split(':');
1293
+ if (parts.length < 2) return -1;
1294
+ const hours = parseInt(parts[0], 10);
1295
+ const minutes = parseInt(parts[1], 10);
1296
+ if (!Number.isFinite(hours) || !Number.isFinite(minutes)) return -1;
1297
+ const target = (0, _dayjs.default)(scheduleDate).hour(hours).minute(minutes).second(4).millisecond(0);
1298
+ const delayMs = target.diff((0, _dayjs.default)());
1299
+ return delayMs >= 0 ? delayMs : -1;
1300
+ }
1301
+
1302
+ /**
1303
+ * 定时重算前刷新 subscriber schedule 上下文:使用执行瞬间时间,禁止沿用首次 query 的 schedule_datetime。
1304
+ */
1305
+ refreshSubscriberScheduleAtExecution(subscriber) {
1306
+ const executedAt = (0, _dayjs.default)();
1307
+ const bookingDate = subscriber.context.schedule_date || executedAt.format('YYYY-MM-DD');
1308
+ subscriber.context.schedule_date = bookingDate;
1309
+ subscriber.context.schedule_datetime = `${bookingDate} ${executedAt.format('HH:mm:ss')}`;
1310
+ }
1311
+
1312
+ /**
1313
+ * 为 subscriber 注册智能定价 scheduleTimePoints 定时重算。
1314
+ * 必须先 clearSubscriberScheduleTimers,再调用本方法。
1315
+ */
1316
+ scheduleSubscriberTimePointReloads(subscriberId, timePoints) {
1317
+ const subscriber = this.productQuerySubscribers.get(subscriberId);
1318
+ if (!subscriber || !timePoints.length) return;
1319
+ const scheduleDate = subscriber.context.schedule_date || (0, _dayjs.default)().format('YYYY-MM-DD');
1320
+ const timerIds = [];
1321
+ for (const timePoint of timePoints) {
1322
+ const delayMs = this.computeScheduleTimePointDelayMs(scheduleDate, timePoint);
1323
+ if (delayMs < 0) continue;
1324
+ const timerId = setTimeout(() => {
1325
+ void this.onScheduleTimePointTimerFire(subscriberId, timerId);
1326
+ }, delayMs);
1327
+ timerIds.push(timerId);
1328
+ }
1329
+ if (timerIds.length > 0) {
1330
+ this.productQueryScheduleTimers.set(subscriberId, timerIds);
1331
+ subscriber.scheduleTimerIds = timerIds;
1332
+ this.logInfo('scheduleSubscriberTimePointReloads: 已注册定时器', {
1333
+ subscriberId,
1334
+ timerCount: timerIds.length,
1335
+ timePoints
1336
+ });
1337
+ }
1338
+ }
1339
+
1340
+ /**
1341
+ * schedule 变价时间点定时器触发:刷新 schedule_datetime 后重算并 callback。
1342
+ */
1343
+ async onScheduleTimePointTimerFire(subscriberId, timerId) {
1344
+ this.removeScheduleTimerRef(subscriberId, timerId);
1345
+ if (!this.productQuerySubscribers.has(subscriberId)) return;
1346
+ const subscriber = this.productQuerySubscribers.get(subscriberId);
1347
+ this.refreshSubscriberScheduleAtExecution(subscriber);
1348
+ await this.recomputeAndNotifySubscriber(subscriberId);
1349
+ }
1350
+
1351
+ /**
1352
+ * 对单个商品 query 订阅者重算并推送(定时路径;不新建 schedule timer)。
1353
+ */
1354
+ async recomputeAndNotifySubscriber(subscriberId) {
1355
+ const subscriber = this.productQuerySubscribers.get(subscriberId);
1356
+ if (!subscriber) return;
1357
+ try {
1358
+ const result = await this.computeProductQueryResult(subscriber.context);
1359
+ subscriber.callback(result);
1360
+ this.logInfo('recomputeAndNotifySubscriber: 已推送', {
1361
+ subscriberId,
1362
+ schedule_datetime: subscriber.context.schedule_datetime
1363
+ });
1364
+ } catch (error) {
1365
+ const errorMessage = error instanceof Error ? error.message : String(error);
1366
+ this.logError('recomputeAndNotifySubscriber: 推送失败', {
1367
+ subscriberId,
1368
+ error: errorMessage
1369
+ });
1370
+ }
1371
+ }
1372
+
1373
+ /**
1374
+ * query 完成后为 subscriber 同步 schedule 定时器(先 clear 再 schedule)。
1375
+ */
1376
+ syncProductQueryScheduleTimers(subscriberId, strategy_context) {
1377
+ this.clearSubscriberScheduleTimers(subscriberId);
1378
+ if (!this.shouldScheduleProductQueryReload(strategy_context)) {
1379
+ return;
1380
+ }
1381
+ const timePoints = this.products?.getLastScheduleTimePoints?.() ?? [];
1382
+ if (timePoints.length > 0) {
1383
+ this.scheduleSubscriberTimePointReloads(subscriberId, timePoints);
1384
+ }
1385
+ }
1386
+
695
1387
  /**
696
1388
  * 处理商品查询请求
697
1389
  * 存储订阅者信息,便于数据变更时推送最新结果
@@ -704,17 +1396,30 @@ class Server {
704
1396
  }) => {
705
1397
  const {
706
1398
  menu_list_ids,
707
- schedule_datetime,
708
- schedule_date
1399
+ customer_id,
1400
+ ids,
1401
+ extension_type,
1402
+ strategy_context
709
1403
  } = data;
710
1404
  const {
711
- callback,
1405
+ schedule_date,
1406
+ schedule_datetime
1407
+ } = this.resolveProductQueryScheduleContext({
1408
+ schedule_date: data.schedule_date,
1409
+ schedule_datetime: data.schedule_datetime
1410
+ });
1411
+ const {
1412
+ callback,
712
1413
  subscriberId
713
1414
  } = config || {};
714
1415
  this.logInfo('handleProductQuery: 开始处理商品查询请求', {
715
1416
  menu_list_ids,
1417
+ ids,
716
1418
  schedule_datetime,
717
- schedule_date
1419
+ schedule_date,
1420
+ customer_id,
1421
+ extension_type,
1422
+ strategyContextKeys: strategy_context ? Object.keys(strategy_context) : []
718
1423
  });
719
1424
 
720
1425
  // 存储订阅者:后续 pubsub 数据变更时可通过 callback 推送最新结果
@@ -723,8 +1428,12 @@ class Server {
723
1428
  callback,
724
1429
  context: {
725
1430
  menu_list_ids,
1431
+ ids,
726
1432
  schedule_date,
727
- schedule_datetime
1433
+ schedule_datetime,
1434
+ customer_id,
1435
+ extension_type,
1436
+ strategy_context
728
1437
  }
729
1438
  });
730
1439
  this.logInfo('handleProductQuery: 已注册订阅者', {
@@ -732,10 +1441,110 @@ class Server {
732
1441
  totalSubscribers: this.productQuerySubscribers.size
733
1442
  });
734
1443
  }
735
- return this.computeProductQueryResult({
1444
+ const result = await this.computeProductQueryResult({
736
1445
  menu_list_ids,
1446
+ ids,
1447
+ schedule_date,
1448
+ schedule_datetime,
1449
+ customer_id,
1450
+ extension_type,
1451
+ strategy_context
1452
+ });
1453
+ if (subscriberId && typeof callback === 'function') {
1454
+ this.syncProductQueryScheduleTimers(subscriberId, strategy_context);
1455
+ }
1456
+ return result;
1457
+ };
1458
+
1459
+ /**
1460
+ * ProductList callers such as standalone retail pages do not always have a selected schedule.
1461
+ * In that case query against the current local time so menu availability can still be evaluated.
1462
+ */
1463
+ resolveProductQueryScheduleContext(params) {
1464
+ const now = (0, _dayjs.default)();
1465
+ const schedule_datetime = typeof params.schedule_datetime === 'string' && params.schedule_datetime.trim() ? params.schedule_datetime : now.format('YYYY-MM-DD HH:mm:ss');
1466
+ const schedule_date = typeof params.schedule_date === 'string' && params.schedule_date.trim() ? params.schedule_date : (0, _dayjs.default)(schedule_datetime).isValid() ? (0, _dayjs.default)(schedule_datetime).format('YYYY-MM-DD') : now.format('YYYY-MM-DD');
1467
+ return {
737
1468
  schedule_date,
738
1469
  schedule_datetime
1470
+ };
1471
+ }
1472
+
1473
+ /**
1474
+ * OpenData 的关联餐牌可能已更新,而 server_menu/server_schedule 仍命中本地持久化缓存。
1475
+ * 仅在当前筛选没有任何生效餐牌时刷新一次,并由调用方重新计算筛选结果。
1476
+ */
1477
+ async refreshMenuScheduleCacheAfterEmptyResult() {
1478
+ if (!this.menu || !this.schedule) return false;
1479
+ const now = Date.now();
1480
+ if (now - this.lastMenuScheduleRefreshAt < this.MENU_SCHEDULE_REFRESH_COOLDOWN_MS) {
1481
+ return false;
1482
+ }
1483
+ if (!this.menuScheduleRefreshInFlight) {
1484
+ this.lastMenuScheduleRefreshAt = now;
1485
+ this.menuScheduleRefreshInFlight = Promise.all([this.menu.loadMenuList(), this.schedule.loadAllSchedule()]).then(() => undefined).catch(error => {
1486
+ this.logWarning('刷新菜单/日程缓存失败,继续使用现有缓存', {
1487
+ error: error instanceof Error ? error.message : String(error)
1488
+ });
1489
+ }).finally(() => {
1490
+ this.menuScheduleRefreshInFlight = null;
1491
+ });
1492
+ }
1493
+ await this.menuScheduleRefreshInFlight;
1494
+ return true;
1495
+ }
1496
+
1497
+ /**
1498
+ * 按商品 id 查询单条(GET /shop/product/query/:productId)
1499
+ * schedule_date 必填:query 或 data;schedule_datetime 可选。不走订阅。
1500
+ */
1501
+ handleProductQueryById = async ({
1502
+ path,
1503
+ method,
1504
+ data,
1505
+ url
1506
+ }) => {
1507
+ const pathname = this.parseRequestPath(url || '', path);
1508
+ const idMatch = pathname.match(/^\/shop\/product\/query\/(\d+)$/);
1509
+ const productId = idMatch ? Number(idMatch[1]) : NaN;
1510
+ if (!Number.isFinite(productId)) {
1511
+ this.logWarning('handleProductQueryById: 无效的商品 id', {
1512
+ path: pathname
1513
+ });
1514
+ return {
1515
+ code: 400,
1516
+ message: '无效的商品路径',
1517
+ status: false,
1518
+ data: {
1519
+ item: null
1520
+ }
1521
+ };
1522
+ }
1523
+ const scheduleDateFromData = data && typeof data === 'object' && typeof data.schedule_date === 'string' ? data.schedule_date.trim() : '';
1524
+ const schedule_date = scheduleDateFromData || (this.extractQueryParam(url || '', 'schedule_date') || '').trim() || (0, _dayjs.default)().format('YYYY-MM-DD');
1525
+ if (!schedule_date) {
1526
+ this.logWarning('handleProductQueryById: 缺少 schedule_date', {
1527
+ productId
1528
+ });
1529
+ return {
1530
+ code: 400,
1531
+ message: 'schedule_date 为必填(query 或 data)',
1532
+ status: false,
1533
+ data: null
1534
+ };
1535
+ }
1536
+ const scheduleDtFromData = data && typeof data === 'object' && typeof data.schedule_datetime === 'string' ? data.schedule_datetime.trim() : '';
1537
+ const scheduleDtFromUrl = (this.extractQueryParam(url || '', 'schedule_datetime') || '').trim();
1538
+ const scheduleDatetime = scheduleDtFromData || scheduleDtFromUrl || `${schedule_date}T00:00:00`;
1539
+ this.logInfo('handleProductQueryById: 开始', {
1540
+ productId,
1541
+ schedule_date,
1542
+ schedule_datetime: scheduleDatetime
1543
+ });
1544
+ return this.computeProductQueryResult({
1545
+ schedule_date,
1546
+ schedule_datetime: scheduleDatetime,
1547
+ product_id: productId
739
1548
  });
740
1549
  };
741
1550
 
@@ -773,20 +1582,32 @@ class Server {
773
1582
  callback,
774
1583
  subscriberId
775
1584
  } = config || {};
1585
+ const useLocal = this.shouldUseLocalOrderQuery(queryPayload);
776
1586
  this.logInfo('handleOrderList: 开始处理订单列表请求', {
777
- data: queryPayload
1587
+ data: queryPayload,
1588
+ useLocal
778
1589
  });
779
- if (subscriberId && typeof callback === 'function') {
780
- this.orderQuerySubscribers.set(subscriberId, {
781
- callback,
782
- context: queryPayload
783
- });
784
- this.logInfo('handleOrderList: 已注册订阅者', {
1590
+ if (useLocal) {
1591
+ if (subscriberId && typeof callback === 'function') {
1592
+ this.orderQuerySubscribers.set(subscriberId, {
1593
+ callback,
1594
+ context: queryPayload
1595
+ });
1596
+ this.logInfo('handleOrderList: 已注册订阅者', {
1597
+ subscriberId,
1598
+ totalSubscribers: this.orderQuerySubscribers.size
1599
+ });
1600
+ }
1601
+ return this.computeOrderQueryResult(queryPayload);
1602
+ }
1603
+ if (subscriberId) {
1604
+ this.orderQuerySubscribers.delete(subscriberId);
1605
+ this.logInfo('handleOrderList: 已清理订阅者(走远端)', {
785
1606
  subscriberId,
786
- totalSubscribers: this.orderQuerySubscribers.size
1607
+ remaining: this.orderQuerySubscribers.size
787
1608
  });
788
1609
  }
789
- return this.computeOrderQueryResult(queryPayload);
1610
+ return this.fetchOrderListFromAPI(queryPayload);
790
1611
  };
791
1612
 
792
1613
  /**
@@ -813,26 +1634,116 @@ class Server {
813
1634
  };
814
1635
 
815
1636
  /**
816
- * 判断预约查询的 sales_time_between 起始日期是否为今天
1637
+ * 处理 sales 单模块搜索:请求透传后端,仅在 OS 层维护已返回订单号集合。
1638
+ *
1639
+ * @example
1640
+ * await this.handleSalesSearchModule({ url: '/shop/es/search/sales', method: 'post', data: { keyword: 'A' } })
817
1641
  */
818
- isBookingQueryForToday(data) {
819
- const range = data?.sales_time_between;
820
- if (!Array.isArray(range) || range.length < 1) return true;
821
- const startDateStr = String(range[0]).split('T')[0].split(' ')[0];
822
- const todayStr = (0, _dayjs.default)().format('YYYY-MM-DD');
823
- return startDateStr === todayStr;
1642
+ handleSalesSearchModule = async ({
1643
+ data,
1644
+ config
1645
+ }) => {
1646
+ return this.handleSalesSearchRequest('single', '/shop/es/search/sales', data, config);
1647
+ };
1648
+
1649
+ /**
1650
+ * 处理 sales 聚合搜索:请求透传后端,仅订阅 sales 分组的已返回订单号。
1651
+ *
1652
+ * @example
1653
+ * await this.handleSalesSearchAggregate({ url: '/shop/es/search', method: 'post', data: { modules: [] } })
1654
+ */
1655
+ handleSalesSearchAggregate = async ({
1656
+ data,
1657
+ config
1658
+ }) => {
1659
+ return this.handleSalesSearchRequest('aggregate', '/shop/es/search', data, config);
1660
+ };
1661
+
1662
+ /**
1663
+ * 取消 sales 搜索订阅。
1664
+ *
1665
+ * @example
1666
+ * await this.handleUnsubscribeSalesSearch({ data: { subscriberId: 'global-search-order' } } as any)
1667
+ */
1668
+ handleUnsubscribeSalesSearch = async ({
1669
+ data
1670
+ }) => {
1671
+ const {
1672
+ subscriberId
1673
+ } = data || {};
1674
+ if (subscriberId) {
1675
+ this.salesSearchSubscribers.delete(subscriberId);
1676
+ this.logInfo('handleUnsubscribeSalesSearch: 已移除订阅者', {
1677
+ subscriberId,
1678
+ remaining: this.salesSearchSubscribers.size
1679
+ });
1680
+ }
1681
+ return {
1682
+ code: 200,
1683
+ message: 'ok',
1684
+ status: true
1685
+ };
1686
+ };
1687
+
1688
+ /**
1689
+ * 执行 sales 搜索并登记可见订单号。
1690
+ * 搜索参数完全透传后端,OS 不做过滤、筛选、排序。
1691
+ *
1692
+ * @example
1693
+ * const result = await this.handleSalesSearchRequest('single', '/shop/es/search/sales', { keyword: '1001' }, {})
1694
+ */
1695
+ async handleSalesSearchRequest(kind, backendPath, data, config) {
1696
+ const queryPayload = data && typeof data === 'object' ? {
1697
+ ...data
1698
+ } : {};
1699
+ const result = await this.fetchSalesSearchFromAPI(backendPath, queryPayload);
1700
+ const {
1701
+ callback,
1702
+ subscriberId,
1703
+ resetVisibleOrderIds
1704
+ } = config || {};
1705
+ if (subscriberId && typeof callback === 'function') {
1706
+ const contextSignature = this.buildSalesSearchSignature(kind, queryPayload);
1707
+ const existing = this.salesSearchSubscribers.get(subscriberId);
1708
+ const shouldReset = resetVisibleOrderIds === true || !existing || existing.kind !== kind || existing.contextSignature !== contextSignature;
1709
+ const visibleOrderIds = shouldReset ? new Set() : new Set(existing.visibleOrderIds);
1710
+ for (const orderId of this.extractSalesSearchOrderIds(kind, result)) {
1711
+ visibleOrderIds.add(orderId);
1712
+ }
1713
+ this.salesSearchSubscribers.set(subscriberId, {
1714
+ callback,
1715
+ kind,
1716
+ contextSignature,
1717
+ visibleOrderIds,
1718
+ lastResponseData: this.extractSalesSearchResponseData(result)
1719
+ });
1720
+ this.logInfo('handleSalesSearchRequest: 已注册/更新订阅者', {
1721
+ subscriberId,
1722
+ kind,
1723
+ shouldReset,
1724
+ visibleCount: visibleOrderIds.size
1725
+ });
1726
+ }
1727
+ return result;
824
1728
  }
825
1729
 
826
1730
  /**
827
- * 非今天的预约查询:通过真实 API 获取数据,再做 flattenOrdersToBookings 拆分
1731
+ * 透传调用后端 sales 搜索接口。
1732
+ *
1733
+ * @example
1734
+ * const res = await this.fetchSalesSearchFromAPI('/shop/es/search/sales', { keyword: '1001' })
828
1735
  */
829
- async fetchBookingListFromAPI(data) {
1736
+ async fetchSalesSearchFromAPI(backendPath, data) {
830
1737
  if (!this.app?.request) {
831
- this.logError('fetchBookingListFromAPI: app.request 不可用');
1738
+ this.logError('fetchSalesSearchFromAPI: app.request 不可用', {
1739
+ backendPath
1740
+ });
832
1741
  return {
833
1742
  code: 500,
834
1743
  message: 'app.request 不可用',
835
- data: {
1744
+ data: backendPath === '/shop/es/search' ? {
1745
+ results: []
1746
+ } : {
836
1747
  list: [],
837
1748
  count: 0
838
1749
  },
@@ -840,115 +1751,2625 @@ class Server {
840
1751
  };
841
1752
  }
842
1753
  try {
843
- const response = await this.app.request.get('/shop/order/sales', {
844
- ...data,
845
- form_record_ids: undefined,
846
- with: ["all"]
847
- }, {
1754
+ const response = await this.app.request.post(backendPath, data, {
848
1755
  isShopApi: true
849
1756
  });
850
- const rawList = response?.data?.list ?? response?.list ?? [];
851
- const list = (0, _filterBookings.filterBookingsFromOrders)(rawList, data);
852
- this.logInfo('fetchBookingListFromAPI: API 返回并拆分完成', {
853
- rawCount: rawList.length,
854
- flattenedCount: list.count
1757
+ const payload = response?.data ?? response;
1758
+ return {
1759
+ code: response?.code ?? 200,
1760
+ status: response?.status ?? true,
1761
+ message: response?.message ?? '',
1762
+ data: payload
1763
+ };
1764
+ } catch (error) {
1765
+ const errorMessage = error instanceof Error ? error.message : String(error);
1766
+ this.logError('fetchSalesSearchFromAPI: 请求失败', {
1767
+ backendPath,
1768
+ error: errorMessage
1769
+ });
1770
+ return {
1771
+ code: 500,
1772
+ message: errorMessage,
1773
+ data: backendPath === '/shop/es/search' ? {
1774
+ results: []
1775
+ } : {
1776
+ list: [],
1777
+ count: 0
1778
+ },
1779
+ status: false
1780
+ };
1781
+ }
1782
+ }
1783
+
1784
+ /**
1785
+ * 构建搜索订阅签名;页码变化不重置,可继续累计 visibleOrderIds。
1786
+ *
1787
+ * @example
1788
+ * const signature = this.buildSalesSearchSignature('single', { keyword: 'A', page: 2 })
1789
+ */
1790
+ buildSalesSearchSignature(kind, payload) {
1791
+ return this.stableSerialize({
1792
+ kind,
1793
+ payload: this.stripSalesSearchPageParams(payload)
1794
+ });
1795
+ }
1796
+
1797
+ /**
1798
+ * 移除页码参数,保留 page size、keyword、filters、sort 等查询语义参数。
1799
+ *
1800
+ * @example
1801
+ * this.stripSalesSearchPageParams({ page: 2, per_page: 20, keyword: 'A' })
1802
+ */
1803
+ stripSalesSearchPageParams(value) {
1804
+ if (Array.isArray(value)) {
1805
+ return value.map(item => this.stripSalesSearchPageParams(item));
1806
+ }
1807
+ if (!value || typeof value !== 'object') return value;
1808
+ const next = {};
1809
+ for (const [key, item] of Object.entries(value)) {
1810
+ if (key === 'page' || key === 'skip') continue;
1811
+ next[key] = this.stripSalesSearchPageParams(item);
1812
+ }
1813
+ return next;
1814
+ }
1815
+
1816
+ /**
1817
+ * 提取响应 data,兼容 OS 包装结构与后端直接响应结构。
1818
+ *
1819
+ * @example
1820
+ * const data = this.extractSalesSearchResponseData(result)
1821
+ */
1822
+ extractSalesSearchResponseData(result) {
1823
+ return result?.data ?? result;
1824
+ }
1825
+
1826
+ /**
1827
+ * 从搜索响应中提取 sales 命中的订单号。
1828
+ *
1829
+ * @example
1830
+ * const ids = this.extractSalesSearchOrderIds('single', result)
1831
+ */
1832
+ extractSalesSearchOrderIds(kind, result) {
1833
+ const data = this.extractSalesSearchResponseData(result);
1834
+ const list = kind === 'aggregate' ? this.findSalesSearchAggregateGroup(data)?.list : data?.list;
1835
+ if (!Array.isArray(list)) return [];
1836
+ return list.map(item => this.getSalesSearchOrderId(item)).filter(id => !!id);
1837
+ }
1838
+
1839
+ /**
1840
+ * 查找聚合搜索响应中的 sales 分组。
1841
+ *
1842
+ * @example
1843
+ * const group = this.findSalesSearchAggregateGroup({ results: [{ module: 'sales' }] })
1844
+ */
1845
+ findSalesSearchAggregateGroup(data) {
1846
+ const results = Array.isArray(data?.results) ? data.results : [];
1847
+ return results.find(item => item?.module === 'sales') || null;
1848
+ }
1849
+
1850
+ /**
1851
+ * 从订单或搜索命中项中取订单号。
1852
+ *
1853
+ * @example
1854
+ * const id = this.getSalesSearchOrderId({ order_id: 1001 })
1855
+ */
1856
+ getSalesSearchOrderId(order) {
1857
+ if (!order || typeof order !== 'object') return null;
1858
+ const raw = order.order_id ?? order.id;
1859
+ if (raw === undefined || raw === null || raw === '') return null;
1860
+ return String(raw);
1861
+ }
1862
+
1863
+ /**
1864
+ * 从查询值中提取 YYYY-MM-DD 日期串。
1865
+ *
1866
+ * @example
1867
+ * this.extractQueryDate('2026-04-15 09:58:00') // => '2026-04-15'
1868
+ */
1869
+ extractQueryDate(raw) {
1870
+ if (raw == null) return null;
1871
+ const date = (0, _dayjs.default)(raw);
1872
+ if (date.isValid()) {
1873
+ return date.format('YYYY-MM-DD');
1874
+ }
1875
+ const text = String(raw).trim();
1876
+ if (!text) return null;
1877
+ return text.split('T')[0].split(' ')[0] || null;
1878
+ }
1879
+
1880
+ /**
1881
+ * 判断时间范围是否完整落在今天。
1882
+ *
1883
+ * @example
1884
+ * this.isRangeFullyToday('2026-04-15', '2026-04-15') // => true
1885
+ */
1886
+ isRangeFullyToday(startRaw, endRaw) {
1887
+ const startDate = this.extractQueryDate(startRaw);
1888
+ const endDate = this.extractQueryDate(endRaw);
1889
+ if (!startDate || !endDate) return false;
1890
+ const today = (0, _dayjs.default)().format('YYYY-MM-DD');
1891
+ return startDate === today && endDate === today;
1892
+ }
1893
+
1894
+ /**
1895
+ * 判断时间范围是否「开始和结束都不是今天」。
1896
+ *
1897
+ * @example
1898
+ * this.isRangeBothNotToday('2026-04-13', '2026-04-14') // => true
1899
+ */
1900
+ isRangeBothNotToday(startRaw, endRaw) {
1901
+ const startDate = this.extractQueryDate(startRaw);
1902
+ const endDate = this.extractQueryDate(endRaw);
1903
+ if (!startDate || !endDate) return false;
1904
+ const today = (0, _dayjs.default)().format('YYYY-MM-DD');
1905
+ return startDate !== today && endDate !== today;
1906
+ }
1907
+
1908
+ /**
1909
+ * 订单查询是否走本地:
1910
+ * - 仅当 start_time 与 end_time 都是今天时,走本地;
1911
+ * - 任意一端不是今天时,走云端,确保跨天查询能拉取非本地窗口数据。
1912
+ */
1913
+ shouldUseLocalOrderQuery(data) {
1914
+ return this.isRangeFullyToday(data?.start_time, data?.end_time);
1915
+ }
1916
+
1917
+ /**
1918
+ * 预约查询是否走本地:
1919
+ * - 当 sales_time_between 的开始和结束都不是今天时,走远端;
1920
+ * - 其余情况走本地。
1921
+ */
1922
+ shouldUseLocalBookingQuery(data) {
1923
+ const range = data?.sales_time_between;
1924
+ if (!Array.isArray(range) || range.length < 2) return true;
1925
+ return !this.isRangeBothNotToday(range[0], range[1]);
1926
+ }
1927
+
1928
+ /**
1929
+ * 日历 SSE 精简 with:由调用方显式声明 compact_fields 决定
1930
+ * - compact_fields === true → 精简字段(周/月视图)
1931
+ * - 其余(false / 未传)→ ['all'](日视图/表格/平面图合并)
1932
+ */
1933
+ resolveBookingSalesWith(queryPayload) {
1934
+ if (queryPayload?.compact_fields === true) {
1935
+ return ['bookings:booking_id,start_date,start_time,end_date,end_time,holder,metadata,parent_id,item_type', 'customer:id,display_name,phone', 'products'
1936
+ // 'resources'
1937
+ ];
1938
+ }
1939
+ return ['all'];
1940
+ }
1941
+
1942
+ /**
1943
+ * 日历 SSE 精简 select:渲染 + filterBookingsFromOrders 所需的主表字段
1944
+ * business_code / payment_status 不直接用于渲染,但 matchOrder 过滤依赖
1945
+ */
1946
+ resolveBookingSalesSelect() {
1947
+ return 'status,payment_status,business_code,phone,customer_name,schedule_date,created_at,metadata';
1948
+ }
1949
+
1950
+ /**
1951
+ * 日历(compact_fields)场景下要直接在 SSE 层排除的订单类型:
1952
+ * 这些类型不会出现在日历上,拉取它们只会浪费带宽并增加前端过滤成本。
1953
+ */
1954
+ BOOKING_SALES_COMPACT_EXCLUDE_TYPES = ['virtual', 'shipping', 'delivery', 'pickup'];
1955
+ normalizeBookingRemoteQueryPayload(data, withFields) {
1956
+ const isFullWith = withFields.length === 1 && withFields[0] === 'all';
1957
+ const isCompact = data?.compact_fields === true;
1958
+ return {
1959
+ ...data,
1960
+ form_record_ids: undefined,
1961
+ enable_remote_memory_cache: undefined,
1962
+ compact_fields: undefined,
1963
+ with: withFields,
1964
+ ...(isFullWith ? {} : {
1965
+ select: this.resolveBookingSalesSelect()
1966
+ }),
1967
+ ...(isCompact ? {
1968
+ exclude_types: this.BOOKING_SALES_COMPACT_EXCLUDE_TYPES
1969
+ } : {}),
1970
+ chunk_size: 50
1971
+ };
1972
+ }
1973
+
1974
+ /**
1975
+ * 远端预约查询内存缓存开关:默认 false,仅显式 true 才启用。
1976
+ */
1977
+ isBookingRemoteMemoryCacheEnabled(queryPayload) {
1978
+ return queryPayload?.enable_remote_memory_cache === true;
1979
+ }
1980
+ stableSerialize(value) {
1981
+ if (value === null || value === undefined) return 'null';
1982
+ if (Array.isArray(value)) {
1983
+ return `[${value.map(item => this.stableSerialize(item)).join(',')}]`;
1984
+ }
1985
+ if (typeof value === 'object') {
1986
+ const obj = value;
1987
+ const keys = Object.keys(obj).filter(k => obj[k] !== undefined).sort();
1988
+ return `{${keys.map(k => `${JSON.stringify(k)}:${this.stableSerialize(obj[k])}`).join(',')}}`;
1989
+ }
1990
+ return JSON.stringify(value);
1991
+ }
1992
+ buildBookingRemoteCacheKey(payload) {
1993
+ return this.stableSerialize(payload);
1994
+ }
1995
+ parseBookingSalesRangeMeta(payload) {
1996
+ const range = payload?.sales_time_between;
1997
+ if (!Array.isArray(range) || range.length < 2) return null;
1998
+ const startAtMs = (0, _dayjs.default)(range[0]).valueOf();
1999
+ const endAtMs = (0, _dayjs.default)(range[1]).valueOf();
2000
+ if (!Number.isFinite(startAtMs) || !Number.isFinite(endAtMs)) return null;
2001
+ if (endAtMs <= startAtMs) return null;
2002
+ return {
2003
+ startAtMs,
2004
+ endAtMs
2005
+ };
2006
+ }
2007
+ readBookingRemoteCache(cacheKey) {
2008
+ const entry = this.bookingRemoteCache.get(cacheKey);
2009
+ if (!entry) return null;
2010
+ if (entry.stale) {
2011
+ this.bookingRemoteCache.delete(cacheKey);
2012
+ return null;
2013
+ }
2014
+ const now = Date.now();
2015
+ if (now - entry.updatedAt > this.BOOKING_REMOTE_CACHE_TTL_MS) {
2016
+ this.bookingRemoteCache.delete(cacheKey);
2017
+ return null;
2018
+ }
2019
+ return entry;
2020
+ }
2021
+ writeBookingRemoteCache(entry) {
2022
+ this.bookingRemoteCache.set(entry.key, entry);
2023
+ if (this.bookingRemoteCache.size <= this.BOOKING_REMOTE_CACHE_MAX_ENTRIES) {
2024
+ return;
2025
+ }
2026
+ let oldestKey = null;
2027
+ let oldestTime = Number.POSITIVE_INFINITY;
2028
+ for (const [key, cache] of this.bookingRemoteCache.entries()) {
2029
+ if (cache.updatedAt < oldestTime) {
2030
+ oldestTime = cache.updatedAt;
2031
+ oldestKey = key;
2032
+ }
2033
+ }
2034
+ if (oldestKey) this.bookingRemoteCache.delete(oldestKey);
2035
+ }
2036
+ toOrderIdKey(order) {
2037
+ if (!order || typeof order !== 'object') return null;
2038
+ const raw = order.order_id;
2039
+ if (raw === undefined || raw === null || raw === '') return null;
2040
+ return String(raw);
2041
+ }
2042
+ markBookingRemoteCacheAllStale(reason) {
2043
+ if (this.bookingRemoteCache.size === 0) return;
2044
+ for (const entry of this.bookingRemoteCache.values()) {
2045
+ entry.stale = true;
2046
+ }
2047
+ this.logWarning('bookingRemoteCache: 全量标记失效', {
2048
+ reason,
2049
+ cacheSize: this.bookingRemoteCache.size
2050
+ });
2051
+ }
2052
+ buildBookingResponse(bookingResult, extra) {
2053
+ return {
2054
+ code: 200,
2055
+ data: {
2056
+ ...(extra ?? {}),
2057
+ list: bookingResult.list || [],
2058
+ count: bookingResult.count || 0,
2059
+ size: bookingResult.size || 0,
2060
+ skip: bookingResult.skip || 0
2061
+ },
2062
+ message: '',
2063
+ status: true
2064
+ };
2065
+ }
2066
+ notifyBookingRemoteSubscribersByCacheKey(cacheKey, entry, reason) {
2067
+ if (this.bookingRemoteQuerySubscribers.size === 0) return;
2068
+ for (const [subscriberId, subscriber] of this.bookingRemoteQuerySubscribers.entries()) {
2069
+ if (subscriber.cacheKey !== cacheKey) continue;
2070
+ try {
2071
+ const callbackStartAt = Date.now();
2072
+ subscriber.callback(this.buildBookingResponse(entry.bookingResult, {
2073
+ cache_hit: true,
2074
+ from_cache_sync: true,
2075
+ sync_reason: reason
2076
+ }));
2077
+ const callbackEndAt = Date.now();
2078
+ this.logInfo('notifyBookingRemoteSubscribersByCacheKey: 已推送', {
2079
+ subscriberId,
2080
+ cacheKey,
2081
+ reason,
2082
+ callbackDurationMs: callbackEndAt - callbackStartAt
2083
+ });
2084
+ } catch (error) {
2085
+ const errMsg = error instanceof Error ? error.message : String(error);
2086
+ this.logError('notifyBookingRemoteSubscribersByCacheKey: 推送失败', {
2087
+ subscriberId,
2088
+ cacheKey,
2089
+ reason,
2090
+ error: errMsg
2091
+ });
2092
+ }
2093
+ }
2094
+ }
2095
+ syncBookingRemoteCacheFromOrderChanges() {
2096
+ if (this.bookingRemoteCache.size === 0) return;
2097
+ const syncStartAt = Date.now();
2098
+ const latestOrders = this.order?.getOrders?.() ?? [];
2099
+ if (!Array.isArray(latestOrders) || latestOrders.length === 0) {
2100
+ this.markBookingRemoteCacheAllStale('orderStoreEmptyAfterChange');
2101
+ return;
2102
+ }
2103
+ let syncedEntryCount = 0;
2104
+ for (const [cacheKey, entry] of this.bookingRemoteCache.entries()) {
2105
+ if (entry.stale) {
2106
+ this.bookingRemoteCache.delete(cacheKey);
2107
+ continue;
2108
+ }
2109
+ try {
2110
+ const queryPayload = entry.queryPayload;
2111
+ const orderMap = new Map();
2112
+ for (const order of entry.rawOrders) {
2113
+ const key = this.toOrderIdKey(order);
2114
+ if (key) orderMap.set(key, order);
2115
+ }
2116
+ let changed = false;
2117
+ for (const latest of latestOrders) {
2118
+ const key = this.toOrderIdKey(latest);
2119
+ if (!key) continue;
2120
+ if (orderMap.has(key)) {
2121
+ orderMap.set(key, latest);
2122
+ changed = true;
2123
+ continue;
2124
+ }
2125
+ orderMap.set(key, latest);
2126
+ changed = true;
2127
+ }
2128
+ if (!changed) continue;
2129
+ const mergedOrders = [...orderMap.values()];
2130
+ const filterStartAt = Date.now();
2131
+ const bookingResult = (0, _filterBookings.sortBookings)((0, _filterBookings.filterBookingsFromOrders)(mergedOrders, queryPayload), queryPayload);
2132
+ const filterEndAt = Date.now();
2133
+ const nextEntry = {
2134
+ ...entry,
2135
+ rawOrders: mergedOrders,
2136
+ bookingResult,
2137
+ updatedAt: Date.now(),
2138
+ stale: false
2139
+ };
2140
+ this.writeBookingRemoteCache(nextEntry);
2141
+ this.notifyBookingRemoteSubscribersByCacheKey(cacheKey, nextEntry, 'orders_changed');
2142
+ syncedEntryCount += 1;
2143
+ this.logInfo('bookingRemoteCache: 单条缓存同步完成', {
2144
+ cacheKey,
2145
+ mergedOrderCount: mergedOrders.length,
2146
+ filteredBookingCount: bookingResult.count,
2147
+ filterDurationMs: filterEndAt - filterStartAt
2148
+ });
2149
+ } catch (error) {
2150
+ const errMsg = error instanceof Error ? error.message : String(error);
2151
+ this.logWarning('bookingRemoteCache: 同步失败并标记失效', {
2152
+ cacheKey,
2153
+ error: errMsg
2154
+ });
2155
+ const current = this.bookingRemoteCache.get(cacheKey);
2156
+ if (current) current.stale = true;
2157
+ }
2158
+ }
2159
+ const syncEndAt = Date.now();
2160
+ if (syncedEntryCount > 0) {
2161
+ this.logInfo('bookingRemoteCache: 已同步 Ably 变更', {
2162
+ syncedEntryCount,
2163
+ cacheSize: this.bookingRemoteCache.size,
2164
+ totalSyncDurationMs: syncEndAt - syncStartAt
2165
+ });
2166
+ }
2167
+ }
2168
+
2169
+ /**
2170
+ * 非本地窗口的订单查询:走真实 API。
2171
+ */
2172
+ async fetchOrderListFromAPI(data) {
2173
+ if (!this.app?.request) {
2174
+ this.logError('fetchOrderListFromAPI: app.request 不可用');
2175
+ return {
2176
+ code: 500,
2177
+ message: 'app.request 不可用',
2178
+ data: {
2179
+ list: [],
2180
+ count: 0
2181
+ },
2182
+ status: false
2183
+ };
2184
+ }
2185
+ try {
2186
+ const response = await this.app.request.post('/shop/order/v2/list', data, {
2187
+ isShopApi: true
2188
+ });
2189
+ const payload = response?.data ?? response;
2190
+ const list = Array.isArray(payload?.list) ? payload.list : [];
2191
+ const count = typeof payload?.count === 'number' ? payload.count : list.length;
2192
+ return {
2193
+ code: 200,
2194
+ data: {
2195
+ ...payload,
2196
+ list,
2197
+ count
2198
+ },
2199
+ message: '',
2200
+ status: true
2201
+ };
2202
+ } catch (error) {
2203
+ const errorMessage = error instanceof Error ? error.message : String(error);
2204
+ this.logError('fetchOrderListFromAPI: 请求失败', {
2205
+ error: errorMessage
2206
+ });
2207
+ return {
2208
+ code: 500,
2209
+ message: errorMessage,
2210
+ data: {
2211
+ list: [],
2212
+ count: 0
2213
+ },
2214
+ status: false
2215
+ };
2216
+ }
2217
+ }
2218
+
2219
+ /**
2220
+ * 非今天的预约查询:通过 SSE 流式拉取订单,再做本地 booking 筛选展开
2221
+ */
2222
+ async fetchBookingListFromAPI(data) {
2223
+ if (!this.order) {
2224
+ this.logError('fetchBookingListFromAPI: Order 模块不可用');
2225
+ return {
2226
+ code: 500,
2227
+ message: 'Order 模块不可用',
2228
+ data: {
2229
+ list: [],
2230
+ count: 0
2231
+ },
2232
+ status: false
2233
+ };
2234
+ }
2235
+ try {
2236
+ const memoryCacheEnabled = this.isBookingRemoteMemoryCacheEnabled(data);
2237
+ const withFields = this.resolveBookingSalesWith(data);
2238
+ const requestPayload = this.normalizeBookingRemoteQueryPayload(data, withFields);
2239
+ const cacheKey = this.buildBookingRemoteCacheKey(requestPayload);
2240
+ if (memoryCacheEnabled) {
2241
+ const cached = this.readBookingRemoteCache(cacheKey);
2242
+ if (cached) {
2243
+ this.logInfo('fetchBookingListFromAPI: 命中内存缓存', {
2244
+ cacheKey,
2245
+ listCount: cached.bookingResult.count,
2246
+ withFields: cached.withFields
2247
+ });
2248
+ return this.buildBookingResponse(cached.bookingResult, {
2249
+ cache_hit: true
2250
+ });
2251
+ }
2252
+ }
2253
+ const rawList = await this.order.fetchOrdersBySSE(requestPayload);
2254
+ const bookingResult = (0, _filterBookings.sortBookings)((0, _filterBookings.filterBookingsFromOrders)(rawList, data), data);
2255
+
2256
+ // DEBUG: 挂到 window 上方便排查 SSE → 日历数据丢失问题
2257
+ if (typeof globalThis !== 'undefined') {
2258
+ globalThis.__SSE_BOOKING_DEBUG__ = {
2259
+ ts: new Date().toISOString(),
2260
+ sseRequestPayload: requestPayload,
2261
+ calendarFilters: data,
2262
+ rawOrderCount: rawList.length,
2263
+ rawOrderSample: rawList.slice(0, 2).map(o => ({
2264
+ order_id: o?.order_id,
2265
+ business_code: o?.business_code,
2266
+ status: o?.status,
2267
+ payment_status: o?.payment_status,
2268
+ bookingsCount: o?.bookings?.length,
2269
+ firstBooking: o?.bookings?.[0] ? {
2270
+ start_date: o.bookings[0].start_date,
2271
+ start_time: o.bookings[0].start_time,
2272
+ parent_id: o.bookings[0].parent_id,
2273
+ item_type: o.bookings[0].item_type,
2274
+ resourcesCount: o.bookings[0].resources?.length
2275
+ } : null
2276
+ })),
2277
+ filteredBookingCount: bookingResult.count,
2278
+ filteredListLength: bookingResult.list?.length
2279
+ };
2280
+ }
2281
+ if (memoryCacheEnabled) {
2282
+ this.writeBookingRemoteCache({
2283
+ key: cacheKey,
2284
+ queryPayload: data,
2285
+ withFields,
2286
+ rawOrders: Array.isArray(rawList) ? rawList : [],
2287
+ bookingResult,
2288
+ updatedAt: Date.now(),
2289
+ stale: false,
2290
+ rangeMeta: this.parseBookingSalesRangeMeta(requestPayload)
2291
+ });
2292
+ }
2293
+ this.logInfo('fetchBookingListFromAPI: SSE 返回并拆分完成', {
2294
+ rawCount: rawList.length,
2295
+ flattenedCount: bookingResult.count,
2296
+ cacheKey,
2297
+ withFields
2298
+ });
2299
+ return this.buildBookingResponse(bookingResult);
2300
+ } catch (error) {
2301
+ const errorMessage = error instanceof Error ? error.message : String(error);
2302
+ this.logError('fetchBookingListFromAPI: SSE 请求失败', {
2303
+ error: errorMessage
2304
+ });
2305
+ return {
2306
+ code: 500,
2307
+ message: errorMessage,
2308
+ data: {
2309
+ list: [],
2310
+ count: 0
2311
+ },
2312
+ status: false
2313
+ };
2314
+ }
2315
+ }
2316
+
2317
+ /**
2318
+ * 处理预约列表查询
2319
+ * 今天:注册订阅者 + 本地数据筛选;非今天:清理订阅者 + 走真实 API
2320
+ */
2321
+ handleBookingList = async ({
2322
+ url,
2323
+ method,
2324
+ data,
2325
+ config
2326
+ }) => {
2327
+ const queryPayload = data && typeof data === 'object' ? {
2328
+ ...data
2329
+ } : {};
2330
+ const {
2331
+ callback,
2332
+ subscriberId
2333
+ } = config || {};
2334
+ const useLocal = this.shouldUseLocalBookingQuery(queryPayload);
2335
+ this.logInfo('handleBookingList: 开始处理预约列表请求', {
2336
+ data: queryPayload,
2337
+ useLocal
2338
+ });
2339
+ if (useLocal) {
2340
+ if (subscriberId) {
2341
+ this.bookingRemoteQuerySubscribers.delete(subscriberId);
2342
+ }
2343
+ if (subscriberId && typeof callback === 'function') {
2344
+ this.bookingQuerySubscribers.set(subscriberId, {
2345
+ callback,
2346
+ context: queryPayload
2347
+ });
2348
+ this.logInfo('handleBookingList: 已注册订阅者(今天)', {
2349
+ subscriberId,
2350
+ totalSubscribers: this.bookingQuerySubscribers.size
2351
+ });
2352
+ }
2353
+ return this.computeBookingQueryResult(queryPayload);
2354
+ } else {
2355
+ if (subscriberId) {
2356
+ this.bookingQuerySubscribers.delete(subscriberId);
2357
+ }
2358
+ if (subscriberId && typeof callback === 'function') {
2359
+ const withFields = this.resolveBookingSalesWith(queryPayload);
2360
+ const requestPayload = this.normalizeBookingRemoteQueryPayload(queryPayload, withFields);
2361
+ const cacheKey = this.buildBookingRemoteCacheKey(requestPayload);
2362
+ this.bookingRemoteQuerySubscribers.set(subscriberId, {
2363
+ callback,
2364
+ context: queryPayload,
2365
+ cacheKey
2366
+ });
2367
+ this.logInfo('handleBookingList: 已注册订阅者(非今天)', {
2368
+ subscriberId,
2369
+ cacheKey,
2370
+ totalSubscribers: this.bookingRemoteQuerySubscribers.size
2371
+ });
2372
+ }
2373
+ if (subscriberId) {
2374
+ this.logInfo('handleBookingList: 已清理订阅者(非今天)', {
2375
+ subscriberId,
2376
+ remaining: this.bookingQuerySubscribers.size
2377
+ });
2378
+ }
2379
+ return this.fetchBookingListFromAPI(queryPayload);
2380
+ }
2381
+ };
2382
+
2383
+ /**
2384
+ * 处理资源列表查询
2385
+ * 转发到资源模块去
2386
+ */
2387
+ handleResourceList = async ({
2388
+ url,
2389
+ method,
2390
+ data,
2391
+ config
2392
+ }) => {
2393
+ const list = this.resource?.getResources({
2394
+ skip: data?.skip || 1,
2395
+ num: data?.num || 10
2396
+ });
2397
+ this.logInfo('Server 劫持 Resource 接口:handleResourceList: 资源列表查询结果', {
2398
+ list
2399
+ });
2400
+ return {
2401
+ code: 200,
2402
+ data: {
2403
+ list,
2404
+ count: list?.length || 0
2405
+ },
2406
+ message: '',
2407
+ status: true
2408
+ };
2409
+ };
2410
+
2411
+ /**
2412
+ * 代理刷新 / 合并订单到本地(单笔 / 批量二合一):
2413
+ * - 入参(GET query 或 data 任一):
2414
+ * - `order_id`:单笔模式,原有行为
2415
+ * - `order_ids`:批量模式,逗号分隔的 id 列表(如 `?order_ids=1,2,3`),数组也兼容
2416
+ * - 同时存在时优先批量
2417
+ * - 单笔流程:后端 /order/sales/{id}?with[]=all → 本地有则覆盖、无则 merge → 落 SQLite → emit onOrdersChanged
2418
+ * - 批量流程:并发拉取每笔详情 → 一次性 `upsertOrdersFromRemote` 合并写入 → 仅 emit 一次 onOrdersChanged,
2419
+ * 单笔失败不影响其他成功项;返回 { overwritten, inserted, failed }
2420
+ * - 监听级联:onOrdersChanged 触发订单 / 预约 / bookingRemoteCache 三路订阅者推送
2421
+ *
2422
+ * @example 单笔:GET /update/localOrder?order_id=96971
2423
+ * @example 批量:GET /update/localOrder?order_ids=96971,96972,96973
2424
+ */
2425
+ handleUpdateLocalOrder = async ({
2426
+ url,
2427
+ data
2428
+ }) => {
2429
+ if (typeof data?.order_id === 'string' && data?.order_id?.startsWith('LOCAL_')) {
2430
+ // 本地订单跳过这个更新
2431
+ // TODO 待优化 UI 层的调用
2432
+ return {
2433
+ code: 200,
2434
+ status: true
2435
+ };
2436
+ }
2437
+ const batchIds = this.parseOrderIdsParam(url, data);
2438
+ if (batchIds.length > 0) {
2439
+ return this.handleUpdateLocalOrdersBatch(batchIds);
2440
+ }
2441
+ const rawOrderId = (data && typeof data === 'object' ? data.order_id : undefined) ?? this.extractQueryParam(url, 'order_id');
2442
+ const orderId = rawOrderId === undefined || rawOrderId === null || rawOrderId === '' ? null : String(rawOrderId).trim();
2443
+ this.logInfo('handleUpdateLocalOrder: 开始处理', {
2444
+ url,
2445
+ orderId
2446
+ });
2447
+ if (!orderId) {
2448
+ this.logWarning('handleUpdateLocalOrder: order_id 缺失', {
2449
+ url,
2450
+ data
2451
+ });
2452
+ return {
2453
+ code: 400,
2454
+ status: false,
2455
+ message: 'order_id 缺失',
2456
+ data: null
2457
+ };
2458
+ }
2459
+ if (!this.order) {
2460
+ this.logError('handleUpdateLocalOrder: Order 模块未注册');
2461
+ return {
2462
+ code: 500,
2463
+ status: false,
2464
+ message: 'Order 模块未注册',
2465
+ data: null
2466
+ };
2467
+ }
2468
+ if (!this.app?.request) {
2469
+ this.logError('handleUpdateLocalOrder: app.request 不可用');
2470
+ return {
2471
+ code: 500,
2472
+ status: false,
2473
+ message: 'app.request 不可用',
2474
+ data: null
2475
+ };
2476
+ }
2477
+ const backendPath = `/shop/order/sales/${encodeURIComponent(orderId)}?with%5B%5D=all`;
2478
+ try {
2479
+ const response = await this.app.request.get(backendPath, undefined, {
2480
+ isShopApi: true
2481
+ });
2482
+ const fresh = response?.data ?? response;
2483
+ if (!fresh || typeof fresh !== 'object' || fresh.order_id == null) {
2484
+ this.logError('handleUpdateLocalOrder: 后端返回订单为空', {
2485
+ orderId,
2486
+ backendPath
2487
+ });
2488
+ return {
2489
+ code: 500,
2490
+ status: false,
2491
+ message: '后端返回订单为空',
2492
+ data: null
2493
+ };
2494
+ }
2495
+ const exists = !!this.order.getOrderByOrderId(orderId);
2496
+ if (exists) {
2497
+ const {
2498
+ overwritten
2499
+ } = await this.order.overwriteExistingOrder(fresh);
2500
+ this.logInfo('handleUpdateLocalOrder: 覆盖完成', {
2501
+ orderId,
2502
+ overwritten
2503
+ });
2504
+ return {
2505
+ code: 200,
2506
+ status: true,
2507
+ message: '',
2508
+ data: {
2509
+ overwritten,
2510
+ inserted: false,
2511
+ order_id: orderId
2512
+ }
2513
+ };
2514
+ }
2515
+ await this.order.upsertOrdersFromRemote([fresh]);
2516
+ this.logInfo('handleUpdateLocalOrder: 本地合并新订单完成', {
2517
+ orderId
2518
+ });
2519
+ return {
2520
+ code: 200,
2521
+ status: true,
2522
+ message: '',
2523
+ data: {
2524
+ overwritten: false,
2525
+ inserted: true,
2526
+ order_id: orderId
2527
+ }
2528
+ };
2529
+ } catch (error) {
2530
+ const errorMessage = error instanceof Error ? error.message : String(error);
2531
+ this.logError('handleUpdateLocalOrder: 请求失败', {
2532
+ orderId,
2533
+ backendPath,
2534
+ error: errorMessage
2535
+ });
2536
+ return {
2537
+ code: 500,
2538
+ status: false,
2539
+ message: errorMessage,
2540
+ data: null
2541
+ };
2542
+ }
2543
+ };
2544
+ handleOrderSalesCheckout = async ({
2545
+ data
2546
+ }) => {
2547
+ return this.handleOrderCheckoutSubmit({
2548
+ backendPath: '/shop/order/sales/checkout',
2549
+ data,
2550
+ title: 'handleOrderSalesCheckout'
2551
+ });
2552
+ };
2553
+ handleOrderCheckout = async ({
2554
+ data
2555
+ }) => {
2556
+ return this.handleOrderCheckoutSubmit({
2557
+ backendPath: '/order/checkout',
2558
+ data,
2559
+ title: 'handleOrderCheckout'
2560
+ });
2561
+ };
2562
+ getDeviceTaskPlugin() {
2563
+ return this.app?.getPlugin?.('deviceTask') || null;
2564
+ }
2565
+ getIdGeneratorPlugin() {
2566
+ return this.app?.getPlugin?.('idGenerator') || null;
2567
+ }
2568
+ getAppData(key) {
2569
+ const getData = this.core.getPlugin('app')?.getData?.();
2570
+ if (typeof getData === 'function') return getData(key);
2571
+ const directValue = this.app?.getData?.(key);
2572
+ if (directValue !== undefined) return directValue;
2573
+ const coreData = this.app?.models?.getStore?.().getDataByModel?.('core', 'core') || this.app?.data?.store?.getStore?.().getDataByModel?.('core', 'core') || this.app?.data?.store?.getStore?.().getDataByModel?.('core')?.core;
2574
+ return coreData?.[key];
2575
+ }
2576
+ getSmallTicketShopInfo() {
2577
+ const shopInfo = this.getAppData('shop_info') || {};
2578
+ return {
2579
+ ...shopInfo,
2580
+ currency_symbol: shopInfo.currency_symbol || this.getAppData('shop_symbol') || this.getAppData('currency_symbol'),
2581
+ currency_code: shopInfo.currency_code || this.getAppData('shop_currency_code') || this.getAppData('currency_code'),
2582
+ tax_title: shopInfo.tax_title || this.getAppData('tax_title'),
2583
+ tax_country_code: shopInfo.tax_country_code || this.getAppData('tax_country_code'),
2584
+ is_price_include_tax: shopInfo.is_price_include_tax ?? this.getAppData('is_price_include_tax')
2585
+ };
2586
+ }
2587
+ taskOk(result) {
2588
+ return {
2589
+ status: 'success',
2590
+ result
2591
+ };
2592
+ }
2593
+ taskFail(code, message, result) {
2594
+ return {
2595
+ status: 'failed',
2596
+ error: {
2597
+ code,
2598
+ message
2599
+ },
2600
+ ...(result !== undefined ? {
2601
+ result
2602
+ } : {})
2603
+ };
2604
+ }
2605
+ taskTimeout(message) {
2606
+ return {
2607
+ status: 'timeout',
2608
+ error: {
2609
+ code: 'TIMEOUT',
2610
+ message
2611
+ }
2612
+ };
2613
+ }
2614
+ async getShortNumberOrDeviceId() {
2615
+ const getAsyncIotDeviceInfo = this.getAppData('async_iot_device_info');
2616
+ if (getAsyncIotDeviceInfo) {
2617
+ const res = await getAsyncIotDeviceInfo?.();
2618
+ if (res?.short_number) {
2619
+ return res?.short_number;
2620
+ }
2621
+ }
2622
+ return String(this.getAppData('device_id') || 0).slice(-2);
2623
+ }
2624
+ buildCheckoutTaskIdempotencyKey(data) {
2625
+ const identity = data?.request_unique_idempotency_token || data?.external_sale_number || data?.order_id || data?.shop_order_number || data?.shop_full_order_number;
2626
+ if (identity === undefined || identity === null || identity === '') return undefined;
2627
+ return `sync_sales:${identity}`;
2628
+ }
2629
+ isBlankCheckoutValue(value) {
2630
+ return value === undefined || value === null || value === '';
2631
+ }
2632
+ isLocalCheckoutOrderId(orderId) {
2633
+ return typeof orderId === 'string' && orderId.startsWith('LOCAL_');
2634
+ }
2635
+ async normalizeCheckoutSubmitData(data, title) {
2636
+ if (!data || typeof data !== 'object') return data;
2637
+ const next = {
2638
+ ...data
2639
+ };
2640
+ const externalSaleNumber = next.external_sale_number;
2641
+ const shouldClearLocalOrderId = this.isLocalCheckoutOrderId(next.order_id);
2642
+ if (shouldClearLocalOrderId) next.order_id = null;
2643
+ // 删除 数据体中的 vouchers,对小票和后端来说他们不需要,这个字段只是单向同步给前端用的,而且目前对订单来说还没什么用
2644
+ delete next.vouchers;
2645
+ const hasCheckoutOrderNumbers = !this.isBlankCheckoutValue(next.shop_order_number) && !this.isBlankCheckoutValue(next.shop_full_order_number);
2646
+ if (hasCheckoutOrderNumbers) {
2647
+ this.logInfo(`${title}: checkout 已携带订单号,跳过查重`, {
2648
+ original_order_id: data.order_id,
2649
+ order_id: next.order_id,
2650
+ external_sale_number: externalSaleNumber,
2651
+ shop_order_number: next.shop_order_number,
2652
+ shop_full_order_number: next.shop_full_order_number
2653
+ });
2654
+ return next;
2655
+ }
2656
+ if (this.isBlankCheckoutValue(externalSaleNumber)) {
2657
+ if (shouldClearLocalOrderId) {
2658
+ this.logInfo(`${title}: checkout 本地订单号已清理`, {
2659
+ original_order_id: data.order_id,
2660
+ order_id: next.order_id
2661
+ });
2662
+ }
2663
+ return next;
2664
+ }
2665
+ if (!this.order || typeof this.order.getLocalOrderFromMemoryByLookup !== 'function') {
2666
+ if (shouldClearLocalOrderId) {
2667
+ this.logInfo(`${title}: checkout 本地订单号已清理`, {
2668
+ original_order_id: data.order_id,
2669
+ external_sale_number: externalSaleNumber
2670
+ });
2671
+ }
2672
+ return next;
2673
+ }
2674
+ try {
2675
+ const localOrder = this.order.getLocalOrderFromMemoryByLookup(externalSaleNumber);
2676
+ if (!localOrder) {
2677
+ if (shouldClearLocalOrderId) {
2678
+ this.logInfo(`${title}: checkout 本地订单号已清理`, {
2679
+ original_order_id: data.order_id,
2680
+ external_sale_number: externalSaleNumber,
2681
+ localOrderFound: false
2682
+ });
2683
+ }
2684
+ return next;
2685
+ }
2686
+ if (this.isBlankCheckoutValue(next.shop_order_number)) {
2687
+ next.shop_order_number = localOrder.shop_order_number;
2688
+ }
2689
+ if (this.isBlankCheckoutValue(next.shop_full_order_number)) {
2690
+ next.shop_full_order_number = localOrder.shop_full_order_number;
2691
+ }
2692
+ this.logInfo(`${title}: checkout 已复用本地订单号`, {
2693
+ original_order_id: data.order_id,
2694
+ order_id: next.order_id,
2695
+ external_sale_number: externalSaleNumber,
2696
+ local_order_id: localOrder.order_id,
2697
+ shop_order_number: next.shop_order_number,
2698
+ shop_full_order_number: next.shop_full_order_number
2699
+ });
2700
+ // 删除 数据体中的 vouchers,对小票和后端来说他们不需要,这个字段只是单向同步给前端用的,而且目前对订单来说还没什么用
2701
+ return next;
2702
+ } catch (error) {
2703
+ this.logWarning(`${title}: checkout 查找本地订单失败`, {
2704
+ external_sale_number: externalSaleNumber,
2705
+ error: this.getUnknownErrorMessage(error),
2706
+ error_detail: this.normalizeUnknownError(error)
2707
+ });
2708
+ return next;
2709
+ }
2710
+ }
2711
+ async ensureCheckoutOrderNumbers(data, title) {
2712
+ if (!data || typeof data !== 'object') return data;
2713
+ const next = {
2714
+ ...data
2715
+ };
2716
+ const needsShopOrderNumber = next.shop_order_number === undefined || next.shop_order_number === null || next.shop_order_number === '';
2717
+ const needsShopFullOrderNumber = next.shop_full_order_number === undefined || next.shop_full_order_number === null || next.shop_full_order_number === '';
2718
+ if (!needsShopOrderNumber && !needsShopFullOrderNumber) return next;
2719
+ const idGenerator = this.getIdGeneratorPlugin();
2720
+ if (!idGenerator) {
2721
+ this.logWarning(`${title}: idGenerator 插件不可用,无法生成订单号`, {
2722
+ needsShopOrderNumber,
2723
+ needsShopFullOrderNumber
2724
+ });
2725
+ return next;
2726
+ }
2727
+ try {
2728
+ if (needsShopOrderNumber && idGenerator.generateShopOrderNumber) {
2729
+ next.shop_order_number = await idGenerator.generateShopOrderNumber({
2730
+ biz: next.business_code || 'ticket'
2731
+ });
2732
+ }
2733
+ if (needsShopFullOrderNumber && idGenerator.generateReceiptId) {
2734
+ next.shop_full_order_number = await idGenerator.generateReceiptId({
2735
+ biz: next.business_code || 'ticket'
2736
+ });
2737
+ }
2738
+ this.logInfo(`${title}: checkout 订单号已准备`, {
2739
+ shop_order_number: next.shop_order_number,
2740
+ shop_full_order_number: next.shop_full_order_number
2741
+ });
2742
+ } catch (error) {
2743
+ this.logError(`${title}: 生成 checkout 订单号失败`, {
2744
+ error: error instanceof Error ? error.message : String(error)
2745
+ });
2746
+ }
2747
+ return next;
2748
+ }
2749
+ async dispatchCheckoutSyncTask(params) {
2750
+ this.registerDeviceTaskActions();
2751
+ const deviceTask = this.getDeviceTaskPlugin();
2752
+ const debugDeviceId = await this.getShortNumberOrDeviceId();
2753
+ const debugIdempotencyKey = this.buildCheckoutTaskIdempotencyKey(params.data);
2754
+ if (!deviceTask?.dispatch) {
2755
+ this.logError(`${params.title}: deviceTask.dispatch 不可用`);
2756
+ return;
2757
+ }
2758
+ try {
2759
+ const result = await deviceTask.dispatch({
2760
+ action: 'sync_sales',
2761
+ device_id: debugDeviceId,
2762
+ payload: {
2763
+ backendPath: params.backendPath,
2764
+ data: params.data,
2765
+ external_sale_number: params.data?.external_sale_number,
2766
+ title: params.title
2767
+ },
2768
+ retry_limit: 5,
2769
+ idempotency_key: debugIdempotencyKey,
2770
+ name: '同步订单',
2771
+ source: 'server',
2772
+ source_type: 'order',
2773
+ source_id: params.data?.order_id || params.data?.external_sale_number || params.data?.shop_order_number
2774
+ });
2775
+ this.logInfo(`${params.title}: sync_sales 任务已派发`, {
2776
+ uuid: result?.uuid,
2777
+ status: result?.status,
2778
+ reused: result?.reused
2779
+ });
2780
+ } catch (error) {
2781
+ this.logError(`${params.title}: 派发 sync_sales 任务失败`, {
2782
+ error: error instanceof Error ? error.message : String(error)
2783
+ });
2784
+ }
2785
+ }
2786
+ async dispatchPrintOtherReceiptTask(params) {
2787
+ const deviceTask = this.getDeviceTaskPlugin();
2788
+ if (!deviceTask?.dispatch) {
2789
+ this.logWarning('dispatchPrintOtherReceiptTask: deviceTask.dispatch 不可用');
2790
+ return;
2791
+ }
2792
+ try {
2793
+ let printIdentity = this.getPrintOrderIdentity(params.syncedOrder);
2794
+ const deviceId = await this.getShortNumberOrDeviceId();
2795
+ await deviceTask.dispatch({
2796
+ action: 'print_all',
2797
+ device_id: params.deviceId ?? deviceId,
2798
+ payload: {
2799
+ type: params.isManualPrint ? '0' : '99',
2800
+ data: params.receiptOnly ? {
2801
+ order_id: printIdentity,
2802
+ small_ticket_data: params.syncedOrder?.payment_info?.small_ticket_data,
2803
+ skip_wristband: true,
2804
+ skip_voucher: true
2805
+ } : {
2806
+ order_id: printIdentity,
2807
+ machine_code_print_info: params.syncedOrder?.payment_info?.machine_code_print_info,
2808
+ machine_code_print_info_v2: params.syncedOrder?.machine_code_print_info_v2,
2809
+ skip_receipt: true
2810
+ }
2811
+ },
2812
+ idempotency_key: this.buildPrintOtherReceiptIdempotencyKey(params.syncedOrder, {
2813
+ receiptOnly: params.receiptOnly
2814
+ }),
2815
+ name: 'PRINT_V1',
2816
+ source: 'server',
2817
+ source_type: 'order',
2818
+ source_id: printIdentity
2819
+ });
2820
+ } catch (error) {
2821
+ this.logError('dispatchPrintOtherReceiptTask: 派发失败', {
2822
+ error: error instanceof Error ? error.message : String(error)
2823
+ });
2824
+ }
2825
+ }
2826
+ buildPrintOtherReceiptIdempotencyKey(order, options) {
2827
+ const identity = order?.external_sale_number || order?.shop_order_number || order?.shop_full_order_number || order?.order_id;
2828
+ if (identity === undefined || identity === null || identity === '') return undefined;
2829
+ return `${options?.receiptOnly ? 'print_receipt' : 'print_voucher'}:${identity}:${Date.now()}`;
2830
+ }
2831
+ getPrintOrderIdentity(order) {
2832
+ return order?.order_id || order?.external_sale_number || order?.shop_order_number || order?.shop_full_order_number;
2833
+ }
2834
+ async dispatchLocalReceiptPrint(params) {
2835
+ if (params.requireFullyPaid && !this.isSalesOrderFullyPaid(params.order)) {
2836
+ return {
2837
+ printed: false,
2838
+ order: params.order
2839
+ };
2840
+ }
2841
+ const printableOrder = await this.withLocalSmallTicketData(params.order, {
2842
+ requireFullyPaid: params.requireFullyPaid
2843
+ });
2844
+ if (!(0, _smallTicket.hasSmallTicketData)(printableOrder.payment_info?.small_ticket_data)) {
2845
+ return {
2846
+ printed: false,
2847
+ order: printableOrder
2848
+ };
2849
+ }
2850
+ this.logInfo('dispatchLocalReceiptPrint: 构建触发任务前', {
2851
+ checkoutData: params.checkoutData,
2852
+ printableOrder,
2853
+ response: params.response
2854
+ });
2855
+ await this.dispatchPrintOtherReceiptTask({
2856
+ checkoutData: params.checkoutData,
2857
+ syncedOrder: printableOrder,
2858
+ response: params.response,
2859
+ receiptOnly: true,
2860
+ isManualPrint: params.isManualPrint
2861
+ });
2862
+ return {
2863
+ printed: true,
2864
+ order: printableOrder
2865
+ };
2866
+ }
2867
+ shouldLookupLocalPrintOrder(input) {
2868
+ const flag = input?.lookup_order_from_db ?? input?.lookupOrderFromDb;
2869
+ return flag === true || flag === 1 || flag === '1';
2870
+ }
2871
+ getLocalPrintOrderLookup(input) {
2872
+ const orderId = input?.order_id ?? input?.orderId;
2873
+ if (orderId !== undefined && orderId !== null && orderId !== '') return orderId;
2874
+ const externalSaleNumber = input?.external_sale_number ?? input?.externalSaleNumber;
2875
+ if (externalSaleNumber !== undefined && externalSaleNumber !== null && externalSaleNumber !== '') {
2876
+ return externalSaleNumber;
2877
+ }
2878
+ return undefined;
2879
+ }
2880
+ handleLocalPrintOrder = async ({
2881
+ data
2882
+ }) => {
2883
+ try {
2884
+ const inputOrder = data?.order && typeof data.order === 'object' ? data.order : data;
2885
+ if (!inputOrder || typeof inputOrder !== 'object') {
2886
+ return {
2887
+ code: 400,
2888
+ status: false,
2889
+ message: '订单数据不能为空',
2890
+ data: {
2891
+ printed: false,
2892
+ order: null
2893
+ }
2894
+ };
2895
+ }
2896
+ const shouldLookup = this.shouldLookupLocalPrintOrder(inputOrder) || this.shouldLookupLocalPrintOrder(data);
2897
+ this.logInfo('handleLocalPrintOrder: 是否需要查找本地订单', {
2898
+ shouldLookup,
2899
+ inputOrder,
2900
+ data
2901
+ });
2902
+ const lookup = shouldLookup ? this.getLocalPrintOrderLookup(inputOrder) ?? this.getLocalPrintOrderLookup(data) : undefined;
2903
+ let sourceOrder = inputOrder;
2904
+ if (shouldLookup) {
2905
+ if (lookup === undefined) {
2906
+ return {
2907
+ code: 400,
2908
+ status: false,
2909
+ message: '订单 lookup 缺失',
2910
+ data: {
2911
+ printed: false,
2912
+ order: null
2913
+ }
2914
+ };
2915
+ }
2916
+ if (!this.order?.getLocalOrderByLookup) {
2917
+ return {
2918
+ code: 500,
2919
+ status: false,
2920
+ message: 'Order 模块未注册',
2921
+ data: {
2922
+ printed: false,
2923
+ order: null
2924
+ }
2925
+ };
2926
+ }
2927
+ const localOrder = await this.order.getLocalOrderByLookup(lookup);
2928
+ if (!localOrder) {
2929
+ return {
2930
+ code: 404,
2931
+ status: false,
2932
+ message: '订单不存在',
2933
+ data: {
2934
+ printed: false,
2935
+ order: null
2936
+ }
2937
+ };
2938
+ }
2939
+ sourceOrder = {
2940
+ ...localOrder,
2941
+ ...inputOrder,
2942
+ order_id: localOrder.order_id ?? inputOrder.order_id ?? inputOrder.orderId,
2943
+ external_sale_number: localOrder.external_sale_number ?? inputOrder.external_sale_number ?? inputOrder.externalSaleNumber
2944
+ };
2945
+ }
2946
+ const checkoutData = await this.ensureCheckoutOrderNumbers({
2947
+ ...sourceOrder,
2948
+ small_ticket_data_flag: 1
2949
+ }, 'handleLocalPrintOrder');
2950
+ const printableOrder = {
2951
+ ...checkoutData,
2952
+ need_sync: checkoutData.need_sync ?? 1
2953
+ };
2954
+ this.logInfo('handleLocalPrintOrder: 本地打印小票,构建触发任务前', {
2955
+ checkoutData,
2956
+ printableOrder
2957
+ });
2958
+ const printResult = await this.dispatchLocalReceiptPrint({
2959
+ checkoutData,
2960
+ order: printableOrder,
2961
+ response: null,
2962
+ requireFullyPaid: false,
2963
+ isManualPrint: true
2964
+ });
2965
+ return {
2966
+ code: 200,
2967
+ status: true,
2968
+ message: '',
2969
+ data: {
2970
+ printed: printResult.printed,
2971
+ order: printResult.order
2972
+ }
2973
+ };
2974
+ } catch (error) {
2975
+ const message = error instanceof Error ? error.message : String(error);
2976
+ this.logError('handleLocalPrintOrder: 本地打印小票失败', {
2977
+ error: message
2978
+ });
2979
+ return {
2980
+ code: 500,
2981
+ status: false,
2982
+ message,
2983
+ data: {
2984
+ printed: false,
2985
+ order: null
2986
+ }
2987
+ };
2988
+ }
2989
+ };
2990
+ handleMachinecodeBatchMeta = async ({
2991
+ data
2992
+ }) => {
2993
+ const title = 'handleMachinecodeBatchMeta';
2994
+ const backendPath = '/shop/machinecode/batch';
2995
+ const ids = Array.isArray(data?.ids) ? data.ids : [];
2996
+ const status = data?.status;
2997
+ this.logInfo(`${title}: 开始处理`, {
2998
+ backendPath,
2999
+ batchSize: ids.length,
3000
+ status,
3001
+ order_lookup: this.resolveMachinecodeBatchMetadataLookup(data) ?? null
3002
+ });
3003
+ if (ids.length === 0) {
3004
+ return {
3005
+ code: 200,
3006
+ status: true,
3007
+ message: '',
3008
+ data: []
3009
+ };
3010
+ }
3011
+ if (status !== 'used' && status !== 'unused') {
3012
+ return {
3013
+ code: 400,
3014
+ status: false,
3015
+ message: '无效的核销状态',
3016
+ data: null
3017
+ };
3018
+ }
3019
+ try {
3020
+ await this.syncMachinecodeRedeemStatusToLocalOrder(data, ids, status);
3021
+ } catch (error) {
3022
+ const message = this.getUnknownErrorMessage(error);
3023
+ this.logError(`${title}: 本地订单核销状态更新失败`, {
3024
+ error: message,
3025
+ error_detail: this.normalizeUnknownError(error)
3026
+ });
3027
+ return {
3028
+ code: 500,
3029
+ status: false,
3030
+ message,
3031
+ data: null
3032
+ };
3033
+ }
3034
+ if (!this.app?.request?.post) {
3035
+ this.logError(`${title}: app.request 不可用`, {
3036
+ backendPath
3037
+ });
3038
+ return {
3039
+ code: 500,
3040
+ status: false,
3041
+ message: 'app.request 不可用',
3042
+ data: null
3043
+ };
3044
+ }
3045
+ try {
3046
+ const response = await this.app.request.post(backendPath, {
3047
+ ids,
3048
+ status
3049
+ }, {
3050
+ isShopApi: true,
3051
+ customToast: () => {}
3052
+ });
3053
+ return {
3054
+ code: response?.code ?? 200,
3055
+ status: response?.status ?? true,
3056
+ message: response?.message ?? '',
3057
+ data: response?.data ?? response
3058
+ };
3059
+ } catch (error) {
3060
+ const backendError = this.extractBackendErrorResponse(error);
3061
+ if (backendError) {
3062
+ return {
3063
+ code: backendError.code ?? 500,
3064
+ status: backendError.status ?? false,
3065
+ message: backendError.message ?? '后端拒绝 batch-metadata',
3066
+ data: backendError.data ?? null
3067
+ };
3068
+ }
3069
+ const message = this.getUnknownErrorMessage(error);
3070
+ this.logError(`${title}: 请求失败`, {
3071
+ backendPath,
3072
+ error: message,
3073
+ error_detail: this.normalizeUnknownError(error)
3074
+ });
3075
+ return {
3076
+ code: 500,
3077
+ status: false,
3078
+ message,
3079
+ data: null
3080
+ };
3081
+ }
3082
+ };
3083
+ resolveMachinecodeBatchMetadataLookup(data) {
3084
+ const candidates = [data?.order_lookup, data?.orderLookup, data?.external_sale_number, data?.externalSaleNumber, data?.order_id, data?.orderId, data?.order_number, data?.orderNumber, data?.shop_order_number, data?.shopOrderNumber, data?.shop_full_order_number, data?.shopFullOrderNumber];
3085
+ return candidates.find(candidate => candidate !== undefined && candidate !== null && candidate !== '');
3086
+ }
3087
+ async syncMachinecodeRedeemStatusToLocalOrder(data, ids, status) {
3088
+ const title = 'handleMachinecodeBatchMetadata';
3089
+ const lookup = this.resolveMachinecodeBatchMetadataLookup(data);
3090
+ if (lookup === undefined) {
3091
+ this.logWarning(`${title}: order lookup 缺失,跳过本地订单核销状态更新`, {
3092
+ batchSize: ids.length,
3093
+ status
3094
+ });
3095
+ return;
3096
+ }
3097
+ if (!this.order?.getLocalOrderByLookup) {
3098
+ this.logWarning(`${title}: Order 模块未注册,跳过本地订单核销状态更新`, {
3099
+ lookup,
3100
+ status
3101
+ });
3102
+ return;
3103
+ }
3104
+ const localOrder = await this.order.getLocalOrderByLookup(lookup);
3105
+ if (!localOrder) {
3106
+ this.logWarning(`${title}: 本地订单不存在,跳过核销状态更新`, {
3107
+ lookup,
3108
+ status
3109
+ });
3110
+ return;
3111
+ }
3112
+ const voucherIdSet = new Set(ids.filter(id => id !== undefined && id !== null && id !== '').map(id => String(id)));
3113
+ if (voucherIdSet.size === 0) {
3114
+ this.logWarning(`${title}: voucher ids 缺失,跳过本地订单核销状态更新`, {
3115
+ lookup,
3116
+ status
3117
+ });
3118
+ return;
3119
+ }
3120
+ const vouchers = Array.isArray(localOrder.vouchers) ? localOrder.vouchers : [];
3121
+ let hasUpdatedVoucher = false;
3122
+ const nextVouchers = vouchers.map(voucher => {
3123
+ const voucherId = voucher?.voucher_id;
3124
+ if (voucherId === undefined || voucherId === null) return voucher;
3125
+ if (!voucherIdSet.has(String(voucherId))) return voucher;
3126
+ hasUpdatedVoucher = true;
3127
+ return {
3128
+ ...voucher,
3129
+ status
3130
+ };
3131
+ });
3132
+ if (!hasUpdatedVoucher) {
3133
+ this.logWarning(`${title}: 本地订单未命中任何 voucher,跳过核销状态更新`, {
3134
+ lookup,
3135
+ voucher_ids: Array.from(voucherIdSet),
3136
+ status
3137
+ });
3138
+ return;
3139
+ }
3140
+ if (typeof this.order.overwriteExistingOrder === 'function') {
3141
+ await this.order.overwriteExistingOrder({
3142
+ ...localOrder,
3143
+ vouchers: nextVouchers
3144
+ });
3145
+ this.logInfo(`${title}: 本地订单 vouchers 核销状态已更新`, {
3146
+ lookup,
3147
+ updatedCount: voucherIdSet.size,
3148
+ status
3149
+ });
3150
+ return;
3151
+ }
3152
+ if (data?.external_sale_number && typeof this.order.markOrderSyncedByExternalSaleNumber === 'function') {
3153
+ await this.order.markOrderSyncedByExternalSaleNumber({
3154
+ externalSaleNumber: data.external_sale_number,
3155
+ patch: {
3156
+ vouchers: nextVouchers
3157
+ }
3158
+ });
3159
+ this.logInfo(`${title}: 本地订单 vouchers 核销状态已更新`, {
3160
+ lookup,
3161
+ updatedCount: voucherIdSet.size,
3162
+ status
3163
+ });
3164
+ return;
3165
+ }
3166
+ this.logWarning(`${title}: Order 模块不支持本地订单覆盖,跳过核销状态更新`, {
3167
+ lookup,
3168
+ status
3169
+ });
3170
+ }
3171
+ async handleOrderCheckoutSubmit(params) {
3172
+ const {
3173
+ backendPath,
3174
+ data,
3175
+ title
3176
+ } = params;
3177
+ this.logInfo(`${title}: 开始处理`, {
3178
+ backendPath,
3179
+ order_id: data?.order_id,
3180
+ external_sale_number: data?.external_sale_number,
3181
+ order_number: data?.order_number
3182
+ });
3183
+ if (data?.sync_mode || data?.syncMode) {
3184
+ return this.handleSyncCheckoutSubmit({
3185
+ backendPath,
3186
+ data,
3187
+ title
3188
+ });
3189
+ }
3190
+ const normalizedData = await this.normalizeCheckoutSubmitData(data, title);
3191
+ const checkoutData = await this.ensureCheckoutOrderNumbers(normalizedData, title);
3192
+ const pendingResult = await this.handlePendingSyncCheckoutOrder({
3193
+ backendPath,
3194
+ data: checkoutData,
3195
+ title,
3196
+ reason: 'checkout 已转入设备任务同步'
3197
+ });
3198
+ if (pendingResult?.status === true) {
3199
+ const pendingOrder = pendingResult.data;
3200
+ const platform = this.core.context?.getPlatform();
3201
+ const isAndroid = platform === 'android';
3202
+ if (pendingOrder && this.shouldBuildSmallTicketData(checkoutData) && this.isSalesOrderFullyPaid(pendingOrder) && (
3203
+ // TODO 这里先加个兼容,如果是安卓,则需要 type=virtual 才走打印,如果是苹果则无限制
3204
+ isAndroid ? checkoutData.type === 'virtual' : true)) {
3205
+ await this.dispatchLocalReceiptPrint({
3206
+ checkoutData,
3207
+ order: pendingOrder,
3208
+ response: pendingResult,
3209
+ requireFullyPaid: true
3210
+ });
3211
+ }
3212
+ await this.dispatchCheckoutSyncTask({
3213
+ backendPath,
3214
+ data: checkoutData,
3215
+ title
3216
+ });
3217
+ }
3218
+ if (!pendingResult.data.order_id) {
3219
+ return {
3220
+ ...pendingResult,
3221
+ data: {
3222
+ ...pendingResult.data,
3223
+ order_id: pendingResult.data.external_sale_number
3224
+ }
3225
+ };
3226
+ }
3227
+ return pendingResult;
3228
+ }
3229
+ async handleSyncCheckoutSubmit(params) {
3230
+ const {
3231
+ backendPath,
3232
+ data,
3233
+ title
3234
+ } = params;
3235
+ if (!this.app?.request?.post) {
3236
+ this.logError(`${title}: sync checkout app.request 不可用`);
3237
+ return {
3238
+ code: 500,
3239
+ status: false,
3240
+ message: 'app.request 不可用',
3241
+ data: null
3242
+ };
3243
+ }
3244
+ try {
3245
+ const syncResult = await this.runCheckoutSync({
3246
+ backendPath,
3247
+ data,
3248
+ title,
3249
+ persistCheckoutDataWithResponse: true
3250
+ });
3251
+ if (syncResult.rejected) {
3252
+ const normalized = this.normalizeCheckoutResponse(syncResult.errorResponse);
3253
+ return {
3254
+ ...normalized,
3255
+ status: false,
3256
+ message: syncResult.message || normalized?.message || '后端明确拒绝 checkout',
3257
+ data: normalized?.data ?? null
3258
+ };
3259
+ }
3260
+ const response = this.withSyncedOrderIdInCheckoutResponse(syncResult.normalizedResponse, syncResult.syncedOrder);
3261
+ const syncedOrder = syncResult.syncedOrder;
3262
+ if (syncedOrder && this.shouldBuildSmallTicketData(syncResult.checkoutData) && this.isSalesOrderFullyPaid(syncedOrder)) {
3263
+ await this.dispatchLocalReceiptPrint({
3264
+ checkoutData: syncResult.checkoutData,
3265
+ order: syncedOrder,
3266
+ response: syncResult.normalizedResponse,
3267
+ requireFullyPaid: true,
3268
+ // print_all 的 type=0 是收据打印;不要沿用同步成功后的券/手环打印 type=99。
3269
+ isManualPrint: true
3270
+ });
3271
+ }
3272
+ return response;
3273
+ } catch (error) {
3274
+ const backendError = this.extractBackendErrorResponse(error);
3275
+ if (backendError) {
3276
+ const normalized = this.normalizeCheckoutResponse(backendError);
3277
+ return {
3278
+ ...normalized,
3279
+ status: false,
3280
+ message: backendError?.message || normalized?.message || '后端明确拒绝 checkout',
3281
+ data: normalized?.data ?? null
3282
+ };
3283
+ }
3284
+ const message = this.getUnknownErrorMessage(error);
3285
+ this.logError(`${title}: sync checkout 请求未明确成功`, {
3286
+ backendPath,
3287
+ error: message,
3288
+ error_detail: this.normalizeUnknownError(error)
3289
+ });
3290
+ return {
3291
+ code: 500,
3292
+ status: false,
3293
+ message,
3294
+ data: null
3295
+ };
3296
+ }
3297
+ }
3298
+ async handlePendingSyncCheckoutOrder(params) {
3299
+ const {
3300
+ backendPath,
3301
+ data,
3302
+ title,
3303
+ reason
3304
+ } = params;
3305
+ const pendingOrder = await this.buildPendingSyncCheckoutOrder(data);
3306
+ if (!pendingOrder) {
3307
+ this.logError(`${title}: checkout 失败且订单缺少本地存储标识`, {
3308
+ backendPath,
3309
+ reason
3310
+ });
3311
+ return {
3312
+ code: 500,
3313
+ status: false,
3314
+ message: '订单缺少本地存储标识,无法写入待同步队列',
3315
+ data: null
3316
+ };
3317
+ }
3318
+ if (!this.order || typeof this.order.upsertPendingSyncOrders !== 'function') {
3319
+ this.logError(`${title}: Order 模块不支持本地待同步写入`, {
3320
+ backendPath,
3321
+ reason
3322
+ });
3323
+ return {
3324
+ code: 500,
3325
+ status: false,
3326
+ message: 'Order 模块不支持本地待同步写入',
3327
+ data: null
3328
+ };
3329
+ }
3330
+ try {
3331
+ await this.order.upsertPendingSyncOrders([pendingOrder]);
3332
+ this.logInfo(`${title}: 订单已写入本地待同步`, {
3333
+ backendPath,
3334
+ reason,
3335
+ order_id: pendingOrder.order_id,
3336
+ external_sale_number: pendingOrder.external_sale_number,
3337
+ order_number: pendingOrder.order_number
3338
+ });
3339
+ const changeGivenAmount = pendingOrder?.payments?.reduce((sum, payment) => sum + this.toAmountNumber(payment?.metadata?.change_given_amount), 0);
3340
+ return {
3341
+ code: 200,
3342
+ status: true,
3343
+ message: '',
3344
+ data: {
3345
+ ...pendingOrder,
3346
+ amount_gap: pendingOrder?.summary?.amount_gap,
3347
+ expect_amount: pendingOrder?.summary?.expect_amount,
3348
+ total_amount: pendingOrder?.summary?.total_amount,
3349
+ payment_status: pendingOrder?.payment_status,
3350
+ payment_info: {
3351
+ ...(pendingOrder?.payment_info || {}),
3352
+ current_change_given_amount: changeGivenAmount
3353
+ }
3354
+ }
3355
+ };
3356
+ } catch (error) {
3357
+ const errorMessage = error instanceof Error ? error.message : String(error);
3358
+ this.logError(`${title}: 写入本地待同步订单失败`, {
3359
+ backendPath,
3360
+ reason,
3361
+ error: errorMessage
3362
+ });
3363
+ return {
3364
+ code: 500,
3365
+ status: false,
3366
+ message: errorMessage,
3367
+ data: null
3368
+ };
3369
+ }
3370
+ }
3371
+ async buildPendingSyncCheckoutOrder(data) {
3372
+ if (!data || typeof data !== 'object') return null;
3373
+ const hasStorageKey = data.external_sale_number !== undefined && data.external_sale_number !== null && data.external_sale_number !== '' ? true : data.order_id !== undefined && data.order_id !== null && data.order_id !== '';
3374
+ if (!hasStorageKey) return null;
3375
+ const pendingOrder = {
3376
+ ...data,
3377
+ need_sync: 1
3378
+ };
3379
+ const productMap = await this.buildSmallTicketProductMap(pendingOrder);
3380
+ const orderWithProductTitles = this.withPendingSyncProductTitles(pendingOrder, productMap);
3381
+ return this.withLocalSmallTicketData(orderWithProductTitles, {
3382
+ requireFullyPaid: true,
3383
+ productMap
3384
+ });
3385
+ }
3386
+ shouldBuildSmallTicketData(order) {
3387
+ if (!order || typeof order !== 'object') return false;
3388
+ // if (hasSmallTicketData(order.payment_info?.small_ticket_data)) return false;
3389
+ return Number(order.small_ticket_data_flag ?? order.smallTicketDataFlag ?? 0) === 1;
3390
+ }
3391
+ shouldPrintSyncedOrder(params) {
3392
+ if (this.hasPaymentStatus(params.syncedOrder)) {
3393
+ return this.isSalesOrderFullyPaid(params.syncedOrder);
3394
+ }
3395
+ return this.isSalesOrderFullyPaid(params.checkoutData);
3396
+ }
3397
+ hasPaymentStatus(order) {
3398
+ return order?.payment_status !== undefined && order?.payment_status !== null && order?.payment_status !== '';
3399
+ }
3400
+ isSalesOrderFullyPaid(order) {
3401
+ if (!order || typeof order !== 'object') return false;
3402
+ const paymentStatus = String(order.payment_status || '').toLowerCase();
3403
+ if (paymentStatus === 'paid' || paymentStatus === 'finished') return true;
3404
+ if (paymentStatus === 'partially_paid' || paymentStatus === 'payment_processing' || paymentStatus === 'pending' || paymentStatus === 'unpaid') {
3405
+ return false;
3406
+ }
3407
+ const amountGap = order.summary?.amount_gap ?? order.amount_gap;
3408
+ if (amountGap !== undefined && amountGap !== null && amountGap !== '') {
3409
+ return this.toAmountNumber(amountGap) <= 0;
3410
+ }
3411
+ const paidAmount = this.sumPaidOrderPayments(order.payments);
3412
+ const targetAmount = this.toAmountNumber(order.summary?.expect_amount ?? order.summary?.total_amount ?? order.expect_amount ?? order.total_amount);
3413
+ if (targetAmount <= 0) return paidAmount > 0;
3414
+ return paidAmount >= targetAmount;
3415
+ }
3416
+ sumPaidOrderPayments(payments) {
3417
+ if (!Array.isArray(payments)) return 0;
3418
+ return payments.reduce((sum, payment) => {
3419
+ const status = String(payment?.status || '').toLowerCase();
3420
+ if (status === 'voided' || status === 'failed' || status === 'cancelled') return sum;
3421
+ return sum + this.toAmountNumber(payment?.amount ?? payment?.origin_amount);
3422
+ }, 0);
3423
+ }
3424
+ toAmountNumber(value) {
3425
+ if (typeof value === 'number') return Number.isFinite(value) ? value : 0;
3426
+ if (typeof value !== 'string') return 0;
3427
+ const parsed = Number(value.replace(/[^\d.-]/g, ''));
3428
+ return Number.isFinite(parsed) ? parsed : 0;
3429
+ }
3430
+ async buildSmallTicketProductMap(order) {
3431
+ const products = Array.isArray(order?.products) ? order.products : [];
3432
+ const productIdSet = new Set();
3433
+ products.forEach(product => {
3434
+ const productId = product?.product_id ?? product?.id;
3435
+ if (productId !== undefined && productId !== null && productId !== '') {
3436
+ productIdSet.add(productId);
3437
+ }
3438
+ if (Array.isArray(product?.product_bundle)) {
3439
+ product.product_bundle.forEach(bundle => {
3440
+ const bundleProductId = bundle?.bundle_product_id ?? bundle?._bundle_product_id ?? bundle?.product_id;
3441
+ if (bundleProductId !== undefined && bundleProductId !== null && bundleProductId !== '') {
3442
+ productIdSet.add(bundleProductId);
3443
+ }
3444
+ });
3445
+ }
3446
+ });
3447
+ const productIds = Array.from(productIdSet);
3448
+ if (!productIds.length || typeof this.products?.getProductById !== 'function') return {};
3449
+ const entries = await Promise.all(productIds.map(async id => {
3450
+ try {
3451
+ const product = await this.products?.getProductById?.(Number(id));
3452
+ return product ? [String(id), product] : null;
3453
+ } catch (error) {
3454
+ this.logWarning('buildSmallTicketProductMap: 查询本地商品失败', {
3455
+ product_id: id,
3456
+ error: error instanceof Error ? error.message : String(error)
3457
+ });
3458
+ return null;
3459
+ }
3460
+ }));
3461
+ return entries.reduce((map, entry) => {
3462
+ if (!entry) return map;
3463
+ const [id, product] = entry;
3464
+ map[id] = product;
3465
+ return map;
3466
+ }, {});
3467
+ }
3468
+ collectSalesDetailProductIds(order) {
3469
+ const products = Array.isArray(order?.products) ? order.products : [];
3470
+ const productIds = new Set();
3471
+ const addProductId = rawProductId => {
3472
+ if (rawProductId === undefined || rawProductId === null || rawProductId === '') return;
3473
+ const productId = Number(rawProductId);
3474
+ if (Number.isFinite(productId)) productIds.add(productId);
3475
+ };
3476
+ products.forEach(product => {
3477
+ addProductId(product?.product_id ?? product?.id);
3478
+ if (Array.isArray(product?.product_bundle)) {
3479
+ product.product_bundle.forEach(bundle => {
3480
+ addProductId(bundle?.bundle_product_id ?? bundle?._bundle_product_id ?? bundle?.product_id);
3481
+ });
3482
+ }
3483
+ });
3484
+ return Array.from(productIds);
3485
+ }
3486
+ async buildSalesDetailProductSnapshotMap(order) {
3487
+ const productIds = this.collectSalesDetailProductIds(order);
3488
+ const getSnapshots = this.products?.getProductSnapshotsByIds;
3489
+ if (!productIds.length || typeof getSnapshots !== 'function') return {};
3490
+ try {
3491
+ return await getSnapshots.call(this.products, productIds);
3492
+ } catch (error) {
3493
+ this.logWarning('buildSalesDetailProductSnapshotMap: 查询本地商品快照失败', {
3494
+ product_ids: productIds,
3495
+ error: error instanceof Error ? error.message : String(error)
3496
+ });
3497
+ return {};
3498
+ }
3499
+ }
3500
+ withProductCatalogSnapshots(product, snapshotMap) {
3501
+ if (!product || typeof product !== 'object') return product;
3502
+ const rawProductId = product?.product_id ?? product?.id;
3503
+ if (rawProductId === undefined || rawProductId === null || rawProductId === '') return product;
3504
+ const productId = Number(rawProductId);
3505
+ if (!Number.isFinite(productId)) return product;
3506
+ const snapshot = snapshotMap[String(productId)];
3507
+ if (!snapshot) return product;
3508
+ const shouldFillCover = (product.product_cover === undefined || product.product_cover === null || product.product_cover === '') && snapshot.cover !== undefined && snapshot.cover !== null && snapshot.cover !== '';
3509
+ const metadata = product.metadata && typeof product.metadata === 'object' ? product.metadata : {};
3510
+ const shouldFillHolderConfig = (metadata.holder_config === undefined || metadata.holder_config === null) && snapshot.holder_config !== undefined && snapshot.holder_config !== null;
3511
+ if (!shouldFillCover && !shouldFillHolderConfig) return product;
3512
+ return {
3513
+ ...product,
3514
+ ...(shouldFillCover ? {
3515
+ product_cover: snapshot.cover
3516
+ } : {}),
3517
+ ...(shouldFillHolderConfig ? {
3518
+ metadata: {
3519
+ ...metadata,
3520
+ holder_config: snapshot.holder_config
3521
+ }
3522
+ } : {})
3523
+ };
3524
+ }
3525
+ withBundleCatalogSnapshots(product, snapshotMap) {
3526
+ if (!Array.isArray(product?.product_bundle)) return product;
3527
+ let hasChanged = false;
3528
+ const productBundle = product.product_bundle.map(bundle => {
3529
+ if (!bundle || typeof bundle !== 'object') return bundle;
3530
+ const rawProductId = bundle?.bundle_product_id ?? bundle?._bundle_product_id ?? bundle?.product_id;
3531
+ if (rawProductId === undefined || rawProductId === null || rawProductId === '') return bundle;
3532
+ const productId = Number(rawProductId);
3533
+ if (!Number.isFinite(productId)) return bundle;
3534
+ const snapshot = snapshotMap[String(productId)];
3535
+ const shouldFillCover = (bundle.cover === undefined || bundle.cover === null || bundle.cover === '') && snapshot?.cover !== undefined && snapshot.cover !== null && snapshot.cover !== '';
3536
+ if (!shouldFillCover) return bundle;
3537
+ hasChanged = true;
3538
+ return {
3539
+ ...bundle,
3540
+ cover: snapshot.cover
3541
+ };
3542
+ });
3543
+ if (!hasChanged) return product;
3544
+ return {
3545
+ ...product,
3546
+ product_bundle: productBundle
3547
+ };
3548
+ }
3549
+ async withSalesDetailProductSnapshots(order) {
3550
+ const products = Array.isArray(order?.products) ? order.products : null;
3551
+ if (!products?.length) return order;
3552
+ const snapshotMap = await this.buildSalesDetailProductSnapshotMap(order);
3553
+ if (!Object.keys(snapshotMap).length) return order;
3554
+ let hasChanged = false;
3555
+ const enrichedProducts = products.map(product => {
3556
+ const productWithBundleSnapshots = this.withBundleCatalogSnapshots(product, snapshotMap);
3557
+ const nextProduct = this.withProductCatalogSnapshots(productWithBundleSnapshots, snapshotMap);
3558
+ if (nextProduct !== product) hasChanged = true;
3559
+ return nextProduct;
3560
+ });
3561
+ if (!hasChanged) return order;
3562
+ const enrichedOrder = {
3563
+ ...order,
3564
+ products: enrichedProducts
3565
+ };
3566
+ return enrichedOrder;
3567
+ }
3568
+ withPendingSyncProductTitles(order, productMap) {
3569
+ const products = Array.isArray(order.products) ? order.products : null;
3570
+ if (!products?.length) return order;
3571
+ return {
3572
+ ...order,
3573
+ products: products.map(product => ({
3574
+ ...product,
3575
+ product_title: this.buildProductTitleSnapshot(product, productMap)
3576
+ }))
3577
+ };
3578
+ }
3579
+ buildProductTitleSnapshot(product, productMap) {
3580
+ const productId = product.product_id ?? product.id;
3581
+ const fallbackProduct = productId !== undefined && productId !== null ? productMap[productId] || productMap[String(productId)] : null;
3582
+ const ownTitle = product.product_title || product.title || product.name || product.product?.title;
3583
+ const fallbackTitle = fallbackProduct?.product_title || fallbackProduct?.title || fallbackProduct?.name || fallbackProduct?.format_title || fallbackProduct?.product?.title;
3584
+ const currentLocale = this.getProductTitleSnapshotCurrentLocale();
3585
+ const autoTitle = this.resolveFirstProductTitleValue(ownTitle) || this.resolveFirstProductTitleValue(fallbackTitle);
3586
+ return PRODUCT_TITLE_SNAPSHOT_KEYS.reduce((snapshot, key) => {
3587
+ snapshot[key] = this.resolveProductTitleValue(ownTitle, key, currentLocale) || this.resolveProductTitleValue(fallbackTitle, key, currentLocale) || (key === 'auto' || key === 'original' ? autoTitle : null);
3588
+ return snapshot;
3589
+ }, {});
3590
+ }
3591
+ getProductTitleSnapshotCurrentLocale() {
3592
+ const shopInfo = this.getSmallTicketShopInfo();
3593
+ const rawLocale = shopInfo.primary_locale || this.getAppData('locale') || this.getAppData('language') || this.core.context?.locale || this.core.context?.language;
3594
+ return this.normalizeProductTitleSnapshotLocale(rawLocale);
3595
+ }
3596
+ normalizeProductTitleSnapshotLocale(value) {
3597
+ if (typeof value !== 'string' || !value) return null;
3598
+ const normalized = value.replace('_', '-');
3599
+ const matchedKey = PRODUCT_TITLE_SNAPSHOT_KEYS.find(key => key === normalized);
3600
+ return matchedKey ?? null;
3601
+ }
3602
+ resolveProductTitleValue(value, key, currentLocale) {
3603
+ if (value === undefined || value === null || value === '') return null;
3604
+ if (typeof value !== 'object') {
3605
+ const title = String(value);
3606
+ if (key === 'auto' || key === 'original') return title;
3607
+ if (key === currentLocale && this.canAssignStringProductTitleToLocale(title, currentLocale)) return title;
3608
+ return null;
3609
+ }
3610
+ const normalizedKey = key.replace('-', '_');
3611
+ const dashedKey = key.replace('_', '-');
3612
+ const rawValue = value[key] ?? value[normalizedKey] ?? value[dashedKey] ?? (key === 'auto' ? value.default : undefined);
3613
+ if (rawValue === undefined || rawValue === null || rawValue === '') return null;
3614
+ if (typeof rawValue === 'object') return this.resolveFirstProductTitleValue(rawValue);
3615
+ return String(rawValue);
3616
+ }
3617
+ canAssignStringProductTitleToLocale(title, locale) {
3618
+ if (!locale) return false;
3619
+ if (locale === 'zh-CN' || locale === 'zh-HK' || locale === 'kitchen') {
3620
+ return this.hasCjkText(title);
3621
+ }
3622
+ if (locale === 'ja') return /[\u3040-\u30ff]/.test(title) || this.hasCjkText(title);
3623
+ if (locale === 'en' || locale === 'pt') return !this.hasCjkText(title);
3624
+ return false;
3625
+ }
3626
+ hasCjkText(value) {
3627
+ return /[\u3400-\u9fff]/.test(value);
3628
+ }
3629
+ resolveFirstProductTitleValue(value) {
3630
+ if (value === undefined || value === null || value === '') return null;
3631
+ if (typeof value !== 'object') return String(value);
3632
+ const preferredKeys = ['auto', 'en', 'original', 'zh-CN', 'zh_CN', 'zh-HK', 'zh_HK', 'kitchen', 'ja', 'pt', 'default'];
3633
+ for (const key of preferredKeys) {
3634
+ const rawValue = value[key];
3635
+ if (rawValue !== undefined && rawValue !== null && rawValue !== '') return String(rawValue);
3636
+ }
3637
+ const firstValue = Object.values(value).find(item => item !== undefined && item !== null && item !== '');
3638
+ return firstValue === undefined ? null : String(firstValue);
3639
+ }
3640
+ getPaymentCodeKey(code) {
3641
+ if (code === undefined || code === null) return '';
3642
+ return String(code).trim().toUpperCase();
3643
+ }
3644
+ isBlankPaymentName(name) {
3645
+ if (name === undefined || name === null) return true;
3646
+ if (typeof name === 'string') return name.trim() === '';
3647
+ return false;
3648
+ }
3649
+ shouldLookupPaymentMethodNameByCode(code) {
3650
+ const codeKey = this.getPaymentCodeKey(code);
3651
+ if (!codeKey) return false;
3652
+ const builtInNameTokens = ['CASH', 'EFTPOS', 'CARD', 'WALLET', 'GIFT'];
3653
+ return !builtInNameTokens.some(token => codeKey.includes(token));
3654
+ }
3655
+ async enrichPaymentNamesByCode(order) {
3656
+ const payments = Array.isArray(order?.payments) ? order.payments : [];
3657
+ const hasMissingName = payments.some(payment => this.isBlankPaymentName(payment?.name) && this.shouldLookupPaymentMethodNameByCode(payment?.code));
3658
+ if (!hasMissingName) return order;
3659
+ try {
3660
+ const paymentModule = await this.getPaymentRouteModule();
3661
+ const payMethods = await paymentModule.getPayMethodListAsync();
3662
+ const nameByCode = new Map();
3663
+ for (const method of payMethods || []) {
3664
+ const codeKey = this.getPaymentCodeKey(method?.code);
3665
+ const name = this.resolveFirstProductTitleValue(method?.name);
3666
+ if (codeKey && name) nameByCode.set(codeKey, name);
3667
+ }
3668
+ let hasResolvedName = false;
3669
+ const enrichedPayments = payments.map(payment => {
3670
+ if (!this.isBlankPaymentName(payment?.name)) return payment;
3671
+ if (!this.shouldLookupPaymentMethodNameByCode(payment?.code)) return payment;
3672
+ const name = nameByCode.get(this.getPaymentCodeKey(payment?.code));
3673
+ if (!name) return payment;
3674
+ hasResolvedName = true;
3675
+ return {
3676
+ ...payment,
3677
+ name
3678
+ };
3679
+ });
3680
+ if (!hasResolvedName) return order;
3681
+ return {
3682
+ ...order,
3683
+ payments: enrichedPayments
3684
+ };
3685
+ } catch (error) {
3686
+ this.logWarning('enrichPaymentNamesByCode: 支付方式名称回填失败', {
3687
+ error: error instanceof Error ? error.message : String(error)
3688
+ });
3689
+ return order;
3690
+ }
3691
+ }
3692
+ async withLocalSmallTicketData(order, options) {
3693
+ if (!this.shouldBuildSmallTicketData(order)) return order;
3694
+ if (options?.requireFullyPaid && !this.isSalesOrderFullyPaid(order)) return order;
3695
+ try {
3696
+ const enrichedOrder = await this.enrichPaymentNamesByCode(order);
3697
+ const productMap = options?.productMap ?? (await this.buildSmallTicketProductMap(enrichedOrder));
3698
+ const smallTicketData = (0, _smallTicket.buildSmallTicketData)({
3699
+ order: enrichedOrder,
3700
+ shopInfo: this.getSmallTicketShopInfo(),
3701
+ productMap
3702
+ });
3703
+ return {
3704
+ ...enrichedOrder,
3705
+ payment_info: {
3706
+ ...(enrichedOrder.payment_info || {}),
3707
+ small_ticket_data: smallTicketData
3708
+ }
3709
+ };
3710
+ } catch (error) {
3711
+ this.logError('withLocalSmallTicketData: 生成本地小票数据失败', {
3712
+ error: error instanceof Error ? error.message : String(error)
3713
+ });
3714
+ return order;
3715
+ }
3716
+ }
3717
+ handleOrderSalesDetail = async ({
3718
+ url,
3719
+ data,
3720
+ path
3721
+ }) => {
3722
+ const requestPath = this.parseRequestPath(url, path);
3723
+ const lookup = this.extractOrderSalesDetailLookup(requestPath);
3724
+ this.logInfo('handleOrderSalesDetail: 开始处理', {
3725
+ url,
3726
+ path: requestPath,
3727
+ lookup
3728
+ });
3729
+ if (!lookup) {
3730
+ this.logWarning('handleOrderSalesDetail: lookup 缺失', {
3731
+ url,
3732
+ path: requestPath
3733
+ });
3734
+ return {
3735
+ code: 400,
3736
+ status: false,
3737
+ message: '订单 lookup 缺失',
3738
+ data: null
3739
+ };
3740
+ }
3741
+ if (!this.order) {
3742
+ this.logError('handleOrderSalesDetail: Order 模块未注册');
3743
+ return {
3744
+ code: 500,
3745
+ status: false,
3746
+ message: 'Order 模块未注册',
3747
+ data: null
3748
+ };
3749
+ }
3750
+ if (!this.shouldForceRemoteSalesDetail(data)) {
3751
+ const localOrder = await this.order.getLocalOrderByLookup(lookup);
3752
+ if (localOrder) {
3753
+ const enrichedOrder = await this.withSalesDetailProductSnapshots(localOrder);
3754
+ this.logInfo('handleOrderSalesDetail: 命中本地订单', {
3755
+ lookup
3756
+ });
3757
+ return {
3758
+ code: 200,
3759
+ status: true,
3760
+ message: '',
3761
+ data: enrichedOrder
3762
+ };
3763
+ }
3764
+ }
3765
+ if (!this.app?.request) {
3766
+ this.logError('handleOrderSalesDetail: app.request 不可用');
3767
+ return {
3768
+ code: 500,
3769
+ status: false,
3770
+ message: 'app.request 不可用',
3771
+ data: null
3772
+ };
3773
+ }
3774
+ const backendPath = this.buildOrderSalesDetailBackendPath(lookup, data);
3775
+ try {
3776
+ const response = await this.app.request.get(backendPath, {
3777
+ lookup_mode: 'multi'
3778
+ }, {
3779
+ isShopApi: true,
3780
+ customToast: () => {}
3781
+ });
3782
+ const fresh = response?.data ?? response;
3783
+ if (!fresh || typeof fresh !== 'object' || fresh.order_id == null) {
3784
+ this.logInfo('handleOrderSalesDetail: 后端返回订单为空', {
3785
+ lookup,
3786
+ backendPath
3787
+ });
3788
+ return {
3789
+ code: 500,
3790
+ status: false,
3791
+ message: '后端返回订单为空',
3792
+ data: null
3793
+ };
3794
+ }
3795
+ await this.order.upsertOrdersFromRemote([fresh]);
3796
+ const savedOrder = await this.order.getLocalOrderByLookup(lookup);
3797
+ if (!savedOrder) {
3798
+ this.logError('handleOrderSalesDetail: 订单写入后回读失败', {
3799
+ lookup
3800
+ });
3801
+ return {
3802
+ code: 500,
3803
+ status: false,
3804
+ message: '订单写入后回读失败',
3805
+ data: null
3806
+ };
3807
+ }
3808
+ this.logInfo('handleOrderSalesDetail: 远端订单已同步到本地', {
3809
+ lookup
3810
+ });
3811
+ const enrichedOrder = await this.withSalesDetailProductSnapshots(savedOrder);
3812
+ return {
3813
+ code: 200,
3814
+ status: true,
3815
+ message: '',
3816
+ data: enrichedOrder
3817
+ };
3818
+ } catch (error) {
3819
+ const errorMessage = error instanceof Error ? error.message : String(error);
3820
+ this.logInfo('handleOrderSalesDetail: 请求失败', {
3821
+ lookup,
3822
+ backendPath,
3823
+ error: errorMessage
3824
+ });
3825
+ return {
3826
+ code: 500,
3827
+ status: false,
3828
+ message: errorMessage,
3829
+ data: null
3830
+ };
3831
+ }
3832
+ };
3833
+
3834
+ /**
3835
+ * 解析 order_ids 入参,兼容 url query / data 字段、字符串(csv) / 数组两种形态。
3836
+ * 自动 trim、去空、按字符串去重,输出归一化后的字符串数组。
3837
+ *
3838
+ * @example
3839
+ * // url: /update/localOrder?order_ids=1,2,2,, → ['1','2']
3840
+ * this.parseOrderIdsParam(url, undefined)
3841
+ *
3842
+ * @example
3843
+ * // data: { order_ids: [1, '2', null] } → ['1','2']
3844
+ * this.parseOrderIdsParam('', { order_ids: [1, '2', null] })
3845
+ */
3846
+ parseOrderIdsParam(url, data) {
3847
+ const collected = [];
3848
+ const pushRaw = raw => {
3849
+ if (raw === undefined || raw === null) return;
3850
+ if (Array.isArray(raw)) {
3851
+ raw.forEach(pushRaw);
3852
+ return;
3853
+ }
3854
+ const str = String(raw).trim();
3855
+ if (!str) return;
3856
+ // csv 兼容:单个字符串值里也允许逗号分隔
3857
+ str.split(',').forEach(piece => {
3858
+ const v = piece.trim();
3859
+ if (v) collected.push(v);
3860
+ });
3861
+ };
3862
+ if (data && typeof data === 'object') {
3863
+ pushRaw(data.order_ids);
3864
+ }
3865
+ pushRaw(this.extractQueryParam(url, 'order_ids'));
3866
+ const seen = new Set();
3867
+ const result = [];
3868
+ for (const id of collected) {
3869
+ if (seen.has(id)) continue;
3870
+ seen.add(id);
3871
+ result.push(id);
3872
+ }
3873
+ return result;
3874
+ }
3875
+
3876
+ /**
3877
+ * 批量代理刷新 / 合并订单到本地:并发拉详情 → 单次合并 → 单次 emit。
3878
+ * - 单笔失败不影响其他成功项
3879
+ * - 没有任何成功项时不触发 emit / 落库
3880
+ *
3881
+ * @example
3882
+ * await this.handleUpdateLocalOrdersBatch(['96971', '96972'])
3883
+ */
3884
+ async handleUpdateLocalOrdersBatch(orderIds) {
3885
+ this.logInfo('handleUpdateLocalOrdersBatch: 开始处理', {
3886
+ count: orderIds.length,
3887
+ orderIds
3888
+ });
3889
+ if (!this.order) {
3890
+ this.logError('handleUpdateLocalOrdersBatch: Order 模块未注册');
3891
+ return {
3892
+ code: 500,
3893
+ status: false,
3894
+ message: 'Order 模块未注册',
3895
+ data: null
3896
+ };
3897
+ }
3898
+ if (!this.app?.request) {
3899
+ this.logError('handleUpdateLocalOrdersBatch: app.request 不可用');
3900
+ return {
3901
+ code: 500,
3902
+ status: false,
3903
+ message: 'app.request 不可用',
3904
+ data: null
3905
+ };
3906
+ }
3907
+
3908
+ // 拉取前先记录"本地是否已存在",用于区分 overwritten / inserted
3909
+ const existedBefore = new Map();
3910
+ for (const id of orderIds) {
3911
+ existedBefore.set(id, !!this.order.getOrderByOrderId(id));
3912
+ }
3913
+
3914
+ // 走 orderDataSource 的批量 HTTP 通道,一次请求拉回多笔详情
3915
+ let fetched = [];
3916
+ try {
3917
+ fetched = await this.order.fetchOrdersByHttp(orderIds);
3918
+ } catch (error) {
3919
+ const message = error instanceof Error ? error.message : String(error);
3920
+ this.logError('handleUpdateLocalOrdersBatch: 批量拉取失败', {
3921
+ message
3922
+ });
3923
+ return {
3924
+ code: 500,
3925
+ status: false,
3926
+ message,
3927
+ data: null
3928
+ };
3929
+ }
3930
+
3931
+ // 按返回结果中的 order_id 建索引,未命中即视为单笔失败
3932
+ const fetchedMap = new Map();
3933
+ for (const order of fetched) {
3934
+ const oid = order?.order_id;
3935
+ if (oid === undefined || oid === null) continue;
3936
+ fetchedMap.set(String(oid), order);
3937
+ }
3938
+ const freshOrders = [];
3939
+ const succeedIds = [];
3940
+ const failed = [];
3941
+ for (const id of orderIds) {
3942
+ const fresh = fetchedMap.get(id);
3943
+ if (fresh) {
3944
+ freshOrders.push(fresh);
3945
+ succeedIds.push(id);
3946
+ } else {
3947
+ failed.push({
3948
+ order_id: id,
3949
+ message: '后端未返回该订单详情'
3950
+ });
3951
+ }
3952
+ }
3953
+ if (freshOrders.length > 0) {
3954
+ try {
3955
+ // 单次合并:内部完成 store 更新 + SQLite 落库 + 仅 emit 一次 onOrdersChanged
3956
+ await this.order.upsertOrdersFromRemote(freshOrders);
3957
+ } catch (error) {
3958
+ const message = error instanceof Error ? error.message : String(error);
3959
+ this.logError('handleUpdateLocalOrdersBatch: 合并写入失败', {
3960
+ message
3961
+ });
3962
+ return {
3963
+ code: 500,
3964
+ status: false,
3965
+ message,
3966
+ data: null
3967
+ };
3968
+ }
3969
+ }
3970
+ const overwritten = succeedIds.filter(id => existedBefore.get(id));
3971
+ const inserted = succeedIds.filter(id => !existedBefore.get(id));
3972
+ this.logInfo('handleUpdateLocalOrdersBatch: 完成', {
3973
+ total: orderIds.length,
3974
+ overwrittenCount: overwritten.length,
3975
+ insertedCount: inserted.length,
3976
+ failedCount: failed.length
3977
+ });
3978
+ return {
3979
+ code: 200,
3980
+ status: true,
3981
+ message: '',
3982
+ data: {
3983
+ overwritten,
3984
+ inserted,
3985
+ failed
3986
+ }
3987
+ };
3988
+ }
3989
+
3990
+ /**
3991
+ * 从 url 中解析指定 query 参数,兼容相对路径与绝对路径
3992
+ */
3993
+ extractQueryParam(url, key) {
3994
+ if (!url) return null;
3995
+ try {
3996
+ const target = url.startsWith('http') ? new URL(url) : new URL(url, 'http://placeholder.local');
3997
+ const value = target.searchParams.get(key);
3998
+ return value ?? null;
3999
+ } catch {
4000
+ const queryIndex = url.indexOf('?');
4001
+ if (queryIndex < 0) return null;
4002
+ const query = url.slice(queryIndex + 1);
4003
+ for (const pair of query.split('&')) {
4004
+ if (!pair) continue;
4005
+ const [rawKey, rawValue = ''] = pair.split('=');
4006
+ try {
4007
+ if (decodeURIComponent(rawKey) === key) {
4008
+ return decodeURIComponent(rawValue);
4009
+ }
4010
+ } catch {
4011
+ if (rawKey === key) return rawValue;
4012
+ }
4013
+ }
4014
+ return null;
4015
+ }
4016
+ }
4017
+ extractOrderSalesDetailLookup(url) {
4018
+ const path = this.parseRequestPath(url);
4019
+ const match = path.match(/^\/shop\/order\/sales\/([^/?#]+)$/);
4020
+ return match?.[1] ? decodeURIComponent(match[1]) : null;
4021
+ }
4022
+ shouldForceRemoteSalesDetail(data) {
4023
+ return data?.forceRemote === true || data?.force_remote === true;
4024
+ }
4025
+ buildOrderSalesDetailBackendPath(lookup, data) {
4026
+ const rawWith = data && typeof data === 'object' ? data.with : undefined;
4027
+ const withFields = Array.isArray(rawWith) && rawWith.length > 0 ? rawWith : ['products', 'scheduleEvents'];
4028
+ const searchParams = new URLSearchParams();
4029
+ withFields.forEach(field => {
4030
+ if (field === undefined || field === null || field === '') return;
4031
+ searchParams.append('with[]', String(field));
4032
+ });
4033
+ searchParams.append('lookup_mode', 'multi');
4034
+ const query = searchParams.toString();
4035
+ return `/shop/order/sales/${encodeURIComponent(lookup)}${query ? `?${query}` : ''}`;
4036
+ }
4037
+ extractOrderDataFromCheckoutResponse(response) {
4038
+ const candidates = [response?.data?.order, response?.data?.sales, response?.data, response?.order, response?.sales, response];
4039
+ for (const candidate of candidates) {
4040
+ if (!candidate || typeof candidate !== 'object') continue;
4041
+ if (candidate.order_id !== undefined && candidate.order_id !== null) {
4042
+ return candidate;
4043
+ }
4044
+ }
4045
+ return null;
4046
+ }
4047
+ getCheckoutExternalSaleNumber(data, payload) {
4048
+ const candidates = [payload?.external_sale_number, data?.external_sale_number, payload?.data?.external_sale_number];
4049
+ const value = candidates.find(candidate => candidate !== undefined && candidate !== null && candidate !== '');
4050
+ return value === undefined ? undefined : String(value);
4051
+ }
4052
+ buildCheckoutSyncSuccessPatch(fresh) {
4053
+ if (!fresh) return {};
4054
+ const patch = {};
4055
+ if (fresh.order_id !== undefined && fresh.order_id !== null) patch.order_id = fresh.order_id;
4056
+ if (fresh.order_number !== undefined && fresh.order_number !== null) {
4057
+ patch.order_number = fresh.order_number;
4058
+ }
4059
+ return patch;
4060
+ }
4061
+ buildSyncedOrderForPrint(params) {
4062
+ const {
4063
+ syncedOrder,
4064
+ checkoutResponseOrder
4065
+ } = params;
4066
+ if (!checkoutResponseOrder || typeof checkoutResponseOrder !== 'object') return syncedOrder;
4067
+ const printableOrder = {
4068
+ ...syncedOrder
4069
+ };
4070
+ const responseOrder = checkoutResponseOrder;
4071
+ if (responseOrder.order_id !== undefined && responseOrder.order_id !== null) {
4072
+ printableOrder.order_id = responseOrder.order_id;
4073
+ }
4074
+ if (responseOrder.payment_status !== undefined && responseOrder.payment_status !== null) {
4075
+ printableOrder.payment_status = responseOrder.payment_status;
4076
+ }
4077
+ if (responseOrder.expect_amount !== undefined && responseOrder.expect_amount !== null) {
4078
+ printableOrder.expect_amount = responseOrder.expect_amount;
4079
+ }
4080
+ if (responseOrder.machine_code_print_info_v2 !== undefined) {
4081
+ printableOrder.machine_code_print_info_v2 = responseOrder.machine_code_print_info_v2;
4082
+ }
4083
+ if (responseOrder.payment_info && typeof responseOrder.payment_info === 'object') {
4084
+ printableOrder.payment_info = {
4085
+ ...(syncedOrder.payment_info || {}),
4086
+ ...responseOrder.payment_info
4087
+ };
4088
+ }
4089
+ return printableOrder;
4090
+ }
4091
+ isMergeableCheckoutOrder(order, externalSaleNumber) {
4092
+ if (!order || typeof order !== 'object') return false;
4093
+ const orderExternalSaleNumber = order.external_sale_number;
4094
+ if (externalSaleNumber && orderExternalSaleNumber !== undefined && orderExternalSaleNumber !== null && String(orderExternalSaleNumber) !== externalSaleNumber) {
4095
+ return false;
4096
+ }
4097
+ if (externalSaleNumber && (orderExternalSaleNumber === undefined || orderExternalSaleNumber === null || orderExternalSaleNumber === '')) {
4098
+ return false;
4099
+ }
4100
+ return Array.isArray(order.products) || Array.isArray(order.bookings) || Array.isArray(order.payments) || !!order.summary || order.order_number !== undefined || order.payment_status !== undefined || order.status !== undefined;
4101
+ }
4102
+ normalizeCheckoutResponse(response) {
4103
+ if (response && typeof response === 'object' && (response.code !== undefined || response.status !== undefined)) {
4104
+ return response;
4105
+ }
4106
+ return {
4107
+ code: 200,
4108
+ status: true,
4109
+ message: '',
4110
+ data: response
4111
+ };
4112
+ }
4113
+ withSyncedOrderIdInCheckoutResponse(response, syncedOrder) {
4114
+ const normalized = this.normalizeCheckoutResponse(response);
4115
+ const orderId = syncedOrder?.order_id;
4116
+ if (orderId === undefined || orderId === null) return normalized;
4117
+ if (!normalized?.data || typeof normalized.data !== 'object') return normalized;
4118
+ return {
4119
+ ...normalized,
4120
+ data: {
4121
+ ...normalized.data,
4122
+ order_id: normalized.data.order_id ?? orderId
4123
+ }
4124
+ };
4125
+ }
4126
+ isCheckoutResponseRejected(response) {
4127
+ const normalized = this.normalizeCheckoutResponse(response);
4128
+ const isErrorCode = normalized?.code !== undefined && normalized.code !== 200 && normalized.code !== '200';
4129
+ const isErrorStatus = normalized?.status === false || typeof normalized?.status === 'number' && normalized.status >= 400;
4130
+ const isErrorSuccess = normalized?.success === false;
4131
+ return isErrorCode || isErrorStatus || isErrorSuccess;
4132
+ }
4133
+ getUnknownErrorMessage(error) {
4134
+ if (error instanceof Error && error.message) return error.message;
4135
+ if (typeof error === 'string') return error;
4136
+ const candidates = [error?.message, error?.response?.data?.message, error?.data?.message, error?.response?.body?.message, error?.body?.message];
4137
+ const message = candidates.find(candidate => typeof candidate === 'string' && candidate !== '');
4138
+ if (message) return message;
4139
+ const normalized = this.normalizeUnknownError(error);
4140
+ if (typeof normalized === 'string') return normalized;
4141
+ try {
4142
+ return JSON.stringify(normalized);
4143
+ } catch {
4144
+ return String(error);
4145
+ }
4146
+ }
4147
+ normalizeUnknownError(error) {
4148
+ if (error instanceof Error) {
4149
+ const normalized = {
4150
+ name: error.name,
4151
+ message: error.message,
4152
+ stack: error.stack
4153
+ };
4154
+ const record = error;
4155
+ if (record.response?.data !== undefined) normalized.responseData = record.response.data;
4156
+ if (record.data !== undefined) normalized.data = record.data;
4157
+ if (record.response?.body !== undefined) normalized.responseBody = record.response.body;
4158
+ if (record.body !== undefined) normalized.body = record.body;
4159
+ return this.safeJsonClone(normalized);
4160
+ }
4161
+ if (!error || typeof error !== 'object') return String(error);
4162
+ return this.safeJsonClone(error);
4163
+ }
4164
+ safeJsonClone(value) {
4165
+ const seen = new WeakSet();
4166
+ try {
4167
+ return JSON.parse(JSON.stringify(value, (_key, current) => {
4168
+ if (!current || typeof current !== 'object') return current;
4169
+ if (seen.has(current)) return '[Circular]';
4170
+ seen.add(current);
4171
+ return current;
4172
+ }));
4173
+ } catch {
4174
+ return String(value);
4175
+ }
4176
+ }
4177
+ extractBackendErrorResponse(error) {
4178
+ const candidates = [error?.response?.data, error?.data, error?.response?.body, error?.body];
4179
+ return candidates.find(candidate => this.isExplicitBackendErrorResponse(candidate)) || null;
4180
+ }
4181
+ isExplicitBackendErrorResponse(candidate) {
4182
+ if (!candidate || typeof candidate !== 'object') return false;
4183
+ return candidate.code !== undefined || candidate.status !== undefined || typeof candidate.message === 'string';
4184
+ }
4185
+
4186
+ /**
4187
+ * 从 url 或路由 path 解析 pathname(不含 query,去掉末尾 /)
4188
+ */
4189
+ parseRequestPath(url, routePath) {
4190
+ const raw = routePath || url;
4191
+ const noQuery = raw.split('?')[0] || '';
4192
+ try {
4193
+ const pathname = url.startsWith('http') ? new URL(url).pathname : noQuery;
4194
+ return pathname.replace(/\/$/, '') || pathname;
4195
+ } catch {
4196
+ return noQuery.replace(/\/$/, '');
4197
+ }
4198
+ }
4199
+
4200
+ /**
4201
+ * 解析平面图 GET 路径为查询上下文
4202
+ */
4203
+ resolveFloorPlanQueryContext(path, base) {
4204
+ if (path === base) {
4205
+ return {
4206
+ kind: 'list'
4207
+ };
4208
+ }
4209
+ if (!path.startsWith(`${base}/`)) {
4210
+ return null;
4211
+ }
4212
+ const after = path.slice(base.length + 1);
4213
+ if (after === 'unsubscribe') {
4214
+ return null;
4215
+ }
4216
+ if (after.startsWith('code/')) {
4217
+ const code = decodeURIComponent(after.slice('code/'.length));
4218
+ if (!code) return null;
4219
+ return {
4220
+ kind: 'code',
4221
+ code
4222
+ };
4223
+ }
4224
+ if (/^\d+$/.test(after)) {
4225
+ return {
4226
+ kind: 'id',
4227
+ id: after
4228
+ };
4229
+ }
4230
+ return null;
4231
+ }
4232
+
4233
+ /**
4234
+ * GET /shop/schedule/floor-plan* 前缀路由:读本地 store;支持 subscriberId + callback 订阅更新
4235
+ */
4236
+ handleFloorPlanGet = async ({
4237
+ url,
4238
+ data,
4239
+ config,
4240
+ path: routePath
4241
+ }) => {
4242
+ const base = '/shop/schedule/floor-plan';
4243
+ const path = this.parseRequestPath(url, routePath);
4244
+ if (path === `${base}/unsubscribe` || path === `${base}/code/unsubscribe`) {
4245
+ const subscriberId = data?.subscriberId;
4246
+ if (subscriberId) {
4247
+ this.floorPlanQuerySubscribers.delete(subscriberId);
4248
+ this.logInfo('handleFloorPlanGet: 已取消订阅', {
4249
+ subscriberId,
4250
+ remaining: this.floorPlanQuerySubscribers.size
4251
+ });
4252
+ }
4253
+ return {
4254
+ code: 200,
4255
+ message: 'ok',
4256
+ status: true
4257
+ };
4258
+ }
4259
+ const {
4260
+ callback,
4261
+ subscriberId
4262
+ } = config || {};
4263
+ const ctx = this.resolveFloorPlanQueryContext(path, base);
4264
+ if (!ctx) {
4265
+ return {
4266
+ status: false,
4267
+ code: 404,
4268
+ message: 'Not Found',
4269
+ data: null
4270
+ };
4271
+ }
4272
+ if (subscriberId && typeof callback === 'function') {
4273
+ this.floorPlanQuerySubscribers.set(subscriberId, {
4274
+ callback,
4275
+ context: ctx
4276
+ });
4277
+ this.logInfo('handleFloorPlanGet: 已注册订阅者', {
4278
+ subscriberId,
4279
+ kind: ctx.kind,
4280
+ total: this.floorPlanQuerySubscribers.size
855
4281
  });
4282
+ }
4283
+ return this.computeFloorPlanQueryResult(ctx);
4284
+ };
4285
+ computeFloorPlanQueryResult(context) {
4286
+ if (!this.floor_plan) {
4287
+ return {
4288
+ status: false,
4289
+ code: 500,
4290
+ message: 'Floor plan 模块未注册',
4291
+ data: null
4292
+ };
4293
+ }
4294
+ if (context.kind === 'list') {
856
4295
  return {
4296
+ status: true,
857
4297
  code: 200,
858
- data: {
859
- ...response.data,
860
- list: list?.list || []
861
- },
862
4298
  message: '',
863
- status: true
4299
+ data: this.floor_plan.getSortedList()
864
4300
  };
865
- } catch (error) {
866
- const errorMessage = error instanceof Error ? error.message : String(error);
867
- this.logError('fetchBookingListFromAPI: 请求失败', {
868
- error: errorMessage
869
- });
4301
+ }
4302
+ if (context.kind === 'id') {
4303
+ const item = this.floor_plan.getById(context.id);
4304
+ if (!item) {
4305
+ return {
4306
+ status: false,
4307
+ code: 404,
4308
+ message: 'Not Found',
4309
+ data: null
4310
+ };
4311
+ }
870
4312
  return {
871
- code: 500,
872
- message: errorMessage,
873
- data: {
874
- list: [],
875
- count: 0
876
- },
877
- status: false
4313
+ status: true,
4314
+ code: 200,
4315
+ message: '',
4316
+ data: item
878
4317
  };
879
4318
  }
4319
+ const item = this.floor_plan.getByCode(context.code);
4320
+ if (!item) {
4321
+ return {
4322
+ status: false,
4323
+ code: 404,
4324
+ message: 'Not Found',
4325
+ data: null
4326
+ };
4327
+ }
4328
+ return {
4329
+ status: true,
4330
+ code: 200,
4331
+ message: '',
4332
+ data: item
4333
+ };
880
4334
  }
881
4335
 
882
4336
  /**
883
- * 处理预约列表查询
884
- * 今天:注册订阅者 + 本地数据筛选;非今天:清理订阅者 + 走真实 API
4337
+ * 平面图数据变更后向所有 GET 订阅者推送最新结果
885
4338
  */
886
- handleBookingList = async ({
887
- url,
888
- method,
889
- data,
890
- config
891
- }) => {
892
- const queryPayload = data && typeof data === 'object' ? {
893
- ...data
894
- } : {};
895
- const {
896
- callback,
897
- subscriberId
898
- } = config || {};
899
- const isToday = this.isBookingQueryForToday(queryPayload);
900
- this.logInfo('handleBookingList: 开始处理预约列表请求', {
901
- data: queryPayload,
902
- isToday
4339
+ async recomputeAndNotifyFloorPlanQuery() {
4340
+ if (this.floorPlanQuerySubscribers.size === 0) return;
4341
+ this.logInfo('recomputeAndNotifyFloorPlanQuery: 开始推送', {
4342
+ subscriberCount: this.floorPlanQuerySubscribers.size
903
4343
  });
904
- if (isToday) {
905
- if (subscriberId && typeof callback === 'function') {
906
- this.bookingQuerySubscribers.set(subscriberId, {
907
- callback,
908
- context: queryPayload
909
- });
910
- this.logInfo('handleBookingList: 已注册订阅者(今天)', {
911
- subscriberId,
912
- totalSubscribers: this.bookingQuerySubscribers.size
4344
+ for (const [subscriberId, sub] of this.floorPlanQuerySubscribers.entries()) {
4345
+ try {
4346
+ const result = this.computeFloorPlanQueryResult(sub.context);
4347
+ sub.callback(result);
4348
+ this.logInfo('recomputeAndNotifyFloorPlanQuery: 已推送', {
4349
+ subscriberId
913
4350
  });
914
- }
915
- return this.computeBookingQueryResult(queryPayload);
916
- } else {
917
- if (subscriberId) {
918
- this.bookingQuerySubscribers.delete(subscriberId);
919
- this.logInfo('handleBookingList: 已清理订阅者(非今天)', {
4351
+ } catch (error) {
4352
+ const errorMessage = error instanceof Error ? error.message : String(error);
4353
+ this.logError('recomputeAndNotifyFloorPlanQuery: 推送失败', {
920
4354
  subscriberId,
921
- remaining: this.bookingQuerySubscribers.size
4355
+ error: errorMessage
922
4356
  });
923
4357
  }
924
- return this.fetchBookingListFromAPI(queryPayload);
925
4358
  }
926
- };
4359
+ }
927
4360
 
928
4361
  /**
929
- * 处理资源列表查询
930
- * 转发到资源模块去
4362
+ * 取消平面图 GET 订阅(也可走 GET .../unsubscribe + subscriberId)
931
4363
  */
932
- handleResourceList = async ({
933
- url,
934
- method,
935
- data,
936
- config
937
- }) => {
938
- const list = this.resource?.getResources({
939
- skip: data?.skip || 1,
940
- num: data?.num || 10
941
- });
942
- return {
943
- code: 200,
944
- data: {
945
- list,
946
- count: list?.length || 0
947
- },
948
- message: '',
949
- status: true
950
- };
951
- };
4364
+ removeFloorPlanQuerySubscriber(subscriberId) {
4365
+ if (subscriberId) {
4366
+ this.floorPlanQuerySubscribers.delete(subscriberId);
4367
+ this.logInfo('removeFloorPlanQuerySubscriber: 已移除', {
4368
+ subscriberId,
4369
+ remaining: this.floorPlanQuerySubscribers.size
4370
+ });
4371
+ }
4372
+ }
952
4373
 
953
4374
  /**
954
4375
  * 取消预约列表查询订阅(HTTP 路由入口)
@@ -961,9 +4382,11 @@ class Server {
961
4382
  } = data || {};
962
4383
  if (subscriberId) {
963
4384
  this.bookingQuerySubscribers.delete(subscriberId);
4385
+ this.bookingRemoteQuerySubscribers.delete(subscriberId);
964
4386
  this.logInfo('handleUnsubscribeBookingQuery: 已移除订阅者', {
965
4387
  subscriberId,
966
- remaining: this.bookingQuerySubscribers.size
4388
+ remainingLocal: this.bookingQuerySubscribers.size,
4389
+ remainingRemote: this.bookingRemoteQuerySubscribers.size
967
4390
  });
968
4391
  }
969
4392
  return {
@@ -1004,7 +4427,8 @@ class Server {
1004
4427
  rawCount: rawList.length,
1005
4428
  filteredCount: result.count,
1006
4429
  size: result.size,
1007
- skip: result.skip
4430
+ skip: result.skip,
4431
+ rejected: result.rejected
1008
4432
  });
1009
4433
  return {
1010
4434
  code: 200,
@@ -1048,6 +4472,39 @@ class Server {
1048
4472
  };
1049
4473
  }
1050
4474
 
4475
+ /**
4476
+ * 规范商品扩展类型过滤参数。
4477
+ *
4478
+ * @example
4479
+ * const types = this.normalizeProductExtensionTypes(['product_add_time']);
4480
+ */
4481
+ normalizeProductExtensionTypes(extension_type) {
4482
+ if (!Array.isArray(extension_type)) return [];
4483
+ return extension_type.map(item => String(item || '').trim()).filter(Boolean);
4484
+ }
4485
+
4486
+ /**
4487
+ * 请求包含 product_add_time 时,按传入 extension_type 数组配置过滤商品。
4488
+ *
4489
+ * @example
4490
+ * const list = this.filterProductsByExtensionTypes(products, ['product_add_time']);
4491
+ */
4492
+ filterProductsByExtensionTypes(products, extension_type) {
4493
+ const extensionTypes = this.normalizeProductExtensionTypes(extension_type);
4494
+ if (!extensionTypes.includes('product_add_time')) return products;
4495
+ return products.filter(product => extensionTypes.includes(String(product?.extension_type || '')));
4496
+ }
4497
+
4498
+ /**
4499
+ * 判断本次查询是否需要跳过餐牌过滤,直接按扩展类型拉商品。
4500
+ *
4501
+ * @example
4502
+ * const skipMenu = this.shouldQueryProductsByExtensionTypes(['product_add_time']);
4503
+ */
4504
+ shouldQueryProductsByExtensionTypes(extension_type) {
4505
+ return this.normalizeProductExtensionTypes(extension_type).includes('product_add_time');
4506
+ }
4507
+
1051
4508
  /**
1052
4509
  * 商品查询的核心计算逻辑(编排 Products、Menu、Schedule 模块)
1053
4510
  * 供 handleProductQuery 首次返回及 pubsub 变更推送复用
@@ -1059,71 +4516,189 @@ class Server {
1059
4516
  const tTotal = performance.now();
1060
4517
  const {
1061
4518
  menu_list_ids,
4519
+ ids,
1062
4520
  schedule_date,
1063
- schedule_datetime
4521
+ schedule_datetime,
4522
+ customer_id,
4523
+ extension_type,
4524
+ product_id,
4525
+ strategy_context
1064
4526
  } = context;
4527
+ const queryIds = Array.isArray(ids) ? ids.map(id => Number(id)).filter(id => Number.isFinite(id)) : [];
4528
+ const formatterContext = this.buildProductFormatterContext({
4529
+ schedule_date,
4530
+ schedule_datetime,
4531
+ customer_id,
4532
+ strategy_context
4533
+ });
1065
4534
  this.logInfo('computeProductQueryResult 开始', {
1066
4535
  menuListIdsCount: menu_list_ids?.length ?? 0,
4536
+ ids: queryIds,
1067
4537
  schedule_datetime,
1068
4538
  schedule_date,
4539
+ customer_id,
4540
+ extension_type,
4541
+ product_id,
4542
+ strategyContextKeys: strategy_context ? Object.keys(strategy_context) : [],
1069
4543
  changedIds: options?.changedIds
1070
4544
  });
1071
4545
  if (!this.products) {
1072
4546
  this.logError('computeProductQueryResult: Products 模块未注册');
1073
4547
  return {
1074
4548
  message: 'Products 模块未注册',
1075
- data: {
1076
- list: [],
1077
- count: 0
1078
- }
4549
+ data: this.buildProductQueryResultPayload([], 0)
4550
+ };
4551
+ }
4552
+ if (queryIds.length > 0) {
4553
+ const uniqueIds = Array.from(new Set(queryIds));
4554
+ const tPrice = performance.now();
4555
+ const productsWithPrice = await this.products.getProductsWithPrice(schedule_date, formatterContext, {
4556
+ changedIds: options?.changedIds,
4557
+ productIds: uniqueIds
4558
+ });
4559
+ (0, _product.perfMark)('computeQuery.getProductsWithPrice(ids)', performance.now() - tPrice, {
4560
+ count: productsWithPrice.length,
4561
+ ids: uniqueIds
4562
+ });
4563
+ const filteredProducts = productsWithPrice.filter(p => uniqueIds.includes(Number(p?.id))).filter(p => (p?.status || 'published') === 'published');
4564
+ const extensionFilteredProducts = this.filterProductsByExtensionTypes(filteredProducts, extension_type);
4565
+ (0, _product.perfMark)('computeProductQueryResult', performance.now() - tTotal, {
4566
+ mode: 'ids',
4567
+ ids: uniqueIds,
4568
+ count: extensionFilteredProducts.length
4569
+ });
4570
+ this.logInfo('computeProductQueryResult 完成(ids)', {
4571
+ ids: uniqueIds,
4572
+ count: extensionFilteredProducts.length
4573
+ });
4574
+ return {
4575
+ code: 200,
4576
+ data: this.buildProductQueryResultPayload(extensionFilteredProducts),
4577
+ message: '',
4578
+ status: true
4579
+ };
4580
+ }
4581
+
4582
+ // 单商品:复用 getProductsWithPrice + 发布状态过滤,不要求 Menu/Schedule(schedule 仍传入格式化上下文)
4583
+ if (product_id != null && Number.isFinite(Number(product_id))) {
4584
+ const pid = Number(product_id);
4585
+ const tPrice = performance.now();
4586
+ const allProductsWithPrice = await this.products.getProductsWithPrice(schedule_date, formatterContext, {
4587
+ changedIds: options?.changedIds
4588
+ });
4589
+ (0, _product.perfMark)('computeQuery.getProductsWithPrice(single)', performance.now() - tPrice, {
4590
+ count: allProductsWithPrice.length,
4591
+ productId: pid
4592
+ });
4593
+ const published = this.filterProductsByExtensionTypes(allProductsWithPrice.filter(p => (p?.status || 'published') === 'published'), extension_type);
4594
+ const item = published.find(p => Number(p.id) === pid) ?? null;
4595
+ (0, _product.perfMark)('computeProductQueryResult', performance.now() - tTotal, {
4596
+ mode: 'single',
4597
+ productId: pid,
4598
+ found: !!item
4599
+ });
4600
+ this.logInfo('computeProductQueryResult 完成(单商品)', {
4601
+ productId: pid,
4602
+ found: !!item
4603
+ });
4604
+ return {
4605
+ code: 200,
4606
+ data: item,
4607
+ message: item ? '' : '商品不存在或未发布',
4608
+ status: true
4609
+ };
4610
+ }
4611
+ if (this.shouldQueryProductsByExtensionTypes(extension_type)) {
4612
+ const tPrice = performance.now();
4613
+ let filteredProducts = await this.products.getProductsWithPrice(schedule_date, formatterContext, {
4614
+ changedIds: options?.changedIds
4615
+ });
4616
+ (0, _product.perfMark)('computeQuery.getProductsWithPrice(extensionType)', performance.now() - tPrice, {
4617
+ count: filteredProducts.length,
4618
+ extension_type
4619
+ });
4620
+ const tStatus = performance.now();
4621
+ const beforeStatusCount = filteredProducts.length;
4622
+ filteredProducts = filteredProducts.filter(p => (p?.status || 'published') === 'published');
4623
+ (0, _product.perfMark)('computeQuery.filterByStatus(extensionType)', performance.now() - tStatus, {
4624
+ before: beforeStatusCount,
4625
+ after: filteredProducts.length
4626
+ });
4627
+ const tExtensionType = performance.now();
4628
+ const beforeExtensionTypeCount = filteredProducts.length;
4629
+ filteredProducts = this.filterProductsByExtensionTypes(filteredProducts, extension_type);
4630
+ (0, _product.perfMark)('computeQuery.filterByExtensionType(extensionType)', performance.now() - tExtensionType, {
4631
+ before: beforeExtensionTypeCount,
4632
+ after: filteredProducts.length,
4633
+ extension_type
4634
+ });
4635
+ const tSort = performance.now();
4636
+ filteredProducts = filteredProducts.sort((a, b) => {
4637
+ const sortDiff = Number(b.sort) - Number(a.sort);
4638
+ if (sortDiff !== 0) return sortDiff;
4639
+ return (a.title || '').localeCompare(b.title || '');
4640
+ });
4641
+ (0, _product.perfMark)('computeQuery.sort(extensionType)', performance.now() - tSort, {
4642
+ count: filteredProducts.length
4643
+ });
4644
+ (0, _product.perfMark)('computeProductQueryResult', performance.now() - tTotal, {
4645
+ mode: 'extensionType',
4646
+ filteredCount: filteredProducts.length
4647
+ });
4648
+ this.logInfo('computeProductQueryResult 完成(extension_type)', {
4649
+ filteredCount: filteredProducts.length,
4650
+ extension_type
4651
+ });
4652
+ return {
4653
+ code: 200,
4654
+ data: this.buildProductQueryResultPayload(filteredProducts),
4655
+ message: '',
4656
+ status: true
1079
4657
  };
1080
4658
  }
1081
4659
  if (!this.menu) {
1082
4660
  this.logError('computeProductQueryResult: Menu 模块未注册');
1083
4661
  return {
1084
4662
  message: 'Menu 模块未注册',
1085
- data: {
1086
- list: [],
1087
- count: 0
1088
- }
4663
+ data: this.buildProductQueryResultPayload([], 0)
1089
4664
  };
1090
4665
  }
1091
4666
  if (!this.schedule) {
1092
4667
  this.logError('computeProductQueryResult: Schedule 模块未注册');
1093
4668
  return {
1094
4669
  message: 'Schedule 模块未注册',
1095
- data: {
1096
- list: [],
1097
- count: 0
1098
- }
4670
+ data: this.buildProductQueryResultPayload([], 0)
1099
4671
  };
1100
4672
  }
1101
4673
  let activeMenuList = [];
1102
4674
  if (menu_list_ids && Array.isArray(menu_list_ids) && menu_list_ids.length > 0) {
1103
4675
  const tMenu = performance.now();
1104
- const menuList = this.menu.getMenuByIds(menu_list_ids);
1105
- activeMenuList = menuList.filter(menu => {
1106
- return this.schedule?.getDateIsInSchedule(schedule_datetime, menu.schedule) || false;
1107
- });
4676
+ const getActiveMenus = () => this.menu.getMenuByIds(menu_list_ids).filter(menu => this.schedule?.getDateIsInSchedule(schedule_datetime, menu.schedule) || false);
4677
+ activeMenuList = getActiveMenus();
4678
+ if (!activeMenuList.length && (await this.refreshMenuScheduleCacheAfterEmptyResult())) {
4679
+ activeMenuList = getActiveMenus();
4680
+ }
1108
4681
  (0, _product.perfMark)('computeQuery.filterActiveMenu', performance.now() - tMenu, {
1109
- totalMenu: menuList.length,
4682
+ totalMenu: menu_list_ids.length,
1110
4683
  activeMenu: activeMenuList.length
1111
4684
  });
1112
4685
  }
1113
- const tPrice = performance.now();
1114
- const allProductsWithPrice = await this.products.getProductsWithPrice(schedule_date, {
1115
- scheduleModule: this.getSchedule()
1116
- }, {
1117
- changedIds: options?.changedIds
1118
- });
1119
- (0, _product.perfMark)('computeQuery.getProductsWithPrice', performance.now() - tPrice, {
1120
- count: allProductsWithPrice.length
1121
- });
1122
4686
  const tFilter = performance.now();
1123
- let filteredProducts = this.filterProductsByMenuConfig(allProductsWithPrice, activeMenuList);
4687
+ const productScope = this.getProductScopeByMenuConfig(activeMenuList);
4688
+ const scopedProductIds = this.products.getProductIdsByScope(productScope);
1124
4689
  (0, _product.perfMark)('computeQuery.filterByMenu', performance.now() - tFilter, {
1125
- before: allProductsWithPrice.length,
1126
- after: filteredProducts.length
4690
+ scopedProductCount: scopedProductIds.length,
4691
+ activeMenuCount: activeMenuList.length,
4692
+ isAllProducts: !!productScope.isAllProducts
4693
+ });
4694
+ const tPrice = performance.now();
4695
+ let filteredProducts = scopedProductIds.length > 0 ? await this.products.getProductsWithPrice(schedule_date, formatterContext, {
4696
+ changedIds: options?.changedIds,
4697
+ productIds: productScope.isAllProducts ? undefined : scopedProductIds
4698
+ }) : [];
4699
+ (0, _product.perfMark)('computeQuery.getProductsWithPrice', performance.now() - tPrice, {
4700
+ count: filteredProducts.length,
4701
+ scopedProductCount: scopedProductIds.length
1127
4702
  });
1128
4703
  const tStatus = performance.now();
1129
4704
  const beforeStatusCount = filteredProducts.length;
@@ -1132,6 +4707,14 @@ class Server {
1132
4707
  before: beforeStatusCount,
1133
4708
  after: filteredProducts.length
1134
4709
  });
4710
+ const tExtensionType = performance.now();
4711
+ const beforeExtensionTypeCount = filteredProducts.length;
4712
+ filteredProducts = this.filterProductsByExtensionTypes(filteredProducts, extension_type);
4713
+ (0, _product.perfMark)('computeQuery.filterByExtensionType', performance.now() - tExtensionType, {
4714
+ before: beforeExtensionTypeCount,
4715
+ after: filteredProducts.length,
4716
+ extension_type
4717
+ });
1135
4718
  const tSort = performance.now();
1136
4719
  filteredProducts = filteredProducts.sort((a, b) => {
1137
4720
  const sortDiff = Number(b.sort) - Number(a.sort);
@@ -1142,26 +4725,41 @@ class Server {
1142
4725
  count: filteredProducts.length
1143
4726
  });
1144
4727
  (0, _product.perfMark)('computeProductQueryResult', performance.now() - tTotal, {
1145
- originalCount: allProductsWithPrice.length,
4728
+ scopedProductCount: scopedProductIds.length,
1146
4729
  filteredCount: filteredProducts.length,
1147
4730
  activeMenuCount: activeMenuList.length
1148
4731
  });
1149
4732
  this.logInfo('computeProductQueryResult 完成', {
1150
- originalCount: allProductsWithPrice.length,
4733
+ scopedProductCount: scopedProductIds.length,
1151
4734
  filteredCount: filteredProducts.length,
1152
4735
  activeMenuCount: activeMenuList.length
1153
4736
  });
1154
4737
  return {
1155
4738
  code: 200,
1156
- data: {
1157
- list: filteredProducts,
1158
- count: filteredProducts.length
1159
- },
4739
+ data: this.buildProductQueryResultPayload(filteredProducts),
1160
4740
  message: '',
1161
4741
  status: true
1162
4742
  };
1163
4743
  }
1164
4744
 
4745
+ /**
4746
+ * 构建商品格式化上下文。
4747
+ *
4748
+ * 智能定价条件使用全量 menuList/scheduleList;Product Query 顶层参数只保留筛选商品集合所需字段。
4749
+ */
4750
+ buildProductFormatterContext(context) {
4751
+ const scheduleModule = this.getSchedule();
4752
+ return {
4753
+ scheduleModule,
4754
+ schedule_datetime: context.schedule_datetime,
4755
+ customer_id: context.customer_id,
4756
+ strategy_context: context.strategy_context,
4757
+ menuList: this.menu?.getMenuList?.() || [],
4758
+ scheduleList: scheduleModule?.getScheduleList?.() || [],
4759
+ dataVariantEvaluator: this.core.context?.dataVariantEvaluator
4760
+ };
4761
+ }
4762
+
1165
4763
  /**
1166
4764
  * 数据变更后,遍历所有订阅者重新计算查询结果并通过 callback 推送
1167
4765
  * 由 ProductsModule 的 onProductsSyncCompleted 事件触发
@@ -1198,15 +4796,24 @@ class Server {
1198
4796
  */
1199
4797
  async recomputeAndNotifyOrderQuery() {
1200
4798
  if (this.orderQuerySubscribers.size === 0) return;
4799
+ const notifyStartAt = Date.now();
1201
4800
  this.logInfo('recomputeAndNotifyOrderQuery: 开始推送', {
1202
- subscriberCount: this.orderQuerySubscribers.size
4801
+ subscriberCount: this.orderQuerySubscribers.size,
4802
+ notifyStartAt: new Date(notifyStartAt).toISOString()
1203
4803
  });
1204
4804
  for (const [subscriberId, subscriber] of this.orderQuerySubscribers.entries()) {
1205
4805
  try {
4806
+ const computeStartAt = Date.now();
1206
4807
  const result = await this.computeOrderQueryResult(subscriber.context);
4808
+ const computeEndAt = Date.now();
4809
+ const callbackStartAt = Date.now();
1207
4810
  subscriber.callback(result);
4811
+ const callbackEndAt = Date.now();
1208
4812
  this.logInfo('recomputeAndNotifyOrderQuery: 已推送', {
1209
- subscriberId
4813
+ subscriberId,
4814
+ computeDurationMs: computeEndAt - computeStartAt,
4815
+ callbackDurationMs: callbackEndAt - callbackStartAt,
4816
+ totalDurationMs: callbackEndAt - computeStartAt
1210
4817
  });
1211
4818
  } catch (error) {
1212
4819
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -1216,6 +4823,114 @@ class Server {
1216
4823
  });
1217
4824
  }
1218
4825
  }
4826
+ const notifyEndAt = Date.now();
4827
+ this.logInfo('recomputeAndNotifyOrderQuery: 推送完成', {
4828
+ subscriberCount: this.orderQuerySubscribers.size,
4829
+ totalNotifyDurationMs: notifyEndAt - notifyStartAt
4830
+ });
4831
+ }
4832
+
4833
+ /**
4834
+ * 订单变更后通知 sales 搜索订阅者。
4835
+ * 仅回传当前查询已返回订单号集合中的变更订单。
4836
+ *
4837
+ * @example
4838
+ * this.notifySalesSearchSubscribers({ list: [], changedOrders: [order], source: 'pubsub.bodyUpsert' })
4839
+ */
4840
+ notifySalesSearchSubscribers(payload) {
4841
+ if (this.salesSearchSubscribers.size === 0) return;
4842
+ const changedOrders = this.extractChangedOrdersFromPayload(payload);
4843
+ if (changedOrders.length === 0) {
4844
+ return;
4845
+ }
4846
+ // 如果当前开启了 checkout 弹窗,先不进行刷新,下面的代码会导致在全局搜索页调用 bookingTicket.destroy()
4847
+ // 导致模块被销毁 ,checkout 弹窗永远无法关闭
4848
+ if (window.isPaymentModalPluginOpen) return;
4849
+ for (const [subscriberId, subscriber] of this.salesSearchSubscribers.entries()) {
4850
+ try {
4851
+ const changedVisibleOrders = changedOrders.filter(order => {
4852
+ const orderId = this.getSalesSearchOrderId(order);
4853
+ return !!orderId && subscriber.visibleOrderIds.has(orderId);
4854
+ });
4855
+ if (changedVisibleOrders.length === 0) {
4856
+ continue;
4857
+ }
4858
+ subscriber.callback(this.buildSalesSearchDeltaResponse(subscriber, changedVisibleOrders));
4859
+ this.logInfo('notifySalesSearchSubscribers: 已推送 delta', {
4860
+ subscriberId,
4861
+ kind: subscriber.kind,
4862
+ count: changedVisibleOrders.length
4863
+ });
4864
+ } catch (error) {
4865
+ const errorMessage = error instanceof Error ? error.message : String(error);
4866
+ this.logError('notifySalesSearchSubscribers: 推送失败', {
4867
+ subscriberId,
4868
+ error: errorMessage
4869
+ });
4870
+ }
4871
+ }
4872
+ }
4873
+
4874
+ /**
4875
+ * 从订单变更事件中提取明确 delta。
4876
+ *
4877
+ * @example
4878
+ * const list = this.extractChangedOrdersFromPayload(payload)
4879
+ */
4880
+ extractChangedOrdersFromPayload(payload) {
4881
+ if (!payload || Array.isArray(payload)) return [];
4882
+ return Array.isArray(payload.changedOrders) ? payload.changedOrders : [];
4883
+ }
4884
+
4885
+ /**
4886
+ * 构建与当前搜索接口一致的 delta 响应。
4887
+ *
4888
+ * @example
4889
+ * const response = this.buildSalesSearchDeltaResponse(subscriber, [order])
4890
+ */
4891
+ buildSalesSearchDeltaResponse(subscriber, changedOrders) {
4892
+ if (subscriber.kind === 'aggregate') {
4893
+ return this.buildAggregateSalesSearchDeltaResponse(subscriber, changedOrders);
4894
+ }
4895
+ const data = subscriber.lastResponseData || {};
4896
+ return {
4897
+ code: 200,
4898
+ status: true,
4899
+ message: '',
4900
+ data: {
4901
+ ...data,
4902
+ list: changedOrders
4903
+ }
4904
+ };
4905
+ }
4906
+
4907
+ /**
4908
+ * 构建聚合搜索接口的 sales 分组 delta 响应。
4909
+ *
4910
+ * @example
4911
+ * const response = this.buildAggregateSalesSearchDeltaResponse(subscriber, [order])
4912
+ */
4913
+ buildAggregateSalesSearchDeltaResponse(subscriber, changedOrders) {
4914
+ const data = subscriber.lastResponseData || {};
4915
+ const results = Array.isArray(data?.results) ? data.results : [];
4916
+ const hasSalesGroup = results.some(item => item?.module === 'sales');
4917
+ const nextResults = hasSalesGroup ? results.map(item => item?.module === 'sales' ? {
4918
+ ...item,
4919
+ list: changedOrders
4920
+ } : item) : [{
4921
+ module: 'sales',
4922
+ list: changedOrders,
4923
+ count: changedOrders.length
4924
+ }];
4925
+ return {
4926
+ code: 200,
4927
+ status: true,
4928
+ message: '',
4929
+ data: {
4930
+ ...data,
4931
+ results: nextResults
4932
+ }
4933
+ };
1219
4934
  }
1220
4935
 
1221
4936
  /**
@@ -1244,31 +4959,29 @@ class Server {
1244
4959
  }
1245
4960
 
1246
4961
  /**
1247
- * 根据餐牌配置过滤商品
1248
- * @param products 所有商品列表
1249
- * @param activeMenuList 生效的餐牌列表
1250
- * @returns 过滤后的商品列表
4962
+ * 根据餐牌配置提取轻量商品范围。
1251
4963
  * @private
1252
4964
  */
1253
- filterProductsByMenuConfig(products, activeMenuList) {
1254
- // 如果没有生效的餐牌,返回空数组
4965
+ getProductScopeByMenuConfig(activeMenuList) {
1255
4966
  if (!activeMenuList || activeMenuList.length === 0) {
1256
- this.logInfo('filterProductsByMenuConfig: 没有生效的餐牌,返回空数组', {
1257
- productCount: products.length,
4967
+ this.logInfo('getProductScopeByMenuConfig: 没有生效的餐牌,返回空数组', {
1258
4968
  activeMenuCount: 0
1259
4969
  });
1260
- return [];
4970
+ return {
4971
+ productIds: [],
4972
+ collectionIds: []
4973
+ };
1261
4974
  }
1262
4975
 
1263
4976
  // 收集允许的商品 IDs 和集合 IDs
1264
- let allowedProductIds = new Set();
1265
- let allowedCollectionIds = new Set();
4977
+ const allowedProductIds = new Set();
4978
+ const allowedCollectionIds = new Set();
1266
4979
  let hasProductAll = false;
1267
4980
  for (const menu of activeMenuList) {
1268
4981
  const config = menu.partyroom_package;
1269
4982
  if (!config) {
1270
4983
  console.warn('[Server] 餐牌缺少 partyroom_package 配置:', menu);
1271
- this.logWarning('filterProductsByMenuConfig: 餐牌缺少 partyroom_package 配置', {
4984
+ this.logWarning('getProductScopeByMenuConfig: 餐牌缺少 partyroom_package 配置', {
1272
4985
  menuId: menu.form_record_id,
1273
4986
  menuName: menu.name
1274
4987
  });
@@ -1277,7 +4990,7 @@ class Server {
1277
4990
 
1278
4991
  // 如果有任何一个餐牌是 product_all,则返回所有商品
1279
4992
  if (config.type === 'product_all') {
1280
- this.logInfo('filterProductsByMenuConfig: 餐牌允许所有商品', {
4993
+ this.logInfo('getProductScopeByMenuConfig: 餐牌允许所有商品', {
1281
4994
  menuId: menu.form_record_id,
1282
4995
  menuName: menu.name,
1283
4996
  configType: config.type
@@ -1290,7 +5003,7 @@ class Server {
1290
5003
  if (config.type === 'products' && Array.isArray(config.products)) {
1291
5004
  const productIds = config.products.map(item => item.product_id);
1292
5005
  productIds.forEach(id => id && allowedProductIds.add(id));
1293
- this.logInfo('filterProductsByMenuConfig: 餐牌允许指定商品', {
5006
+ this.logInfo('getProductScopeByMenuConfig: 餐牌允许指定商品', {
1294
5007
  menuId: menu.form_record_id,
1295
5008
  menuName: menu.name,
1296
5009
  configType: config.type,
@@ -1302,7 +5015,7 @@ class Server {
1302
5015
  if (config.type === 'product_collection' && Array.isArray(config.products)) {
1303
5016
  const collectionIds = config.products.map(item => item.product_collection_id);
1304
5017
  collectionIds.forEach(id => id && allowedCollectionIds.add(id));
1305
- this.logInfo('filterProductsByMenuConfig: 餐牌允许商品集合', {
5018
+ this.logInfo('getProductScopeByMenuConfig: 餐牌允许商品集合', {
1306
5019
  menuId: menu.form_record_id,
1307
5020
  menuName: menu.name,
1308
5021
  configType: config.type,
@@ -1313,49 +5026,32 @@ class Server {
1313
5026
 
1314
5027
  // 如果有餐牌允许所有商品,返回所有商品
1315
5028
  if (hasProductAll) {
1316
- this.logInfo('filterProductsByMenuConfig: 返回所有商品(product_all)', {
1317
- productCount: products.length
1318
- });
1319
- return products;
5029
+ this.logInfo('getProductScopeByMenuConfig: 返回所有商品 scope(product_all)');
5030
+ return {
5031
+ isAllProducts: true
5032
+ };
1320
5033
  }
1321
5034
 
1322
5035
  // 如果没有收集到任何商品 ID 和集合 ID,返回空数组
1323
5036
  if (allowedProductIds.size === 0 && allowedCollectionIds.size === 0) {
1324
- this.logWarning('filterProductsByMenuConfig: 没有允许的商品和集合', {
1325
- productCount: products.length,
5037
+ this.logWarning('getProductScopeByMenuConfig: 没有允许的商品和集合', {
1326
5038
  activeMenuCount: activeMenuList.length
1327
5039
  });
1328
- return [];
5040
+ return {
5041
+ productIds: [],
5042
+ collectionIds: []
5043
+ };
1329
5044
  }
1330
5045
  const allowedProductIdsArray = Array.from(allowedProductIds);
1331
5046
  const allowedCollectionIdsArray = Array.from(allowedCollectionIds);
1332
- this.logInfo('filterProductsByMenuConfig: 收集到的允许规则', {
5047
+ this.logInfo('getProductScopeByMenuConfig: 收集到的允许规则', {
1333
5048
  allowedProductCount: allowedProductIds.size,
1334
5049
  allowedCollectionCount: allowedCollectionIds.size
1335
5050
  });
1336
-
1337
- // 过滤商品
1338
- const filteredProducts = products.filter(product => {
1339
- // 1. 检查商品 ID 是否在允许列表中
1340
- if (allowedProductIds.has(product.id)) {
1341
- return true;
1342
- }
1343
-
1344
- // 2. 检查商品是否属于允许的集合
1345
- if (allowedCollectionIds.size > 0 && Array.isArray(product.collection)) {
1346
- const hasMatchingCollection = product.collection.some(col => allowedCollectionIds.has(col.id));
1347
- if (hasMatchingCollection) {
1348
- return true;
1349
- }
1350
- }
1351
- return false;
1352
- });
1353
- this.logInfo('filterProductsByMenuConfig: 过滤完成', {
1354
- originalCount: products.length,
1355
- filteredCount: filteredProducts.length,
1356
- removedCount: products.length - filteredProducts.length
1357
- });
1358
- return filteredProducts;
5051
+ return {
5052
+ productIds: allowedProductIdsArray,
5053
+ collectionIds: allowedCollectionIdsArray
5054
+ };
1359
5055
  }
1360
5056
 
1361
5057
  /**