@pisell/pisellos 2.3.132 → 2.3.134

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/dist/model/strategy/adapter/promotion/index.js +9 -0
  2. package/dist/modules/Order/index.d.ts +1 -1
  3. package/dist/solution/BookingByStep/index.d.ts +1 -1
  4. package/dist/solution/VenueBooking/index.d.ts +15 -2
  5. package/dist/solution/VenueBooking/index.js +1036 -760
  6. package/dist/solution/VenueBooking/types.d.ts +2 -1
  7. package/dist/solution/VenueBooking/types.js +1 -0
  8. package/dist/solution/VenueBooking/utils/dateSummary.d.ts +1 -0
  9. package/dist/solution/VenueBooking/utils/dateSummary.js +7 -4
  10. package/dist/solution/VenueBooking/utils/smartPricing.d.ts +71 -0
  11. package/dist/solution/VenueBooking/utils/smartPricing.js +273 -0
  12. package/dist/solution/VenueBooking/utils/timeSlot.d.ts +1 -0
  13. package/dist/solution/VenueBooking/utils/timeSlot.js +6 -3
  14. package/lib/model/strategy/adapter/promotion/index.js +46 -1
  15. package/lib/modules/Order/index.d.ts +1 -1
  16. package/lib/solution/BookingByStep/index.d.ts +1 -1
  17. package/lib/solution/VenueBooking/index.d.ts +15 -2
  18. package/lib/solution/VenueBooking/index.js +234 -88
  19. package/lib/solution/VenueBooking/types.d.ts +2 -1
  20. package/lib/solution/VenueBooking/types.js +1 -0
  21. package/lib/solution/VenueBooking/utils/dateSummary.d.ts +1 -0
  22. package/lib/solution/VenueBooking/utils/dateSummary.js +7 -4
  23. package/lib/solution/VenueBooking/utils/smartPricing.d.ts +71 -0
  24. package/lib/solution/VenueBooking/utils/smartPricing.js +224 -0
  25. package/lib/solution/VenueBooking/utils/timeSlot.d.ts +1 -0
  26. package/lib/solution/VenueBooking/utils/timeSlot.js +5 -2
  27. package/package.json +1 -1
@@ -34,6 +34,7 @@ var _itemRule = require("../../model/strategy/adapter/itemRule");
34
34
  var _resource = require("./utils/resource");
35
35
  var _timeSlot = require("./utils/timeSlot");
36
36
  var _dateSummary = require("./utils/dateSummary");
37
+ var _smartPricing = require("./utils/smartPricing");
37
38
  var _slotMerge = require("./utils/slotMerge");
38
39
  var _utils3 = require("../../modules/Order/utils");
39
40
  var _Order = require("../../modules/Order");
