@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.
- package/dist/model/strategy/adapter/promotion/evaluator.js +3 -2
- package/dist/model/strategy/adapter/walletPass/utils.js +7 -4
- package/dist/modules/BaseModule.d.ts +2 -2
- package/dist/modules/BaseModule.js +4 -4
- package/dist/modules/BookingContext/index.d.ts +48 -0
- package/dist/modules/BookingContext/index.js +566 -0
- package/dist/modules/BookingContext/types.d.ts +102 -0
- package/dist/modules/BookingContext/types.js +51 -0
- package/dist/modules/BookingContext/utils/buildCacheItemFromOrderLine.d.ts +16 -0
- package/dist/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +302 -0
- package/dist/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.d.ts +8 -0
- package/dist/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.js +149 -0
- package/dist/modules/BookingContext/utils/cacheItemToBookingInput.d.ts +76 -0
- package/dist/modules/BookingContext/utils/cacheItemToBookingInput.js +249 -0
- package/dist/modules/BookingContext/utils/flexible.d.ts +28 -0
- package/dist/modules/BookingContext/utils/flexible.js +56 -0
- package/dist/modules/BookingContext/utils/formatResourceList.d.ts +8 -0
- package/dist/modules/BookingContext/utils/formatResourceList.js +38 -0
- package/dist/modules/BookingContext/utils/index.d.ts +11 -0
- package/dist/modules/BookingContext/utils/index.js +11 -0
- package/dist/modules/BookingContext/utils/noResource.d.ts +13 -0
- package/dist/modules/BookingContext/utils/noResource.js +77 -0
- package/dist/modules/BookingContext/utils/orderLineDisplay.d.ts +1 -0
- package/dist/modules/BookingContext/utils/orderLineDisplay.js +36 -0
- package/dist/modules/BookingContext/utils/productExtend.d.ts +72 -0
- package/dist/modules/BookingContext/utils/productExtend.js +339 -0
- package/dist/modules/BookingContext/utils/resourceErrors.d.ts +41 -0
- package/dist/modules/BookingContext/utils/resourceErrors.js +74 -0
- package/dist/modules/BookingContext/utils/resourceSelection.d.ts +7 -0
- package/dist/modules/BookingContext/utils/resourceSelection.js +24 -0
- package/dist/modules/BookingContext/utils/resources.d.ts +80 -0
- package/dist/modules/BookingContext/utils/resources.js +486 -0
- package/dist/modules/BookingContext/utils/serviceTimes.d.ts +55 -0
- package/dist/modules/BookingContext/utils/serviceTimes.js +151 -0
- package/dist/modules/BookingContext/utils/timeSlices.d.ts +42 -0
- package/dist/modules/BookingContext/utils/timeSlices.js +100 -0
- package/dist/modules/Cart/utils/cartProduct.js +26 -5
- package/dist/modules/Cart/utils/changePrice.js +27 -20
- package/dist/modules/Customer/index.d.ts +20 -0
- package/dist/modules/Customer/index.js +172 -83
- package/dist/modules/Customer/types.d.ts +2 -0
- package/dist/modules/Discount/index.d.ts +2 -1
- package/dist/modules/Discount/index.js +37 -9
- package/dist/modules/Discount/types.d.ts +1 -0
- package/dist/modules/OpenData/index.d.ts +8 -0
- package/dist/modules/OpenData/index.js +36 -16
- package/dist/modules/Order/index.d.ts +448 -23
- package/dist/modules/Order/index.js +5417 -561
- package/dist/modules/Order/types.d.ts +693 -88
- package/dist/modules/Order/types.js +68 -1
- package/dist/modules/Order/utils/bundleDiscountHydration.d.ts +5 -0
- package/dist/modules/Order/utils/bundleDiscountHydration.js +144 -0
- package/dist/modules/Order/utils/manualProductDiscount.d.ts +106 -0
- package/dist/modules/Order/utils/manualProductDiscount.js +210 -0
- package/dist/modules/Order/utils/orderCollectionIdentity.d.ts +53 -0
- package/dist/modules/Order/utils/orderCollectionIdentity.js +405 -0
- package/dist/modules/Order/utils.d.ts +167 -18
- package/dist/modules/Order/utils.js +960 -165
- package/dist/modules/Payment/index.d.ts +10 -9
- package/dist/modules/Payment/index.js +465 -703
- package/dist/modules/Payment/types.d.ts +40 -16
- package/dist/modules/Payment/utils.d.ts +1 -1
- package/dist/modules/Payment/utils.js +4 -3
- package/dist/modules/Payment/walletpass.js +44 -29
- package/dist/modules/Product/types.d.ts +29 -7
- package/dist/modules/ProductList/index.d.ts +17 -13
- package/dist/modules/ProductList/index.js +184 -74
- package/dist/modules/ProductList/types.d.ts +17 -0
- package/dist/modules/Quotation/index.d.ts +22 -2
- package/dist/modules/Quotation/index.js +108 -42
- package/dist/modules/Quotation/types.d.ts +19 -0
- package/dist/modules/ResourcePlanner/index.d.ts +24 -0
- package/dist/modules/ResourcePlanner/index.js +181 -0
- package/dist/modules/ResourcePlanner/planner.d.ts +14 -0
- package/dist/modules/ResourcePlanner/planner.js +1069 -0
- package/dist/modules/ResourcePlanner/types.d.ts +227 -0
- package/dist/modules/ResourcePlanner/types.js +1 -0
- package/dist/modules/Rules/index.d.ts +8 -10
- package/dist/modules/Rules/index.js +221 -109
- package/dist/modules/Rules/types.d.ts +14 -3
- package/dist/modules/SalesSummary/index.d.ts +23 -26
- package/dist/modules/SalesSummary/index.js +152 -46
- package/dist/modules/SalesSummary/types.d.ts +9 -1
- package/dist/modules/SalesSummary/utils.d.ts +3 -22
- package/dist/modules/SalesSummary/utils.js +803 -163
- package/dist/modules/Schedule/getDateIsInSchedule.js +14 -7
- package/dist/modules/Schedule/index.d.ts +18 -5
- package/dist/modules/Schedule/index.js +78 -24
- package/dist/modules/Schedule/types.d.ts +2 -0
- package/dist/modules/Summary/index.js +4 -3
- package/dist/modules/SurchargeList/index.d.ts +18 -0
- package/dist/modules/SurchargeList/index.js +185 -0
- package/dist/modules/SurchargeList/types.d.ts +15 -0
- package/dist/modules/SurchargeList/types.js +1 -0
- package/dist/modules/index.d.ts +3 -0
- package/dist/modules/index.js +4 -1
- package/dist/plugins/app.d.ts +2 -0
- package/dist/plugins/request.d.ts +1 -0
- package/dist/server/index.d.ts +462 -8
- package/dist/server/index.js +6112 -656
- package/dist/server/modules/floor-plan/index.d.ts +39 -0
- package/dist/server/modules/floor-plan/index.js +595 -0
- package/dist/server/modules/floor-plan/types.d.ts +43 -0
- package/dist/server/modules/floor-plan/types.js +13 -0
- package/dist/server/modules/index.d.ts +7 -2
- package/dist/server/modules/index.js +6 -0
- package/dist/server/modules/order/index.d.ts +207 -14
- package/dist/server/modules/order/index.js +2326 -379
- package/dist/server/modules/order/types.d.ts +76 -5
- package/dist/server/modules/order/types.js +14 -1
- package/dist/server/modules/order/utils/filterBookings.d.ts +7 -1
- package/dist/server/modules/order/utils/filterBookings.js +118 -11
- package/dist/server/modules/order/utils/filterOrders.js +242 -14
- package/dist/server/modules/payment/index.d.ts +28 -0
- package/dist/server/modules/payment/index.js +305 -0
- package/dist/server/modules/payment/types.d.ts +9 -0
- package/dist/server/modules/payment/types.js +1 -0
- package/dist/server/modules/products/index.d.ts +105 -14
- package/dist/server/modules/products/index.js +1393 -516
- package/dist/server/modules/products/types.d.ts +34 -1
- package/dist/server/modules/products/types.js +4 -0
- package/dist/server/modules/quotation/index.d.ts +39 -0
- package/dist/server/modules/quotation/index.js +278 -66
- package/dist/server/modules/resource/index.d.ts +2 -3
- package/dist/server/modules/resource/index.js +108 -86
- package/dist/server/modules/schedule/index.d.ts +6 -4
- package/dist/server/modules/schedule/index.js +116 -85
- package/dist/server/test.json +713 -0
- package/dist/server/types.d.ts +2 -0
- package/dist/server/utils/product.js +2 -2
- package/dist/server/utils/small-ticket.d.ts +71 -0
- package/dist/server/utils/small-ticket.js +963 -0
- package/dist/solution/BaseSales/index.d.ts +378 -0
- package/dist/solution/BaseSales/index.js +4006 -0
- package/dist/solution/BaseSales/types.d.ts +394 -0
- package/dist/solution/BaseSales/types.js +40 -0
- package/dist/solution/BaseSales/utils/cartPromotion.d.ts +287 -0
- package/dist/solution/BaseSales/utils/cartPromotion.js +1450 -0
- package/dist/solution/BaseSales/utils/parseSalesResponse.d.ts +144 -0
- package/dist/solution/BaseSales/utils/parseSalesResponse.js +157 -0
- package/dist/solution/BaseSales/utils/quotationPrice.d.ts +18 -0
- package/dist/solution/BaseSales/utils/quotationPrice.js +250 -0
- package/dist/solution/BaseSales/utils/transformBaseProductToOrderProduct.d.ts +63 -0
- package/dist/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +218 -0
- package/dist/solution/BaseSales/utils.d.ts +40 -0
- package/dist/solution/BaseSales/utils.js +263 -0
- package/dist/solution/BookingByStep/index.d.ts +1 -1
- package/dist/solution/BookingTicket/index.d.ts +394 -25
- package/dist/solution/BookingTicket/index.js +2506 -443
- package/dist/solution/BookingTicket/types.d.ts +191 -6
- package/dist/solution/BookingTicket/types.js +83 -10
- package/dist/solution/BookingTicket/utils/addProductDecision.d.ts +117 -0
- package/dist/solution/BookingTicket/utils/addProductDecision.js +346 -0
- package/dist/solution/BookingTicket/utils/addTimeAvailability.d.ts +35 -0
- package/dist/solution/BookingTicket/utils/addTimeAvailability.js +95 -0
- package/dist/solution/BookingTicket/utils/bookingStatus.d.ts +77 -0
- package/dist/solution/BookingTicket/utils/bookingStatus.js +136 -0
- package/dist/solution/BookingTicket/utils/cartView.d.ts +29 -0
- package/dist/solution/BookingTicket/utils/cartView.js +209 -0
- package/dist/solution/BookingTicket/utils/exampleData.d.ts +123 -0
- package/dist/solution/BookingTicket/utils/exampleData.js +183 -0
- package/dist/solution/BookingTicket/utils/orderCustomer.d.ts +4 -0
- package/dist/solution/BookingTicket/utils/orderCustomer.js +31 -0
- package/dist/solution/BookingTicket/utils/resolveBestAddTimePlan.d.ts +185 -0
- package/dist/solution/BookingTicket/utils/resolveBestAddTimePlan.js +438 -0
- package/dist/solution/BookingTicket/utils/scan/applyGlobalScan.d.ts +36 -0
- package/dist/solution/BookingTicket/utils/scan/applyGlobalScan.js +400 -0
- package/dist/solution/BookingTicket/utils/scan/cloudSearch.d.ts +4 -0
- package/dist/solution/BookingTicket/utils/scan/cloudSearch.js +41 -1
- package/dist/solution/BookingTicket/utils/scan/formatGlobalScan.d.ts +32 -0
- package/dist/solution/BookingTicket/utils/scan/formatGlobalScan.js +56 -0
- package/dist/solution/BookingTicket/utils/scan/handleScan.d.ts +3 -1
- package/dist/solution/BookingTicket/utils/scan/handleScan.js +11 -3
- package/dist/solution/BookingTicket/utils/scan/index.d.ts +5 -0
- package/dist/solution/BookingTicket/utils/scan/index.js +20 -8
- package/dist/solution/BookingTicket/utils/scan/scanProductValues.d.ts +21 -0
- package/dist/solution/BookingTicket/utils/scan/scanProductValues.js +150 -0
- package/dist/solution/BookingTicket/utils/sessionCatalogStale.d.ts +23 -0
- package/dist/solution/BookingTicket/utils/sessionCatalogStale.js +78 -0
- package/dist/solution/Checkout/index.d.ts +1 -0
- package/dist/solution/Checkout/index.js +25 -34
- package/dist/solution/Checkout/utils/index.js +4 -4
- package/dist/solution/RegisterAndLogin/config.js +2 -10
- package/dist/solution/Sales/index.d.ts +50 -5
- package/dist/solution/Sales/index.js +484 -92
- package/dist/solution/Sales/types.d.ts +26 -2
- package/dist/solution/ScanOrder/index.d.ts +27 -21
- package/dist/solution/ScanOrder/index.js +499 -434
- package/dist/solution/ScanOrder/types.d.ts +33 -11
- package/dist/solution/ScanOrder/types.js +4 -5
- package/dist/solution/ScanOrder/utils.d.ts +4 -1
- package/dist/solution/ScanOrder/utils.js +109 -34
- package/dist/solution/ShopDiscount/index.js +4 -4
- package/dist/solution/UnifiedBookingSales/index.d.ts +183 -0
- package/dist/solution/UnifiedBookingSales/index.js +1871 -0
- package/dist/solution/UnifiedBookingSales/types.d.ts +143 -0
- package/dist/solution/UnifiedBookingSales/types.js +11 -0
- package/dist/solution/VenueBooking/index.d.ts +29 -20
- package/dist/solution/VenueBooking/index.js +301 -213
- package/dist/solution/VenueBooking/sales-section-4-annotated.json +0 -2
- package/dist/solution/VenueBooking/utils/slotMerge.d.ts +6 -0
- package/dist/solution/VenueBooking/utils/slotMerge.js +9 -3
- package/dist/solution/index.d.ts +2 -0
- package/dist/solution/index.js +3 -1
- package/dist/types/index.d.ts +11 -1
- package/lib/model/strategy/adapter/promotion/evaluator.js +3 -2
- package/lib/model/strategy/adapter/walletPass/utils.js +6 -4
- package/lib/modules/BaseModule.d.ts +2 -2
- package/lib/modules/BaseModule.js +4 -4
- package/lib/modules/BookingContext/index.d.ts +48 -0
- package/lib/modules/BookingContext/index.js +390 -0
- package/lib/modules/BookingContext/types.d.ts +102 -0
- package/lib/modules/BookingContext/types.js +48 -0
- package/lib/modules/BookingContext/utils/buildCacheItemFromOrderLine.d.ts +16 -0
- package/lib/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +286 -0
- package/lib/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.d.ts +8 -0
- package/lib/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.js +148 -0
- package/lib/modules/BookingContext/utils/cacheItemToBookingInput.d.ts +76 -0
- package/lib/modules/BookingContext/utils/cacheItemToBookingInput.js +242 -0
- package/lib/modules/BookingContext/utils/flexible.d.ts +28 -0
- package/lib/modules/BookingContext/utils/flexible.js +63 -0
- package/lib/modules/BookingContext/utils/formatResourceList.d.ts +8 -0
- package/lib/modules/BookingContext/utils/formatResourceList.js +35 -0
- package/lib/modules/BookingContext/utils/index.d.ts +11 -0
- package/lib/modules/BookingContext/utils/index.js +126 -0
- package/lib/modules/BookingContext/utils/noResource.d.ts +13 -0
- package/lib/modules/BookingContext/utils/noResource.js +78 -0
- package/lib/modules/BookingContext/utils/orderLineDisplay.d.ts +1 -0
- package/lib/modules/BookingContext/utils/orderLineDisplay.js +29 -0
- package/lib/modules/BookingContext/utils/productExtend.d.ts +72 -0
- package/lib/modules/BookingContext/utils/productExtend.js +330 -0
- package/lib/modules/BookingContext/utils/resourceErrors.d.ts +41 -0
- package/lib/modules/BookingContext/utils/resourceErrors.js +80 -0
- package/lib/modules/BookingContext/utils/resourceSelection.d.ts +7 -0
- package/lib/modules/BookingContext/utils/resourceSelection.js +24 -0
- package/lib/modules/BookingContext/utils/resources.d.ts +80 -0
- package/lib/modules/BookingContext/utils/resources.js +419 -0
- package/lib/modules/BookingContext/utils/serviceTimes.d.ts +55 -0
- package/lib/modules/BookingContext/utils/serviceTimes.js +161 -0
- package/lib/modules/BookingContext/utils/timeSlices.d.ts +42 -0
- package/lib/modules/BookingContext/utils/timeSlices.js +114 -0
- package/lib/modules/Cart/utils/cartProduct.js +27 -5
- package/lib/modules/Cart/utils/changePrice.js +20 -14
- package/lib/modules/Customer/index.d.ts +20 -0
- package/lib/modules/Customer/index.js +63 -10
- package/lib/modules/Customer/types.d.ts +2 -0
- package/lib/modules/Discount/index.d.ts +2 -1
- package/lib/modules/Discount/index.js +18 -2
- package/lib/modules/Discount/types.d.ts +1 -0
- package/lib/modules/OpenData/index.d.ts +8 -0
- package/lib/modules/OpenData/index.js +21 -3
- package/lib/modules/Order/index.d.ts +448 -23
- package/lib/modules/Order/index.js +3641 -308
- package/lib/modules/Order/types.d.ts +693 -88
- package/lib/modules/Order/types.js +55 -1
- package/lib/modules/Order/utils/bundleDiscountHydration.d.ts +5 -0
- package/lib/modules/Order/utils/bundleDiscountHydration.js +106 -0
- package/lib/modules/Order/utils/manualProductDiscount.d.ts +106 -0
- package/lib/modules/Order/utils/manualProductDiscount.js +207 -0
- package/lib/modules/Order/utils/orderCollectionIdentity.d.ts +53 -0
- package/lib/modules/Order/utils/orderCollectionIdentity.js +353 -0
- package/lib/modules/Order/utils.d.ts +167 -18
- package/lib/modules/Order/utils.js +974 -138
- package/lib/modules/Payment/index.d.ts +10 -9
- package/lib/modules/Payment/index.js +30 -124
- package/lib/modules/Payment/types.d.ts +40 -16
- package/lib/modules/Payment/utils.d.ts +1 -1
- package/lib/modules/Payment/utils.js +4 -3
- package/lib/modules/Payment/walletpass.js +16 -6
- package/lib/modules/Product/types.d.ts +29 -7
- package/lib/modules/ProductList/index.d.ts +17 -13
- package/lib/modules/ProductList/index.js +92 -51
- package/lib/modules/ProductList/types.d.ts +17 -0
- package/lib/modules/Quotation/index.d.ts +22 -2
- package/lib/modules/Quotation/index.js +76 -20
- package/lib/modules/Quotation/types.d.ts +19 -0
- package/lib/modules/ResourcePlanner/index.d.ts +24 -0
- package/lib/modules/ResourcePlanner/index.js +147 -0
- package/lib/modules/ResourcePlanner/planner.d.ts +14 -0
- package/lib/modules/ResourcePlanner/planner.js +895 -0
- package/lib/modules/ResourcePlanner/types.d.ts +227 -0
- package/lib/modules/ResourcePlanner/types.js +5 -0
- package/lib/modules/Rules/index.d.ts +8 -10
- package/lib/modules/Rules/index.js +152 -60
- package/lib/modules/Rules/types.d.ts +14 -3
- package/lib/modules/SalesSummary/index.d.ts +23 -26
- package/lib/modules/SalesSummary/index.js +84 -16
- package/lib/modules/SalesSummary/types.d.ts +9 -1
- package/lib/modules/SalesSummary/utils.d.ts +3 -22
- package/lib/modules/SalesSummary/utils.js +616 -111
- package/lib/modules/Schedule/getDateIsInSchedule.js +14 -7
- package/lib/modules/Schedule/index.d.ts +18 -5
- package/lib/modules/Schedule/index.js +49 -14
- package/lib/modules/Schedule/types.d.ts +2 -0
- package/lib/modules/Summary/index.js +2 -2
- package/lib/modules/SurchargeList/index.d.ts +18 -0
- package/lib/modules/SurchargeList/index.js +95 -0
- package/lib/modules/SurchargeList/types.d.ts +15 -0
- package/lib/modules/SurchargeList/types.js +5 -0
- package/lib/modules/index.d.ts +3 -0
- package/lib/modules/index.js +33 -0
- package/lib/plugins/app.d.ts +2 -0
- package/lib/plugins/request.d.ts +1 -0
- package/lib/server/index.d.ts +462 -8
- package/lib/server/index.js +3901 -205
- package/lib/server/modules/floor-plan/index.d.ts +39 -0
- package/lib/server/modules/floor-plan/index.js +306 -0
- package/lib/server/modules/floor-plan/types.d.ts +43 -0
- package/lib/server/modules/floor-plan/types.js +19 -0
- package/lib/server/modules/index.d.ts +7 -2
- package/lib/server/modules/index.js +21 -0
- package/lib/server/modules/order/index.d.ts +207 -14
- package/lib/server/modules/order/index.js +1324 -141
- package/lib/server/modules/order/types.d.ts +76 -5
- package/lib/server/modules/order/types.js +11 -1
- package/lib/server/modules/order/utils/filterBookings.d.ts +7 -1
- package/lib/server/modules/order/utils/filterBookings.js +119 -11
- package/lib/server/modules/order/utils/filterOrders.js +238 -13
- package/lib/server/modules/payment/index.d.ts +28 -0
- package/lib/server/modules/payment/index.js +168 -0
- package/lib/server/modules/payment/types.d.ts +9 -0
- package/lib/server/modules/payment/types.js +5 -0
- package/lib/server/modules/products/index.d.ts +105 -14
- package/lib/server/modules/products/index.js +588 -97
- package/lib/server/modules/products/types.d.ts +34 -1
- package/lib/server/modules/products/types.js +3 -0
- package/lib/server/modules/quotation/index.d.ts +39 -0
- package/lib/server/modules/quotation/index.js +145 -1
- package/lib/server/modules/resource/index.d.ts +2 -3
- package/lib/server/modules/resource/index.js +71 -45
- package/lib/server/modules/schedule/index.d.ts +6 -4
- package/lib/server/modules/schedule/index.js +55 -44
- package/lib/server/test.json +713 -0
- package/lib/server/types.d.ts +2 -0
- package/lib/server/utils/product.js +2 -2
- package/lib/server/utils/small-ticket.d.ts +71 -0
- package/lib/server/utils/small-ticket.js +926 -0
- package/lib/solution/BaseSales/index.d.ts +378 -0
- package/lib/solution/BaseSales/index.js +2367 -0
- package/lib/solution/BaseSales/types.d.ts +394 -0
- package/lib/solution/BaseSales/types.js +47 -0
- package/lib/solution/BaseSales/utils/cartPromotion.d.ts +287 -0
- package/lib/solution/BaseSales/utils/cartPromotion.js +1155 -0
- package/lib/solution/BaseSales/utils/parseSalesResponse.d.ts +144 -0
- package/lib/solution/BaseSales/utils/parseSalesResponse.js +140 -0
- package/lib/solution/BaseSales/utils/quotationPrice.d.ts +18 -0
- package/lib/solution/BaseSales/utils/quotationPrice.js +231 -0
- package/lib/solution/BaseSales/utils/transformBaseProductToOrderProduct.d.ts +63 -0
- package/lib/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +215 -0
- package/lib/solution/BaseSales/utils.d.ts +40 -0
- package/lib/solution/BaseSales/utils.js +259 -0
- package/lib/solution/BookingByStep/index.d.ts +1 -1
- package/lib/solution/BookingTicket/index.d.ts +394 -25
- package/lib/solution/BookingTicket/index.js +1350 -76
- package/lib/solution/BookingTicket/types.d.ts +191 -6
- package/lib/solution/BookingTicket/types.js +71 -10
- package/lib/solution/BookingTicket/utils/addProductDecision.d.ts +117 -0
- package/lib/solution/BookingTicket/utils/addProductDecision.js +353 -0
- package/lib/solution/BookingTicket/utils/addTimeAvailability.d.ts +35 -0
- package/lib/solution/BookingTicket/utils/addTimeAvailability.js +94 -0
- package/lib/solution/BookingTicket/utils/bookingStatus.d.ts +77 -0
- package/lib/solution/BookingTicket/utils/bookingStatus.js +136 -0
- package/lib/solution/BookingTicket/utils/cartView.d.ts +29 -0
- package/lib/solution/BookingTicket/utils/cartView.js +200 -0
- package/lib/solution/BookingTicket/utils/exampleData.d.ts +123 -0
- package/lib/solution/BookingTicket/utils/exampleData.js +185 -0
- package/lib/solution/BookingTicket/utils/orderCustomer.d.ts +4 -0
- package/lib/solution/BookingTicket/utils/orderCustomer.js +35 -0
- package/lib/solution/BookingTicket/utils/resolveBestAddTimePlan.d.ts +185 -0
- package/lib/solution/BookingTicket/utils/resolveBestAddTimePlan.js +387 -0
- package/lib/solution/BookingTicket/utils/scan/applyGlobalScan.d.ts +36 -0
- package/lib/solution/BookingTicket/utils/scan/applyGlobalScan.js +220 -0
- package/lib/solution/BookingTicket/utils/scan/cloudSearch.d.ts +4 -0
- package/lib/solution/BookingTicket/utils/scan/cloudSearch.js +24 -3
- package/lib/solution/BookingTicket/utils/scan/formatGlobalScan.d.ts +32 -0
- package/lib/solution/BookingTicket/utils/scan/formatGlobalScan.js +64 -0
- package/lib/solution/BookingTicket/utils/scan/handleScan.d.ts +3 -1
- package/lib/solution/BookingTicket/utils/scan/handleScan.js +4 -2
- package/lib/solution/BookingTicket/utils/scan/index.d.ts +5 -0
- package/lib/solution/BookingTicket/utils/scan/index.js +14 -2
- package/lib/solution/BookingTicket/utils/scan/scanProductValues.d.ts +21 -0
- package/lib/solution/BookingTicket/utils/scan/scanProductValues.js +115 -0
- package/lib/solution/BookingTicket/utils/sessionCatalogStale.d.ts +23 -0
- package/lib/solution/BookingTicket/utils/sessionCatalogStale.js +83 -0
- package/lib/solution/Checkout/index.d.ts +1 -0
- package/lib/solution/Checkout/index.js +18 -19
- package/lib/solution/Checkout/utils/index.js +4 -4
- package/lib/solution/RegisterAndLogin/config.js +2 -10
- package/lib/solution/Sales/index.d.ts +50 -5
- package/lib/solution/Sales/index.js +296 -39
- package/lib/solution/Sales/types.d.ts +26 -2
- package/lib/solution/ScanOrder/index.d.ts +27 -21
- package/lib/solution/ScanOrder/index.js +144 -103
- package/lib/solution/ScanOrder/types.d.ts +33 -11
- package/lib/solution/ScanOrder/types.js +4 -5
- package/lib/solution/ScanOrder/utils.d.ts +4 -1
- package/lib/solution/ScanOrder/utils.js +98 -26
- package/lib/solution/ShopDiscount/index.js +3 -3
- package/lib/solution/UnifiedBookingSales/index.d.ts +183 -0
- package/lib/solution/UnifiedBookingSales/index.js +1050 -0
- package/lib/solution/UnifiedBookingSales/types.d.ts +143 -0
- package/lib/solution/UnifiedBookingSales/types.js +14 -0
- package/lib/solution/VenueBooking/index.d.ts +29 -20
- package/lib/solution/VenueBooking/index.js +169 -94
- package/lib/solution/VenueBooking/sales-section-4-annotated.json +0 -2
- package/lib/solution/VenueBooking/utils/slotMerge.d.ts +6 -0
- package/lib/solution/VenueBooking/utils/slotMerge.js +9 -3
- package/lib/solution/index.d.ts +2 -0
- package/lib/solution/index.js +22 -0
- package/lib/types/index.d.ts +11 -1
- package/package.json +1 -1
- package/dist/solution/Checkout/appointmentDemo.json +0 -1
- package/lib/solution/Checkout/appointmentDemo.json +0 -1
package/lib/server/index.js
CHANGED
|
@@ -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
|
|
18
|
+
var _types2 = require("./modules/products/types");
|
|
16
19
|
var _product = require("./utils/product");
|
|
17
20
|
var _order = require("./modules/order");
|
|
18
|
-
var
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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
|
-
|
|
708
|
-
|
|
1399
|
+
customer_id,
|
|
1400
|
+
ids,
|
|
1401
|
+
extension_type,
|
|
1402
|
+
strategy_context
|
|
709
1403
|
} = data;
|
|
710
1404
|
const {
|
|
711
|
-
|
|
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
|
-
|
|
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 (
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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
|
-
|
|
1607
|
+
remaining: this.orderQuerySubscribers.size
|
|
787
1608
|
});
|
|
788
1609
|
}
|
|
789
|
-
return this.
|
|
1610
|
+
return this.fetchOrderListFromAPI(queryPayload);
|
|
790
1611
|
};
|
|
791
1612
|
|
|
792
1613
|
/**
|
|
@@ -813,26 +1634,116 @@ class Server {
|
|
|
813
1634
|
};
|
|
814
1635
|
|
|
815
1636
|
/**
|
|
816
|
-
*
|
|
1637
|
+
* 处理 sales 单模块搜索:请求透传后端,仅在 OS 层维护已返回订单号集合。
|
|
1638
|
+
*
|
|
1639
|
+
* @example
|
|
1640
|
+
* await this.handleSalesSearchModule({ url: '/shop/es/search/sales', method: 'post', data: { keyword: 'A' } })
|
|
817
1641
|
*/
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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
|
-
*
|
|
1731
|
+
* 透传调用后端 sales 搜索接口。
|
|
1732
|
+
*
|
|
1733
|
+
* @example
|
|
1734
|
+
* const res = await this.fetchSalesSearchFromAPI('/shop/es/search/sales', { keyword: '1001' })
|
|
828
1735
|
*/
|
|
829
|
-
async
|
|
1736
|
+
async fetchSalesSearchFromAPI(backendPath, data) {
|
|
830
1737
|
if (!this.app?.request) {
|
|
831
|
-
this.logError('
|
|
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.
|
|
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
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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
|
-
|
|
4299
|
+
data: this.floor_plan.getSortedList()
|
|
864
4300
|
};
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
this.
|
|
868
|
-
|
|
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
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
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
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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
|
-
|
|
905
|
-
|
|
906
|
-
this.
|
|
907
|
-
|
|
908
|
-
|
|
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
|
-
|
|
916
|
-
|
|
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
|
-
|
|
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
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1077
|
-
|
|
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
|
|
1105
|
-
activeMenuList =
|
|
1106
|
-
|
|
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:
|
|
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
|
-
|
|
4687
|
+
const productScope = this.getProductScopeByMenuConfig(activeMenuList);
|
|
4688
|
+
const scopedProductIds = this.products.getProductIdsByScope(productScope);
|
|
1124
4689
|
(0, _product.perfMark)('computeQuery.filterByMenu', performance.now() - tFilter, {
|
|
1125
|
-
|
|
1126
|
-
|
|
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
|
-
|
|
4728
|
+
scopedProductCount: scopedProductIds.length,
|
|
1146
4729
|
filteredCount: filteredProducts.length,
|
|
1147
4730
|
activeMenuCount: activeMenuList.length
|
|
1148
4731
|
});
|
|
1149
4732
|
this.logInfo('computeProductQueryResult 完成', {
|
|
1150
|
-
|
|
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
|
-
|
|
1254
|
-
// 如果没有生效的餐牌,返回空数组
|
|
4965
|
+
getProductScopeByMenuConfig(activeMenuList) {
|
|
1255
4966
|
if (!activeMenuList || activeMenuList.length === 0) {
|
|
1256
|
-
this.logInfo('
|
|
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
|
-
|
|
1265
|
-
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
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('
|
|
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('
|
|
5047
|
+
this.logInfo('getProductScopeByMenuConfig: 收集到的允许规则', {
|
|
1333
5048
|
allowedProductCount: allowedProductIds.size,
|
|
1334
5049
|
allowedCollectionCount: allowedCollectionIds.size
|
|
1335
5050
|
});
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
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
|
/**
|