@@ -94,6 +95,12 @@ class VenueBookingImpl extends _BaseSales.BaseSalesImpl {
94
95
  customerLoginRefreshIdInFlight = null;
95
96
  loadAllProductsInFlight = null;
96
97
  productsLoaded = false;
98
+ // undefined 尚未同步宿主身份;null 表示已明确退出,禁止回退到旧用户插件。
99
+ smartPricingCustomerId;
100
+ smartPricingProductCatalog = [];
101
+ slotPriceMapCache = new Map();
102
+ slotPriceStrategyConfigs;
103
+ slotPriceScheduleList = undefined;
97
104
  loadOpenDataConfigInFlight = null;
98
105
  static OPEN_DATA_CACHE_TTL = 5 * 60 * 1000;
99
106
  getLoggerContext() {
@@ -287,6 +294,9 @@ class VenueBookingImpl extends _BaseSales.BaseSalesImpl {
287
294
  return null;
288
295
  }
289
296
  resolveHolderCustomerId() {
297
+ if (this.smartPricingCustomerId !== undefined) {
298
+ return this.smartPricingCustomerId ?? undefined;
299
+ }
290
300
  const customer = JSON.parse(this.window?.getLocalStorage?.('customer') || '{}');
291
301
  if (customer?.id) return Number(customer.id);
292
302
  return undefined;
@@ -338,52 +348,65 @@ class VenueBookingImpl extends _BaseSales.BaseSalesImpl {
338
348
  }
339
349
  registerCustomerLoginListeners() {
340
350
  this.clearLoginEffectListeners();
341
- const accountModule = this.core?.getModule('account');
342
- const registerAndLoginModule = this.core?.getModule('registerAndLogin');
343
- const currentAccount = accountModule?.getCurrentAccount?.() || accountModule?.getAccount?.() || null;
344
- const createHandleLogin = eventName => async payload => {
351
+ const handleLogin = async payload => {
345
352
  const customerId = this.resolveCustomerIdFromLoginPayload(payload);
346
353
  if (!customerId) return;
347
- await this.refreshOrderMarketingAfterLogin({
354
+ await this.refreshOrderMarketingForCustomer({
348
355
  customerId
349
356
  });
350
357
  };
351
- this.registerLoginEffect(_types3.AccountHooks.OnLogin, createHandleLogin(_types3.AccountHooks.OnLogin));
352
- this.registerLoginEffect(_types4.RegisterAndLoginHooks.onLoginSuccess, createHandleLogin(_types4.RegisterAndLoginHooks.onLoginSuccess));
358
+ this.registerLoginEffect(_types3.AccountHooks.OnLogin, handleLogin);
359
+ this.registerLoginEffect(_types4.RegisterAndLoginHooks.onLoginSuccess, handleLogin);
360
+ this.registerLoginEffect(_types3.AccountHooks.OnLogout, () => this.onCustomerLogout());
353
361
  }
354
- async refreshOrderMarketingAfterLogin(params) {
362
+ async refreshOrderMarketingForCustomer(params) {
355
363
  if (!this.store.order) throw new Error('order 模块未初始化');
356
- if (this.customerLoginRefreshInFlight) {
357
- if (this.customerLoginRefreshIdInFlight === params.customerId) {
358
- await this.customerLoginRefreshInFlight;
359
- return;
360
- }
364
+ if (this.customerLoginRefreshInFlight && this.customerLoginRefreshIdInFlight === params.customerId) {
361
365
  await this.customerLoginRefreshInFlight;
366
+ return;
362
367
  }
368
+ const previousRefresh = this.customerLoginRefreshInFlight;
363
369
  this.customerLoginRefreshIdInFlight = params.customerId;
364
370
  const refreshTask = (async () => {
365
- // 刷新 holder 表单数据
366
- if (this.store.holder) {
367
- await this.store.holder.refreshAllFormRecords({
368
- customer_id: params.customerId,
369
- useCache: false
370
- });
371
- }
372
- if (this.store.quotation) {
373
- await this.ensureQuotationsLoaded({
374
- channel: this.otherParams?.channel
371
+ // 身份切换按调用顺序执行;上一请求失败也不能阻止退出清理。
372
+ if (previousRefresh) await previousRefresh.catch(() => undefined);
373
+ this.smartPricingCustomerId = params.customerId;
374
+ const order = this.store.order;
375
+ if (params.customerId === null) {
376
+ order.clearOrderCustomer();
377
+ // 清除客户记录,保留表单定义供下次登录刷新。
378
+ const holder = this.store.holder;
379
+ for (const formId of Object.keys(holder?.getHolderFormMap() || {})) {
380
+ holder?.setFormData(formId, {
381
+ records: []
382
+ });
383
+ }
384
+ } else {
385
+ const tempOrder = order.ensureTempOrder();
386
+ if (Number(tempOrder.customer_id) !== params.customerId) {
387
+ const user = this.core.getPlugin('user')?.get?.();
388
+ order.updateTempOrderCustomer({
389
+ ...(Number(user?.id) === params.customerId ? user : {}),
390
+ customer_id: params.customerId
391
+ });
392
+ }
393
+ // 先获取新客户的钱包/优惠资产,再按该上下文计算智能价。
394
+ await order.loadDiscountConfig({
395
+ customerId: params.customerId,
396
+ apply: false
375
397
  });
398
+ if (this.store.holder) {
399
+ await this.store.holder.refreshAllFormRecords({
400
+ customer_id: params.customerId,
401
+ useCache: false
402
+ });
403
+ }
376
404
  }
377
- this.recalculateOrderPricesFromQuotation();
378
- await this.store.order.loadDiscountConfig({
379
- customerId: params.customerId
380
- });
381
- await this.store.order.recalculateSummary({
382
- createIfMissing: true
383
- });
384
- this.store.order.persistTempOrder();
385
- await this.refreshItemRuleQuantityLimits();
386
- await this.refreshCartValidationPassed();
405
+
406
+ // 不复用身份切换前尚未返回的商品请求,避免旧客户的结果覆盖新价格。
407
+ if (this.loadAllProductsInFlight) await this.loadAllProductsInFlight;
408
+ if (this.productsLoaded) await this.loadAllProducts();
409
+ await this.refreshSmartPricing();
387
410
  })();
388
411
  this.customerLoginRefreshInFlight = refreshTask;
389
412
  try {
@@ -407,6 +430,12 @@ class VenueBookingImpl extends _BaseSales.BaseSalesImpl {
407
430
  this.store.status = 'initializing';
408
431
  this.store.error = null;
409
432
  this.store.rawResourceData = [];
433
+ this.smartPricingCustomerId = undefined;
434
+ this.smartPricingProductCatalog = [];
435
+ this.productsLoaded = false;
436
+ this.slotPriceStrategyConfigs = undefined;
437
+ this.slotPriceScheduleList = undefined;
438
+ this.slotPriceMapCache.clear();
410
439
  this.baseSlotConfig = {
411
440
  ..._types.DEFAULT_SLOT_CONFIG,
412
441
  ...(options.otherParams?.slotConfig || {})
@@ -644,6 +673,80 @@ class VenueBookingImpl extends _BaseSales.BaseSalesImpl {
644
673
  failures: []
645
674
  };
646
675
  }
676
+ resolveCustomerIdForSmartPricing() {
677
+ if (this.smartPricingCustomerId !== undefined) return this.smartPricingCustomerId ?? undefined;
678
+ try {
679
+ const user = this.core?.getPlugin('user');
680
+ const customerId = Number(user?.get?.()?.id);
681
+ return Number.isFinite(customerId) && customerId > 0 ? customerId : undefined;
682
+ } catch {
683
+ return undefined;
684
+ }
685
+ }
686
+ buildSmartPricingContext() {
687
+ const strategyContext = this.otherParams?.strategy_context || {};
688
+ const orderWalletIds = this.store.order?.getAvailableWalletIds?.();
689
+ const fallbackWalletIds = this.otherParams?.available_wallet_ids ?? this.otherParams?.availableWalletIds ?? strategyContext.available_wallet_ids ?? strategyContext.availableWalletIds;
690
+ const walletIds = this.smartPricingCustomerId === null ? [] : orderWalletIds?.length ? orderWalletIds : fallbackWalletIds;
691
+ return {
692
+ evaluator: this.core?.context?.dataVariantEvaluator,
693
+ scheduleList: this.store.schedule?.getScheduleList?.() || [],
694
+ menuList: this.otherParams?.menuList || this.otherParams?.openData?.menuList || this.core?.context?.menuList || [],
695
+ customerId: this.resolveCustomerIdForSmartPricing(),
696
+ availableWalletIds: Array.isArray(walletIds) ? walletIds : [],
697
+ channel: this.otherParams?.channel ?? strategyContext.channel,
698
+ businessCode: this.otherParams?.businessCode ?? strategyContext.business_code ?? strategyContext.businessCode,
699
+ orderType: this.otherParams?.orderType || this.otherParams?.type || strategyContext.orderType || strategyContext.order_type,
700
+ strategy_context: strategyContext
701
+ };
702
+ }
703
+ buildSlotPriceMapForDate(date, config, productIds) {
704
+ const context = this.buildSmartPricingContext();
705
+ const strategyConfigs = context.evaluator?.getStrategyConfigs?.();
706
+ if (strategyConfigs !== this.slotPriceStrategyConfigs) {
707
+ this.slotPriceMapCache.clear();
708
+ this.slotPriceStrategyConfigs = strategyConfigs;
709
+ }
710
+ if (context.scheduleList !== this.slotPriceScheduleList) {
711
+ this.slotPriceMapCache.clear();
712
+ this.slotPriceScheduleList = context.scheduleList;
713
+ }
714
+ const cacheKey = JSON.stringify([date, config, productIds, context.customerId, context.availableWalletIds, context.channel, context.businessCode, context.orderType]);
715
+ const cached = this.slotPriceMapCache.get(cacheKey);
716
+ if (cached) return cached;
717
+ const priceMap = (0, _smartPricing.buildSlotPriceMapByScheduleSegments)({
718
+ products: this.smartPricingProductCatalog,
719
+ productIds,
720
+ date,
721
+ timeLabels: (0, _timeSlot.generateTimeLabels)(config),
722
+ config,
723
+ context
724
+ });
725
+ this.slotPriceMapCache.set(cacheKey, priceMap);
726
+ return priceMap;
727
+ }
728
+
729
+ /** 规则或客户上下文更新后,同步商品展示、已选时段和订单汇总。 */
730
+ async refreshSmartPricing() {
731
+ if (this.loadAllProductsInFlight) await this.loadAllProductsInFlight;
732
+ this.slotPriceMapCache.clear();
733
+ if (!this.productsLoaded || !this.store.order) return;
734
+ await this.store.schedule?.loadAllSchedule();
735
+ const products = (0, _smartPricing.formatProductsWithDataVariant)(this.smartPricingProductCatalog, this.buildSmartPricingContext());
736
+ this.store.venueProducts?.addProduct(products.filter(product => product.duration != null));
737
+ this.store.addonProducts?.addProduct(products.filter(product => product.duration == null));
738
+ this.recalculateOrderPricesFromSmartPricing();
739
+ this.store.order.applyDiscount();
740
+ await this.store.order.recalculateSummary({
741
+ createIfMissing: false
742
+ });
743
+ this.store.order.persistTempOrder();
744
+ await this.refreshItemRuleQuantityLimits();
745
+ await this.refreshCartValidationPassed();
746
+ await this.core.effects.emit(_types.VenueBookingHooks.onSmartPricingUpdated, {
747
+ customerId: this.resolveCustomerIdForSmartPricing()
748
+ });
749
+ }
647
750
 
648
751
  // ─── 场地商品 & 附加商品 ───
649
752
 
@@ -660,19 +763,34 @@ class VenueBookingImpl extends _BaseSales.BaseSalesImpl {
660
763
  if (!this.store.venueProducts) throw new Error('venueProducts 模块未初始化');
661
764
  if (!this.store.addonProducts) throw new Error('addonProducts 模块未初始化');
662
765
  await this.loadOpenDataConfig();
766
+ if (!this.store.schedule) throw new Error('schedule 模块未初始化');
767
+ await this.store.schedule.loadAllSchedule();
663
768
  const associatedMenus = this.otherParams?.openData?.['menu.associated_menus'] || [];
664
769
  if (!associatedMenus.length) {
665
770
  this.logMethodError('loadAllProducts', new Error('未获取到餐牌配置(menu.associated_menus),请检查 OpenData 配置'));
666
771
  throw new Error('未获取到餐牌配置(menu.associated_menus),请检查 OpenData 配置');
667
772
  }
668
773
  const menuListIds = associatedMenus.map(n => Number(n.value));
774
+ const pricingContext = this.buildSmartPricingContext();
669
775
  const allProducts = await this.store.venueProducts.loadProducts({
670
776
  menu_list_ids: menuListIds,
671
777
  cacheId: this.cacheId,
672
778
  schedule_date: (0, _dayjs.default)().format('YYYY-MM-DD'),
673
- schedule_datetime: (0, _dayjs.default)().format('YYYY-MM-DD HH:mm:ss')
779
+ schedule_datetime: (0, _dayjs.default)().format('YYYY-MM-DD HH:mm:ss'),
780
+ customer_id: this.resolveCustomerIdForSmartPricing(),
781
+ strategy_context: {
782
+ ...pricingContext.strategy_context,
783
+ channel: pricingContext.channel,
784
+ business_code: pricingContext.businessCode,
785
+ order_type: pricingContext.orderType,
786
+ available_wallet_ids: pricingContext.availableWalletIds
787
+ }
674
788
  });
675
- const list = Array.isArray(allProducts) ? allProducts : [];
789
+ const loadedList = Array.isArray(allProducts) ? allProducts : [];
790
+ const rawProducts = this.store.venueProducts.getRawProductPriceSources(loadedList.map(product => product.id));
791
+ this.smartPricingProductCatalog = await Promise.all(loadedList.map(async product => rawProducts.get(product.id) ?? (await this.core.server?.getProducts?.()?.getProductById(product.id)) ?? product));
792
+ const list = (0, _smartPricing.formatProductsWithDataVariant)(this.smartPricingProductCatalog, this.buildSmartPricingContext());
793
+ this.slotPriceMapCache.clear();
676
794
  const venueList = list.filter(p => p.duration != null);
677
795
  const addonList = list.filter(p => p.duration == null);
678
796
  const venueStore = this.store.venueProducts?.store;
@@ -680,7 +798,9 @@ class VenueBookingImpl extends _BaseSales.BaseSalesImpl {
680
798
  venueStore.list = venueList.slice().sort((a, b) => Number(b.sort) - Number(a.sort));
681
799
  }
682
800
  this.store.addonProducts.addProduct(addonList);
683
- this.resourceProductMap = (0, _resource.buildResourceProductMap)(venueList);
801
+
802
+ // 无规则命中时仍应回到原始商品价,不能回退到加载时刻的智能价。
803
+ this.resourceProductMap = (0, _resource.buildResourceProductMap)(this.smartPricingProductCatalog.filter(product => product.duration != null));
684
804
 
685
805
  // 加载 holder 表单
686
806
  if (this.store.order) {
@@ -704,8 +824,10 @@ class VenueBookingImpl extends _BaseSales.BaseSalesImpl {
704
824
  }
705
825
  }
706
826
  async loadVenueProducts() {
707
- const result = await this.loadAllProducts();
708
- return result.venueProducts;
827
+ await this.loadAllProducts();
828
+ // 同步会话中已选的时段,避免新网格价格与恢复的购物车价格不一致。
829
+ await this.refreshSmartPricing();
830
+ return this.getVenueProducts();
709
831
  }
710
832
  async loadAddonProducts() {
711
833
  const result = await this.loadAllProducts();
@@ -866,31 +988,22 @@ class VenueBookingImpl extends _BaseSales.BaseSalesImpl {
866
988
  config: this.baseSlotConfig,
867
989
  rawResources: this.store.rawResourceData,
868
990
  resourceProductMap: this.resourceProductMap,
869
- quotationModule: this.store.quotation,
870
- channel: this.otherParams?.channel,
871
- resolveConfig: date => this.resolveSlotConfigForDate(date)
991
+ resolveConfig: date => this.resolveSlotConfigForDate(date),
992
+ buildSlotPriceMap: (date, config, productIds) => this.buildSlotPriceMapForDate(date, config, productIds)
872
993
  });
873
994
  }
874
995
  getTimeSlotGrid(date) {
875
996
  const resolvedSlotConfig = this.syncOperatingHoursToSlotConfig(date);
876
- let quotationPriceMap;
877
- if (this.store.quotation) {
878
- const productIds = [...new Set([...this.resourceProductMap.values()].flat().map(m => m.productId))];
879
- const timeLabels = (0, _timeSlot.generateTimeLabels)(resolvedSlotConfig);
880
- const timePoints = timeLabels.map(label => `${date} ${label}`);
881
- quotationPriceMap = this.store.quotation.buildProductPriceMap({
882
- productIds,
883
- timePoints,
884
- channel: this.otherParams?.channel
885
- });
886
- }
887
- return (0, _timeSlot.buildTimeSlotGrid)({
997
+ const productIds = [...new Set([...this.resourceProductMap.values()].flat().map(mapping => mapping.productId))];
998
+ const slotPriceMap = this.buildSlotPriceMapForDate(date, resolvedSlotConfig, productIds);
999
+ const grid = (0, _timeSlot.buildTimeSlotGrid)({
888
1000
  date,
889
1001
  config: resolvedSlotConfig,
890
1002
  rawResources: this.store.rawResourceData,
891
1003
  resourceProductMap: this.resourceProductMap,
892
- quotationPriceMap
1004
+ slotPriceMap
893
1005
  });
1006
+ return grid;
894
1007
  }
895
1008
 
896
1009
  // ─── 时间槽订单操作 ───
@@ -1199,9 +1312,7 @@ class VenueBookingImpl extends _BaseSales.BaseSalesImpl {
1199
1312
  booking_uid: bookingUuid,
1200
1313
  price_breakdown: (0, _slotMerge.buildPriceBreakdown)({
1201
1314
  group,
1202
- productId: mapping.productId,
1203
- quotation: this.store.quotation,
1204
- channel: this.otherParams?.channel
1315
+ productId: mapping.productId
1205
1316
  })
1206
1317
  },
1207
1318
  _origin: venueProductOrigin
@@ -1252,6 +1363,7 @@ class VenueBookingImpl extends _BaseSales.BaseSalesImpl {
1252
1363
  // ─── 配置 ───
1253
1364
 
1254
1365
  setSlotConfig(config) {
1366
+ this.slotPriceMapCache.clear();
1255
1367
  this.baseSlotConfig = {
1256
1368
  ...this.baseSlotConfig,
1257
1369
  ...config
@@ -1274,6 +1386,8 @@ class VenueBookingImpl extends _BaseSales.BaseSalesImpl {
1274
1386
  if (!this.store.schedule) throw new Error('schedule 模块未初始化');
1275
1387
  await this.store.schedule.loadAllSchedule();
1276
1388
  this.injectScheduleResolverToQuotation();
1389
+ this.slotPriceMapCache.clear();
1390
+ await this.refreshSmartPricing();
1277
1391
  this.logMethodSuccess('loadSchedules');
1278
1392
  } catch (error) {
1279
1393
  this.logMethodError('loadSchedules', error);
@@ -1313,7 +1427,7 @@ class VenueBookingImpl extends _BaseSales.BaseSalesImpl {
1313
1427
  customerId: params.customerId
1314
1428
  });
1315
1429
  try {
1316
- await this.refreshOrderMarketingAfterLogin({
1430
+ await this.refreshOrderMarketingForCustomer({
1317
1431
  customerId: params.customerId
1318
1432
  });
1319
1433
  this.logMethodSuccess('onCustomerLogin', {
@@ -1324,8 +1438,15 @@ class VenueBookingImpl extends _BaseSales.BaseSalesImpl {
1324
1438
  throw error;
1325
1439
  }
1326
1440
  }
1327
- recalculateOrderPricesFromQuotation() {
1328
- if (!this.store.order || !this.store.quotation) return;
1441
+
1442
+ /** 宿主退出登录或以匿名身份重新进入时,清理客户资产并恢复匿名价格。 */
1443
+ async onCustomerLogout() {
1444
+ await this.refreshOrderMarketingForCustomer({
1445
+ customerId: null
1446
+ });
1447
+ }
1448
+ recalculateOrderPricesFromSmartPricing() {
1449
+ if (!this.store.order) return;
1329
1450
  const tempOrder = this.store.order.getTempOrder();
1330
1451
  if (!tempOrder?.products?.length) return;
1331
1452
  const now = (0, _dayjs.default)().format('YYYY-MM-DD HH:mm:ss');
@@ -1336,40 +1457,67 @@ class VenueBookingImpl extends _BaseSales.BaseSalesImpl {
1336
1457
  const mapping = mappings.find(m => Number(m.productId) === Number(product.product_id)) || mappings[0];
1337
1458
  if (!mapping) continue;
1338
1459
  const slots = (0, _slotMerge.expandMergedSlotToIndividual)(product, this.store.slotConfig.slotDurationMinutes);
1339
- const updatedSlots = slots.map(slot => ({
1340
- ...slot,
1341
- productId: mapping.productId,
1342
- price: this.store.quotation.getPriceForProduct({
1460
+ const updatedSlots = slots.map(slot => {
1461
+ const slotStart = (0, _dayjs.default)(slot.startTime);
1462
+ let date = slotStart.format('YYYY-MM-DD');
1463
+ const previousDate = slotStart.subtract(1, 'day').format('YYYY-MM-DD');
1464
+ const previousConfig = this.resolveSlotConfigForDate(previousDate);
1465
+ if ((0, _timeSlot.isBusinessHoursCrossDay)(previousConfig) && slotStart.format('HH:mm') < previousConfig.businessStartTime) {
1466
+ date = previousDate;
1467
+ }
1468
+ const slotPriceMap = this.buildSlotPriceMapForDate(date, this.resolveSlotConfigForDate(date), [mapping.productId]);
1469
+ return {
1470
+ ...slot,
1343
1471
  productId: mapping.productId,
1344
- datetime: slot.startTime,
1345
- channel: this.otherParams?.channel
1346
- }) ?? mapping.price
1347
- }));
1472
+ price: (0, _smartPricing.getSlotPriceFromMap)({
1473
+ slotPriceMap,
1474
+ productId: mapping.productId,
1475
+ slotStartTime: slot.startTime,
1476
+ fallbackPrice: mapping.price
1477
+ })
1478
+ };
1479
+ });
1348
1480
  const merged = (0, _slotMerge.mergeConsecutiveSlots)(updatedSlots);
1349
1481
  if (merged.length === 1) {
1350
- product.selling_price = merged[0].totalPrice;
1351
- product.original_price = merged[0].totalPrice;
1482
+ this.resetProductPriceForSmartPricing(product, merged[0].totalPrice);
1352
1483
  product.metadata.price_breakdown = (0, _slotMerge.buildPriceBreakdown)({
1353
1484
  group: merged[0],
1354
- productId: mapping.productId,
1355
- quotation: this.store.quotation,
1356
- channel: this.otherParams?.channel
1485
+ productId: mapping.productId
1357
1486
  });
1358
1487
  }
1359
1488
  } else if (product.product_id != null) {
1360
- const quotationPrice = this.store.quotation.getPriceForProduct({
1489
+ const price = (0, _smartPricing.getPriceForProductAtDatetime)({
1490
+ products: this.smartPricingProductCatalog,
1491
+ context: this.buildSmartPricingContext(),
1361
1492
  productId: product.product_id,
1362
1493
  variantId: product.product_variant_id ?? undefined,
1363
1494
  datetime: now,
1364
- channel: this.otherParams?.channel
1495
+ fallbackPrice: product.metadata?.source_product_price ?? product.original_price
1365
1496
  });
1366
- if (quotationPrice !== null) {
1367
- product.selling_price = quotationPrice;
1368
- product.original_price = quotationPrice;
1369
- }
1497
+ this.resetProductPriceForSmartPricing(product, price);
1370
1498
  }
1371
1499
  }
1372
1500
  }
1501
+ resetProductPriceForSmartPricing(product, price) {
1502
+ const mainPrice = new _decimal.default(price).plus((0, _utils3.sumOptionUnitPrice)((0, _utils3.getProductSkuOptions)(product))).toFixed(2);
1503
+ product.metadata = {
1504
+ ...product.metadata,
1505
+ source_product_price: price,
1506
+ main_product_original_price: mainPrice,
1507
+ main_product_selling_price: mainPrice,
1508
+ price_schema_version: 2
1509
+ };
1510
+ product.discount_list = [];
1511
+ product.selling_price = (0, _utils3.composeLinePrice)({
1512
+ mainPrice,
1513
+ bundle: product.product_bundle
1514
+ });
1515
+ product.original_price = (0, _utils3.composeLinePrice)({
1516
+ mainPrice,
1517
+ bundle: product.product_bundle,
1518
+ useOriginalBundle: true
1519
+ });
1520
+ }
1373
1521
  async scanCode(code, customerId) {
1374
1522
  this.logMethodStart('scanCode', {
1375
1523
  code
@@ -1565,19 +1713,16 @@ class VenueBookingImpl extends _BaseSales.BaseSalesImpl {
1565
1713
  });
1566
1714
  try {
1567
1715
  if (!this.store.order) throw new Error('order 模块未初始化');
1568
- if (!product.metadata?.venue_booking && this.store.quotation && product.product_id != null) {
1569
- const quotationPrice = this.store.quotation.getPriceForProduct({
1716
+ if (!product.metadata?.venue_booking && product.product_id != null) {
1717
+ const price = (0, _smartPricing.getPriceForProductAtDatetime)({
1718
+ products: this.smartPricingProductCatalog,
1719
+ context: this.buildSmartPricingContext(),
1570
1720
  productId: product.product_id,
1571
1721
  variantId: product.product_variant_id ?? undefined,
1572
1722
  datetime: (0, _dayjs.default)().format('YYYY-MM-DD HH:mm:ss'),
1573
- channel: this.otherParams?.channel
1723
+ fallbackPrice: product.metadata?.source_product_price ?? product.selling_price
1574
1724
  });
1575
- if (quotationPrice !== null) {
1576
- product.selling_price = quotationPrice;
1577
- product.original_price = quotationPrice;
1578
- } else if (product.selling_price != null) {
1579
- product.original_price = product.selling_price;
1580
- }
1725
+ this.resetProductPriceForSmartPricing(product, price);
1581
1726
  }
1582
1727
  const products = await this.store.order.addProductToOrder(product);
1583
1728
  await this.refreshItemRuleQuantityLimits();
@@ -2093,6 +2238,7 @@ class VenueBookingImpl extends _BaseSales.BaseSalesImpl {
2093
2238
  async setOtherParams(params, {
2094
2239
  cover = false
2095
2240
  } = {}) {
2241
+ this.slotPriceMapCache.clear();
2096
2242
  if (cover) {
2097
2243
  this.otherParams = params;
2098
2244
  } else {
@@ -7,7 +7,8 @@ export declare enum VenueBookingHooks {
7
7
  onDestroy = "venueBooking:onDestroy",
8
8
  onRetryInit = "venueBooking:onRetryInit",
9
9
  onRefresh = "venueBooking:onRefresh",
10
- onCartValidationChanged = "venueBooking:onCartValidationChanged"
10
+ onCartValidationChanged = "venueBooking:onCartValidationChanged",
11
+ onSmartPricingUpdated = "venueBooking:onSmartPricingUpdated"
11
12
  }
12
13
  export type VenueBookingStatus = 'idle' | 'initializing' | 'ready' | 'error';
13
14
  export interface VenueBookingEntryContext {
@@ -10,6 +10,7 @@ let VenueBookingHooks = exports.VenueBookingHooks = /*#__PURE__*/function (Venue
10
10
  VenueBookingHooks["onRetryInit"] = "venueBooking:onRetryInit";
11
11
  VenueBookingHooks["onRefresh"] = "venueBooking:onRefresh";
12
12
  VenueBookingHooks["onCartValidationChanged"] = "venueBooking:onCartValidationChanged";
13
+ VenueBookingHooks["onSmartPricingUpdated"] = "venueBooking:onSmartPricingUpdated";
13
14
  return VenueBookingHooks;
14
15
  }({}); // ─── 场地预定核心类型 ───
15
16
  const DEFAULT_SLOT_CONFIG = exports.DEFAULT_SLOT_CONFIG = {
@@ -9,4 +9,5 @@ export declare function buildDateRangeSummary(params: {
9
9
  quotationModule?: QuotationModule;
10
10
  channel?: string;
11
11
  resolveConfig?: (date: string) => VenueBookingSlotConfig;
12
+ buildSlotPriceMap?: (date: string, config: VenueBookingSlotConfig, productIds: number[]) => Map<string, string | null>;
12
13
  }): VenueDateSummaryItem[];
@@ -16,17 +16,19 @@ function buildDateRangeSummary(params) {
16
16
  resourceProductMap,
17
17
  quotationModule,
18
18
  channel,
19
- resolveConfig
19
+ resolveConfig,
20
+ buildSlotPriceMap
20
21
  } = params;
21
22
  const result = [];
22
- const productIds = quotationModule ? [...new Set([...resourceProductMap.values()].flat().map(m => m.productId))] : [];
23
+ const productIds = buildSlotPriceMap || quotationModule ? [...new Set([...resourceProductMap.values()].flat().map(m => m.productId))] : [];
23
24
  let cursor = (0, _dayjs.default)(startDate);
24
25
  const end = (0, _dayjs.default)(endDate);
25
26
  while (!cursor.isAfter(end)) {
26
27
  const date = cursor.format('YYYY-MM-DD');
27
28
  const effectiveConfig = resolveConfig?.(date) || config;
28
29
  let quotationPriceMap;
29
- if (quotationModule && productIds.length) {
30
+ const slotPriceMap = buildSlotPriceMap?.(date, effectiveConfig, productIds);
31
+ if (!slotPriceMap && quotationModule && productIds.length) {
30
32
  const timeLabels = (0, _timeSlot.generateTimeLabels)(effectiveConfig);
31
33
  const timePoints = timeLabels.map(label => `${date} ${label}`);
32
34
  quotationPriceMap = quotationModule.buildProductPriceMap({
@@ -40,7 +42,8 @@ function buildDateRangeSummary(params) {
40
42
  config: effectiveConfig,
41
43
  rawResources,
42
44
  resourceProductMap,
43
- quotationPriceMap
45
+ quotationPriceMap,
46
+ slotPriceMap
44
47
  });
45
48
  let totalSlots = 0;
46
49
  let availableSlots = 0;
@@ -0,0 +1,71 @@
1
+ import type { ProductData } from '../../../modules/Product/types';
2
+ import type { ScheduleItem } from '../../../modules/Schedule/types';
3
+ import type { DataVariantBusinessData, DataVariantMenu, DataVariantProduct } from '../../../model/strategy/adapter/dataVariant/type';
4
+ import type { DataVariantEvaluator } from '../../../model/strategy/adapter/dataVariant/evaluator';
5
+ import type { StrategyConfig } from '../../../model/strategy/type';
6
+ import type { VenueBookingSlotConfig } from '../types';
7
+ /** VenueBooking 智能定价运行时上下文 */
8
+ export interface VenueBookingSmartPricingContext {
9
+ evaluator?: DataVariantEvaluator | null;
10
+ scheduleList?: ScheduleItem[];
11
+ menuList?: DataVariantMenu[];
12
+ customerId?: number | string;
13
+ availableWalletIds?: Array<number | string>;
14
+ channel?: string;
15
+ businessCode?: string;
16
+ orderType?: string;
17
+ strategy_context?: Record<string, any>;
18
+ strategyConfigs?: StrategyConfig[];
19
+ }
20
+ /** 从商品上读取展示/算价用的 price 字段 */
21
+ export declare function extractProductPrice(product: DataVariantProduct): string | null;
22
+ /** 组装 DataVariant 评估入参,对齐 OS Server buildProductFormatterContext 语义 */
23
+ export declare function buildDataVariantBusinessData(products: ProductData[], context: VenueBookingSmartPricingContext, scheduleDateTime: string): DataVariantBusinessData;
24
+ /**
25
+ * 对商品列表执行一次智能定价格式化(loadProducts 后使用)。
26
+ * evaluator 未注入时原样返回并打 warning。
27
+ */
28
+ export declare function formatProductsWithDataVariant(products: ProductData[], context: VenueBookingSmartPricingContext, scheduleDateTime?: string): ProductData[];
29
+ /**
30
+ * 根据 schedule_time_points 与营业开始时间,构造当天需要评估的 datetime 列表(升序)。
31
+ * 跨日营业时,早于营业开始小时的时间点归到次日。
32
+ */
33
+ export declare function buildEvaluationDatetimes(params: {
34
+ date: string;
35
+ config: VenueBookingSlotConfig;
36
+ scheduleTimePoints: string[];
37
+ }): string[];
38
+ /** 找到 slot 时间所属分段的评估 datetime(max(evalDt <= slotDt)) */
39
+ export declare function findSegmentEvalDatetime(slotDatetime: string, evalDatetimes: string[]): string;
40
+ /** 将 slot 标签转为与 timeSlot 网格一致的 slotStart 字符串 */
41
+ export declare function buildSlotStartDatetime(date: string, label: string, config: VenueBookingSlotConfig): string;
42
+ /**
43
+ * 按 schedule 分段批量 resolveProducts,输出与 quotation 相同 key 格式的 slot 价格 Map。
44
+ * key: `${productId}:${slotStart}`,value: price | null
45
+ */
46
+ export declare function buildSlotPriceMapByScheduleSegments(params: {
47
+ products: ProductData[];
48
+ productIds: number[];
49
+ date: string;
50
+ timeLabels: string[];
51
+ config: VenueBookingSlotConfig;
52
+ context: VenueBookingSmartPricingContext;
53
+ }): Map<string, string | null>;
54
+ /**
55
+ * 指定商品在某一 datetime 的智能定价(addon / 单点重算用)。
56
+ */
57
+ export declare function getPriceForProductAtDatetime(params: {
58
+ products: ProductData[];
59
+ context: VenueBookingSmartPricingContext;
60
+ productId: number;
61
+ variantId?: number;
62
+ datetime: string;
63
+ fallbackPrice?: string;
64
+ }): string;
65
+ /** 从 slot 价格 Map 读取单价,找不到则回退 fallback */
66
+ export declare function getSlotPriceFromMap(params: {
67
+ slotPriceMap: Map<string, string | null>;
68
+ productId: number;
69
+ slotStartTime: string;
70
+ fallbackPrice?: string;
71
+ }): string;