@pisell/pisellos 2.3.74 → 2.3.76
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/core/index.js +6 -10
- package/dist/model/strategy/adapter/promotion/index.js +9 -0
- package/dist/model/strategy/adapter/walletPass/example.js +3 -26
- package/dist/model/strategy/strategy-example.js +5 -19
- package/dist/modules/Cart/index.js +0 -3
- package/dist/modules/Cart/utils/cartProduct.js +0 -1
- package/dist/modules/Date/index.js +1 -5
- package/dist/modules/Discount/index.js +2 -4
- package/dist/modules/OpenData/index.js +0 -2
- package/dist/modules/Order/index.d.ts +1 -1
- package/dist/modules/Order/index.js +75 -91
- package/dist/modules/Order/types.d.ts +3 -1
- package/dist/modules/Payment/index.js +97 -162
- package/dist/modules/Rules/index.js +1 -4
- package/dist/modules/Schedule/index.js +0 -1
- package/dist/modules/Schedule/utils.js +0 -1
- package/dist/modules/Step/index.js +0 -1
- package/dist/plugins/request.js +1 -4
- package/dist/plugins/window.js +2 -6
- package/dist/server/index.js +97 -129
- package/dist/server/modules/menu/index.js +32 -41
- package/dist/server/modules/order/index.js +2 -4
- package/dist/server/modules/products/index.js +24 -42
- package/dist/server/modules/quotation/index.js +29 -38
- package/dist/server/modules/resource/index.js +3 -4
- package/dist/server/modules/schedule/index.js +27 -35
- package/dist/server/utils/product.js +0 -1
- package/dist/solution/BaseSales/index.js +100 -72
- package/dist/solution/BaseSales/utils/cartPromotion.d.ts +5 -0
- package/dist/solution/BaseSales/utils/cartPromotion.js +15 -6
- package/dist/solution/BookingByStep/index.js +7 -59
- package/dist/solution/BookingByStep/utils/capacity.js +1 -11
- package/dist/solution/BookingByStep/utils/resources.js +1 -3
- package/dist/solution/BookingByStep/utils/timeslots.js +2 -43
- package/dist/solution/BookingTicket/index.js +0 -2
- package/dist/solution/BookingTicket/utils/scan/cloudSearch.js +6 -7
- package/dist/solution/BookingTicket/utils/scan/handleScan.js +25 -31
- package/dist/solution/BookingTicket/utils/scan/index.js +6 -10
- package/dist/solution/BuyTickets/index.js +9 -12
- package/dist/solution/Checkout/index.js +177 -252
- package/dist/solution/Checkout/utils/index.js +4 -16
- package/dist/solution/RegisterAndLogin/index.js +30 -76
- package/dist/solution/ScanOrder/index.js +50 -60
- package/dist/solution/ShopDiscount/index.js +2 -7
- package/dist/solution/VenueBooking/index.js +45 -55
- package/dist/utils/task.js +15 -18
- package/dist/utils/watch.js +0 -1
- package/lib/apis/picoding.js +2 -0
- package/lib/core/index.js +134 -134
- package/lib/effects/index.js +46 -57
- package/lib/index.js +82 -49
- package/lib/model/index.js +14 -21
- package/lib/model/strategy/adapter/dataVariant/adapter.js +38 -50
- package/lib/model/strategy/adapter/dataVariant/evaluator.js +162 -236
- package/lib/model/strategy/adapter/dataVariant/examples.js +229 -237
- package/lib/model/strategy/adapter/dataVariant/index.js +39 -34
- package/lib/model/strategy/adapter/dataVariant/type.js +57 -28
- package/lib/model/strategy/adapter/index.js +100 -64
- package/lib/model/strategy/adapter/itemRule/adapter.js +156 -144
- package/lib/model/strategy/adapter/itemRule/evaluator.js +36 -48
- package/lib/model/strategy/adapter/itemRule/examples.js +295 -225
- package/lib/model/strategy/adapter/itemRule/index.js +83 -57
- package/lib/model/strategy/adapter/itemRule/type.js +97 -36
- package/lib/model/strategy/adapter/promotion/adapter.js +91 -69
- package/lib/model/strategy/adapter/promotion/evaluator.js +444 -310
- package/lib/model/strategy/adapter/promotion/examples.js +222 -234
- package/lib/model/strategy/adapter/promotion/index.js +1 -0
- package/lib/model/strategy/adapter/promotion/type.js +243 -36
- package/lib/model/strategy/adapter/type.js +4 -16
- package/lib/model/strategy/adapter/walletPass/evaluator.js +180 -150
- package/lib/model/strategy/adapter/walletPass/example.js +217 -190
- package/lib/model/strategy/adapter/walletPass/index.js +75 -51
- package/lib/model/strategy/adapter/walletPass/locales.js +36 -58
- package/lib/model/strategy/adapter/walletPass/type.js +4 -16
- package/lib/model/strategy/adapter/walletPass/utils.js +588 -378
- package/lib/model/strategy/engine.js +270 -168
- package/lib/model/strategy/index.js +49 -34
- package/lib/model/strategy/strategy-example.js +243 -239
- package/lib/model/strategy/type.js +100 -40
- package/lib/modules/Account/index.js +39 -53
- package/lib/modules/Account/types.js +20 -33
- package/lib/modules/AccountList/index.js +116 -154
- package/lib/modules/AccountList/types.js +30 -31
- package/lib/modules/AccountList/utils.js +18 -30
- package/lib/modules/BaseModule.js +23 -42
- package/lib/modules/BookingContext/index.js +158 -136
- package/lib/modules/BookingContext/types.js +46 -32
- package/lib/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +138 -146
- package/lib/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.js +78 -83
- package/lib/modules/BookingContext/utils/cacheItemToBookingInput.js +170 -150
- package/lib/modules/BookingContext/utils/flexible.js +38 -55
- package/lib/modules/BookingContext/utils/formatResourceList.js +19 -34
- package/lib/modules/BookingContext/utils/index.js +124 -41
- package/lib/modules/BookingContext/utils/noResource.js +58 -70
- package/lib/modules/BookingContext/utils/orderLineDisplay.js +14 -41
- package/lib/modules/BookingContext/utils/productExtend.js +202 -155
- package/lib/modules/BookingContext/utils/resourceErrors.js +53 -53
- package/lib/modules/BookingContext/utils/resourceSelection.js +19 -41
- package/lib/modules/BookingContext/utils/resources.js +209 -167
- package/lib/modules/BookingContext/utils/serviceTimes.js +115 -116
- package/lib/modules/BookingContext/utils/timeSlices.js +76 -86
- package/lib/modules/Cart/index.js +170 -124
- package/lib/modules/Cart/types.js +46 -51
- package/lib/modules/Cart/utils/cartAccount.js +40 -39
- package/lib/modules/Cart/utils/cartDate.js +56 -61
- package/lib/modules/Cart/utils/cartDiscount.js +28 -33
- package/lib/modules/Cart/utils/cartNote.js +27 -32
- package/lib/modules/Cart/utils/cartProduct.js +321 -251
- package/lib/modules/Cart/utils/cartRelationForms.js +29 -35
- package/lib/modules/Cart/utils/cartResource.js +70 -78
- package/lib/modules/Cart/utils/changePrice.js +22 -50
- package/lib/modules/Cart/utils/index.js +106 -48
- package/lib/modules/Customer/constants.js +13 -34
- package/lib/modules/Customer/index.js +235 -172
- package/lib/modules/Customer/types.js +38 -35
- package/lib/modules/Date/index.js +116 -115
- package/lib/modules/Date/types.js +16 -28
- package/lib/modules/Date/utils.js +174 -123
- package/lib/modules/Discount/index.js +122 -127
- package/lib/modules/Discount/types.js +9 -31
- package/lib/modules/Guests/index.js +30 -56
- package/lib/modules/Guests/types.js +23 -33
- package/lib/modules/Holder/index.js +209 -189
- package/lib/modules/Holder/types.js +4 -16
- package/lib/modules/Holder/utils.js +20 -49
- package/lib/modules/OpenData/index.js +70 -76
- package/lib/modules/OpenData/types.js +4 -16
- package/lib/modules/OpenData/utils.js +44 -78
- package/lib/modules/Order/index.d.ts +1 -1
- package/lib/modules/Order/index.js +1733 -2121
- package/lib/modules/Order/payment-utils.js +77 -120
- package/lib/modules/Order/types.d.ts +3 -1
- package/lib/modules/Order/types.js +88 -35
- package/lib/modules/Order/utils/bundleDiscountHydration.js +41 -74
- package/lib/modules/Order/utils/discountProductLineIdentity.js +142 -100
- package/lib/modules/Order/utils/manualProductDiscount.js +124 -123
- package/lib/modules/Order/utils/orderCollectionIdentity.js +184 -244
- package/lib/modules/Order/utils.js +814 -647
- package/lib/modules/Payment/cash.js +20 -33
- package/lib/modules/Payment/cashRecommendationAlgorithm.js +145 -76
- package/lib/modules/Payment/eftpos.js +16 -30
- package/lib/modules/Payment/index.js +532 -399
- package/lib/modules/Payment/mx51.js +1 -0
- package/lib/modules/Payment/types.js +230 -108
- package/lib/modules/Payment/utils.js +52 -51
- package/lib/modules/Payment/walletpass.js +485 -660
- package/lib/modules/Product/index.js +51 -46
- package/lib/modules/Product/types.js +4 -16
- package/lib/modules/Product/utils.js +32 -33
- package/lib/modules/ProductList/index.js +113 -123
- package/lib/modules/ProductList/types.js +9 -31
- package/lib/modules/Quotation/index.js +81 -118
- package/lib/modules/Quotation/types.js +4 -16
- package/lib/modules/Resource/index.js +27 -59
- package/lib/modules/Resource/types.js +22 -32
- package/lib/modules/Resource/utils.js +30 -38
- package/lib/modules/ResourcePlanner/index.js +25 -23
- package/lib/modules/ResourcePlanner/localClock.js +54 -119
- package/lib/modules/ResourcePlanner/planner.js +589 -770
- package/lib/modules/ResourcePlanner/types.js +10 -29
- package/lib/modules/Rules/index.js +1218 -959
- package/lib/modules/Rules/types.js +17 -40
- package/lib/modules/SalesSummary/index.js +85 -90
- package/lib/modules/SalesSummary/types.js +4 -16
- package/lib/modules/SalesSummary/utils.js +355 -430
- package/lib/modules/ScanOrderLogger/index.js +59 -79
- package/lib/modules/ScanOrderLogger/providers/feishu.js +54 -78
- package/lib/modules/ScanOrderLogger/providers/grafana.js +13 -36
- package/lib/modules/ScanOrderLogger/types.js +4 -16
- package/lib/modules/Schedule/getDateIsInSchedule.js +334 -163
- package/lib/modules/Schedule/index.js +100 -114
- package/lib/modules/Schedule/type.js +4 -16
- package/lib/modules/Schedule/types.js +4 -16
- package/lib/modules/Schedule/utils.js +433 -291
- package/lib/modules/Step/index.js +40 -52
- package/lib/modules/Step/tyeps.js +4 -16
- package/lib/modules/Summary/index.js +66 -71
- package/lib/modules/Summary/types.js +4 -16
- package/lib/modules/Summary/utils.js +773 -418
- package/lib/modules/SurchargeList/index.js +46 -60
- package/lib/modules/SurchargeList/types.js +4 -16
- package/lib/modules/index.js +245 -63
- package/lib/plugins/app.js +4 -16
- package/lib/plugins/index.js +36 -25
- package/lib/plugins/request.js +137 -126
- package/lib/plugins/shopStore.js +4 -16
- package/lib/plugins/user.js +4 -16
- package/lib/plugins/window.js +171 -179
- package/lib/server/index.js +3050 -2665
- package/lib/server/modules/floor-plan/index.js +118 -134
- package/lib/server/modules/floor-plan/types.js +15 -30
- package/lib/server/modules/index.js +106 -68
- package/lib/server/modules/menu/index.js +142 -122
- package/lib/server/modules/menu/types.js +18 -31
- package/lib/server/modules/order/index.js +784 -1002
- package/lib/server/modules/order/types.js +122 -33
- package/lib/server/modules/order/utils/filterBookings.js +219 -194
- package/lib/server/modules/order/utils/filterOrders.js +118 -92
- package/lib/server/modules/payment/index.js +59 -83
- package/lib/server/modules/payment/types.js +4 -16
- package/lib/server/modules/products/index.js +583 -471
- package/lib/server/modules/products/types.js +44 -34
- package/lib/server/modules/quotation/index.js +137 -172
- package/lib/server/modules/quotation/types.js +21 -31
- package/lib/server/modules/resource/index.js +220 -186
- package/lib/server/modules/resource/types.js +42 -33
- package/lib/server/modules/schedule/index.js +135 -123
- package/lib/server/modules/schedule/types.js +14 -21
- package/lib/server/modules/schedule/utils.js +334 -163
- package/lib/server/types.js +4 -16
- package/lib/server/utils/index.js +25 -23
- package/lib/server/utils/product.js +196 -139
- package/lib/server/utils/schedule.js +34 -41
- package/lib/server/utils/small-ticket.js +479 -456
- package/lib/server/utils/time.js +40 -49
- package/lib/solution/BaseSales/index.js +1220 -1412
- package/lib/solution/BaseSales/types.js +42 -38
- package/lib/solution/BaseSales/utils/cartPromotion.d.ts +5 -0
- package/lib/solution/BaseSales/utils/cartPromotion.js +652 -498
- package/lib/solution/BaseSales/utils/parseSalesResponse.js +85 -105
- package/lib/solution/BaseSales/utils/quotationPrice.js +56 -114
- package/lib/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +108 -145
- package/lib/solution/BaseSales/utils.js +158 -143
- package/lib/solution/BookingByStep/index.js +1527 -1340
- package/lib/solution/BookingByStep/types.js +40 -99
- package/lib/solution/BookingByStep/utils/capacity.js +192 -160
- package/lib/solution/BookingByStep/utils/products.js +42 -52
- package/lib/solution/BookingByStep/utils/resources.js +527 -330
- package/lib/solution/BookingByStep/utils/stock.js +65 -44
- package/lib/solution/BookingByStep/utils/timeslots.js +129 -131
- package/lib/solution/BookingTicket/index.js +607 -522
- package/lib/solution/BookingTicket/types.js +99 -77
- package/lib/solution/BookingTicket/utils/addProductDecision.js +226 -189
- package/lib/solution/BookingTicket/utils/addTimeAvailability.js +58 -89
- package/lib/solution/BookingTicket/utils/bookingStatus.js +107 -78
- package/lib/solution/BookingTicket/utils/cartView.js +114 -99
- package/lib/solution/BookingTicket/utils/exampleData.js +185 -0
- package/lib/solution/BookingTicket/utils/orderCustomer.js +18 -41
- package/lib/solution/BookingTicket/utils/resolveBestAddTimePlan.js +219 -86
- package/lib/solution/BookingTicket/utils/scan/applyGlobalScan.js +168 -116
- package/lib/solution/BookingTicket/utils/scan/cloudSearch.js +79 -91
- package/lib/solution/BookingTicket/utils/scan/formatGlobalScan.js +54 -41
- package/lib/solution/BookingTicket/utils/scan/handleScan.js +84 -81
- package/lib/solution/BookingTicket/utils/scan/index.js +98 -104
- package/lib/solution/BookingTicket/utils/scan/scanCache.js +90 -43
- package/lib/solution/BookingTicket/utils/scan/scanProductValues.js +52 -64
- package/lib/solution/BookingTicket/utils/sessionCatalogStale.js +56 -58
- package/lib/solution/BookingTicket/utils/weighingProductSku.js +34 -48
- package/lib/solution/BuyTickets/index.js +67 -70
- package/lib/solution/BuyTickets/types.js +22 -38
- package/lib/solution/Checkout/index.js +1451 -1158
- package/lib/solution/Checkout/types.js +91 -61
- package/lib/solution/Checkout/utils/index.js +228 -156
- package/lib/solution/PlaceOrder/index.js +55 -0
- package/lib/solution/RegisterAndLogin/config.js +493 -460
- package/lib/solution/RegisterAndLogin/index.js +633 -630
- package/lib/solution/RegisterAndLogin/types.js +189 -76
- package/lib/solution/RegisterAndLogin/utils.js +183 -125
- package/lib/solution/Sales/index.js +328 -324
- package/lib/solution/Sales/types.js +27 -33
- package/lib/solution/ScanOrder/index.js +832 -864
- package/lib/solution/ScanOrder/types.js +33 -34
- package/lib/solution/ScanOrder/utils.js +409 -374
- package/lib/solution/ShopDiscount/index.js +226 -232
- package/lib/solution/ShopDiscount/types.js +15 -37
- package/lib/solution/ShopDiscount/utils.js +300 -172
- package/lib/solution/UnifiedBookingSales/index.js +1035 -1201
- package/lib/solution/UnifiedBookingSales/remoteOccupancyCache.js +99 -159
- package/lib/solution/UnifiedBookingSales/types.js +10 -32
- package/lib/solution/VenueBooking/index.js +811 -879
- package/lib/solution/VenueBooking/types.js +19 -39
- package/lib/solution/VenueBooking/utils/dateSummary.js +27 -56
- package/lib/solution/VenueBooking/utils/resource.js +31 -54
- package/lib/solution/VenueBooking/utils/slotMerge.js +94 -107
- package/lib/solution/VenueBooking/utils/timeSlot.js +132 -132
- package/lib/solution/VenueBooking/utils.js +130 -67
- package/lib/solution/index.js +124 -41
- package/lib/store/createStore.js +32 -29
- package/lib/types/index.js +4 -16
- package/lib/utils/payment-number.js +26 -46
- package/lib/utils/task.js +36 -36
- package/lib/utils/watch.js +81 -47
- package/package.json +2 -1
|
@@ -1,122 +1,128 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
-
var __export = (target, all) => {
|
|
8
|
-
for (var name in all)
|
|
9
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
-
};
|
|
11
|
-
var __copyProps = (to, from, except, desc) => {
|
|
12
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
-
for (let key of __getOwnPropNames(from))
|
|
14
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
-
}
|
|
17
|
-
return to;
|
|
18
|
-
};
|
|
19
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
-
mod
|
|
26
|
-
));
|
|
27
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
-
|
|
29
|
-
// src/model/strategy/adapter/promotion/evaluator.ts
|
|
30
|
-
var evaluator_exports = {};
|
|
31
|
-
__export(evaluator_exports, {
|
|
32
|
-
PromotionEvaluator: () => PromotionEvaluator
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
33
5
|
});
|
|
34
|
-
|
|
35
|
-
var
|
|
36
|
-
var
|
|
37
|
-
var
|
|
38
|
-
var
|
|
39
|
-
|
|
6
|
+
exports.PromotionEvaluator = void 0;
|
|
7
|
+
var _decimal = _interopRequireDefault(require("decimal.js"));
|
|
8
|
+
var _engine = require("../../engine");
|
|
9
|
+
var _adapter = require("./adapter");
|
|
10
|
+
var _type = require("./type");
|
|
11
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
12
|
+
// ============================================
|
|
13
|
+
// 类型定义
|
|
14
|
+
// ============================================
|
|
15
|
+
|
|
16
|
+
// ============================================
|
|
17
|
+
// 多语言定义
|
|
18
|
+
// ============================================
|
|
19
|
+
|
|
20
|
+
const defaultLocales = {
|
|
40
21
|
en: {
|
|
41
|
-
no_applicable_promotion:
|
|
42
|
-
promotion_not_in_time_range:
|
|
43
|
-
product_not_in_promotion:
|
|
22
|
+
no_applicable_promotion: 'No applicable promotion',
|
|
23
|
+
promotion_not_in_time_range: 'Promotion not in valid time range',
|
|
24
|
+
product_not_in_promotion: 'Product not in promotion scope'
|
|
44
25
|
},
|
|
45
|
-
|
|
46
|
-
no_applicable_promotion:
|
|
47
|
-
promotion_not_in_time_range:
|
|
48
|
-
product_not_in_promotion:
|
|
26
|
+
'zh-CN': {
|
|
27
|
+
no_applicable_promotion: '无适用的促销活动',
|
|
28
|
+
promotion_not_in_time_range: '促销活动不在有效时间范围内',
|
|
29
|
+
product_not_in_promotion: '商品不在促销范围内'
|
|
49
30
|
},
|
|
50
|
-
|
|
51
|
-
no_applicable_promotion:
|
|
52
|
-
promotion_not_in_time_range:
|
|
53
|
-
product_not_in_promotion:
|
|
31
|
+
'zh-HK': {
|
|
32
|
+
no_applicable_promotion: '無適用的促銷活動',
|
|
33
|
+
promotion_not_in_time_range: '促銷活動不在有效時間範圍內',
|
|
34
|
+
product_not_in_promotion: '商品不在促銷範圍內'
|
|
54
35
|
}
|
|
55
36
|
};
|
|
56
|
-
|
|
37
|
+
|
|
38
|
+
// ============================================
|
|
39
|
+
// PromotionEvaluator 实现
|
|
40
|
+
// ============================================
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Promotion 评估器
|
|
44
|
+
*
|
|
45
|
+
* 用于评估商品适用的促销活动
|
|
46
|
+
* 封装了 StrategyEngine 和 PromotionAdapter 的调用逻辑
|
|
47
|
+
*/
|
|
48
|
+
class PromotionEvaluator {
|
|
49
|
+
engine;
|
|
50
|
+
adapter;
|
|
51
|
+
strategyConfigs = [];
|
|
52
|
+
locale = 'en';
|
|
53
|
+
locales = defaultLocales;
|
|
57
54
|
constructor() {
|
|
58
|
-
this.
|
|
59
|
-
this.locale = "en";
|
|
60
|
-
this.locales = defaultLocales;
|
|
61
|
-
this.engine = new import_engine.StrategyEngine({
|
|
55
|
+
this.engine = new _engine.StrategyEngine({
|
|
62
56
|
debug: false,
|
|
63
57
|
enableTrace: false
|
|
64
58
|
});
|
|
65
|
-
this.adapter = new
|
|
59
|
+
this.adapter = new _adapter.PromotionAdapter();
|
|
66
60
|
}
|
|
61
|
+
|
|
67
62
|
// ============================================
|
|
68
63
|
// 配置管理
|
|
69
64
|
// ============================================
|
|
65
|
+
|
|
70
66
|
/**
|
|
71
67
|
* 设置策略配置列表
|
|
72
68
|
*/
|
|
73
69
|
setStrategyConfigs(strategyConfigs) {
|
|
74
|
-
const newStrategyConfigs = strategyConfigs.filter(
|
|
70
|
+
const newStrategyConfigs = strategyConfigs.filter(item => item.metadata.type === 'promotion');
|
|
75
71
|
this.strategyConfigs = newStrategyConfigs;
|
|
76
72
|
}
|
|
73
|
+
|
|
77
74
|
/**
|
|
78
75
|
* 获取策略配置列表
|
|
79
76
|
*/
|
|
80
77
|
getStrategyConfigs() {
|
|
81
78
|
return this.strategyConfigs;
|
|
82
79
|
}
|
|
80
|
+
|
|
83
81
|
/**
|
|
84
82
|
* 添加策略配置
|
|
85
83
|
*/
|
|
86
84
|
addStrategyConfig(strategyConfig) {
|
|
87
85
|
this.strategyConfigs.push(strategyConfig);
|
|
88
86
|
}
|
|
87
|
+
|
|
89
88
|
/**
|
|
90
89
|
* 设置语言
|
|
91
90
|
*/
|
|
92
91
|
setLocale(locale) {
|
|
93
92
|
this.locale = locale;
|
|
94
93
|
}
|
|
94
|
+
|
|
95
95
|
/**
|
|
96
96
|
* 设置自定义多语言文案
|
|
97
97
|
*/
|
|
98
98
|
setLocales(locales) {
|
|
99
|
-
this.locales = {
|
|
99
|
+
this.locales = {
|
|
100
|
+
...defaultLocales,
|
|
101
|
+
...locales
|
|
102
|
+
};
|
|
100
103
|
}
|
|
104
|
+
|
|
101
105
|
/**
|
|
102
106
|
* 获取多语言文案
|
|
103
107
|
*/
|
|
104
108
|
getText(key) {
|
|
105
|
-
|
|
106
|
-
return ((_b = (_a = this.locales) == null ? void 0 : _a[this.locale]) == null ? void 0 : _b[key]) || key;
|
|
109
|
+
return this.locales?.[this.locale]?.[key] || key;
|
|
107
110
|
}
|
|
111
|
+
|
|
108
112
|
/**
|
|
109
113
|
* 获取多语言名称
|
|
110
114
|
*/
|
|
111
115
|
getLocalizedName(name) {
|
|
112
|
-
if (typeof name ===
|
|
116
|
+
if (typeof name === 'string') {
|
|
113
117
|
return name;
|
|
114
118
|
}
|
|
115
|
-
return name[this.locale] || name[
|
|
119
|
+
return name[this.locale] || name['en'] || Object.values(name)[0] || '';
|
|
116
120
|
}
|
|
121
|
+
|
|
117
122
|
// ============================================
|
|
118
123
|
// 核心评估方法
|
|
119
124
|
// ============================================
|
|
125
|
+
|
|
120
126
|
/**
|
|
121
127
|
* 评估商品列表
|
|
122
128
|
*
|
|
@@ -127,22 +133,35 @@ var PromotionEvaluator = class {
|
|
|
127
133
|
* @returns 每个商品的促销评估结果
|
|
128
134
|
*/
|
|
129
135
|
evaluateProducts(input) {
|
|
130
|
-
const {
|
|
136
|
+
const {
|
|
137
|
+
products,
|
|
138
|
+
strategyConfigs,
|
|
139
|
+
channel
|
|
140
|
+
} = input;
|
|
131
141
|
const configs = strategyConfigs || this.strategyConfigs;
|
|
132
142
|
const results = [];
|
|
133
143
|
for (const product of products) {
|
|
134
144
|
const applicablePromotions = [];
|
|
145
|
+
|
|
146
|
+
// 遍历所有策略,检查此商品是否适用
|
|
135
147
|
for (const config of configs) {
|
|
136
148
|
if (this.shouldSkipStrategyForProduct(product, config)) {
|
|
137
149
|
continue;
|
|
138
150
|
}
|
|
151
|
+
|
|
152
|
+
// 1. 先用 getProductMatchInfo 检查商品是否在策略的适用范围内(包含 bundle 子商品检查)
|
|
139
153
|
const matchInfo = this.getProductMatchInfo(product, config);
|
|
140
154
|
if (!matchInfo.isMatch) {
|
|
155
|
+
// 商品不在策略适用范围内,跳过
|
|
141
156
|
continue;
|
|
142
157
|
}
|
|
158
|
+
|
|
159
|
+
// 2. 准备上下文(如果是 bundle 子商品匹配,需要用 bundle 子商品的 ID)
|
|
143
160
|
let contextProduct = product;
|
|
144
|
-
if (matchInfo.matchedBundleIndex !==
|
|
161
|
+
if (matchInfo.matchedBundleIndex !== undefined && product.bundle) {
|
|
145
162
|
const bundleItem = product.bundle[matchInfo.matchedBundleIndex];
|
|
163
|
+
// 创建一个临时的"虚拟商品"用于策略引擎评估
|
|
164
|
+
// 这样策略引擎的 product_match 条件能正确通过
|
|
146
165
|
contextProduct = {
|
|
147
166
|
...product,
|
|
148
167
|
product_id: bundleItem._bundle_product_id,
|
|
@@ -154,6 +173,8 @@ var PromotionEvaluator = class {
|
|
|
154
173
|
currentProduct: contextProduct,
|
|
155
174
|
channel
|
|
156
175
|
});
|
|
176
|
+
|
|
177
|
+
// 3. 评估策略(检查其他条件如时间范围等)
|
|
157
178
|
const result = this.engine.evaluate(config, context);
|
|
158
179
|
if (result.applicable && result.matchedActions.length > 0) {
|
|
159
180
|
const action = result.matchedActions[0];
|
|
@@ -184,6 +205,7 @@ var PromotionEvaluator = class {
|
|
|
184
205
|
}
|
|
185
206
|
return results;
|
|
186
207
|
}
|
|
208
|
+
|
|
187
209
|
/**
|
|
188
210
|
* 评估购物车
|
|
189
211
|
*
|
|
@@ -194,29 +216,42 @@ var PromotionEvaluator = class {
|
|
|
194
216
|
* @returns 购物车评估结果
|
|
195
217
|
*/
|
|
196
218
|
evaluateCart(input) {
|
|
197
|
-
const {
|
|
219
|
+
const {
|
|
220
|
+
products,
|
|
221
|
+
strategyConfigs,
|
|
222
|
+
channel
|
|
223
|
+
} = input;
|
|
198
224
|
const configs = strategyConfigs || this.strategyConfigs;
|
|
225
|
+
|
|
226
|
+
// 1. 先获取每个商品的促销结果
|
|
199
227
|
const productResults = this.evaluateProducts(input);
|
|
200
|
-
|
|
228
|
+
|
|
229
|
+
// 2. 收集所有适用的促销(去重)
|
|
230
|
+
const promotionMap = new Map();
|
|
201
231
|
for (const productResult of productResults) {
|
|
202
232
|
for (const promo of productResult.applicablePromotions) {
|
|
203
233
|
const existing = promotionMap.get(promo.strategyId);
|
|
204
234
|
const product = productResult.product;
|
|
235
|
+
|
|
236
|
+
// 计算促销数量:如果是 bundle 子商品匹配,数量 = 主商品数量 × 子商品数量
|
|
205
237
|
let promoQuantity = product.quantity;
|
|
206
|
-
if (promo.matchedBundleIndex !==
|
|
238
|
+
if (promo.matchedBundleIndex !== undefined && product.bundle) {
|
|
207
239
|
const bundleItem = product.bundle[promo.matchedBundleIndex];
|
|
208
240
|
promoQuantity = product.quantity * (bundleItem.quantity || 1);
|
|
209
241
|
}
|
|
210
242
|
if (existing) {
|
|
243
|
+
// 已存在,添加商品到列表
|
|
211
244
|
existing.applicableProducts.push(product);
|
|
212
245
|
existing.totalQuantity += promoQuantity;
|
|
213
246
|
existing.totalAmount += product.price * product.quantity;
|
|
247
|
+
// 记录每个商品的 matchedBundleIndex
|
|
214
248
|
if (!existing.productMatchedBundleIndexMap) {
|
|
215
|
-
existing.productMatchedBundleIndexMap =
|
|
249
|
+
existing.productMatchedBundleIndexMap = new Map();
|
|
216
250
|
}
|
|
217
251
|
existing.productMatchedBundleIndexMap.set(product.id, promo.matchedBundleIndex);
|
|
218
252
|
} else {
|
|
219
|
-
|
|
253
|
+
// 新促销
|
|
254
|
+
const productMatchedBundleIndexMap = new Map();
|
|
220
255
|
productMatchedBundleIndexMap.set(product.id, promo.matchedBundleIndex);
|
|
221
256
|
promotionMap.set(promo.strategyId, {
|
|
222
257
|
...promo,
|
|
@@ -229,30 +264,28 @@ var PromotionEvaluator = class {
|
|
|
229
264
|
}
|
|
230
265
|
}
|
|
231
266
|
const applicablePromotions = Array.from(promotionMap.values());
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
);
|
|
267
|
+
|
|
268
|
+
// 3. 按促销分组
|
|
269
|
+
const promotionGroups = applicablePromotions.map(promo => ({
|
|
270
|
+
strategyId: promo.strategyId,
|
|
271
|
+
strategyName: promo.strategyName,
|
|
272
|
+
strategyMetadata: promo.strategyConfig?.metadata,
|
|
273
|
+
strategyConfig: promo.strategyConfig,
|
|
274
|
+
actionType: promo.actionType,
|
|
275
|
+
actionDetail: promo.actionDetail,
|
|
276
|
+
products: promo.applicableProducts,
|
|
277
|
+
totalQuantity: promo.totalQuantity,
|
|
278
|
+
totalAmount: promo.totalAmount,
|
|
279
|
+
// 传递 productMatchedBundleIndexMap 到 group
|
|
280
|
+
productMatchedBundleIndexMap: promo.productMatchedBundleIndexMap
|
|
281
|
+
}));
|
|
250
282
|
return {
|
|
251
283
|
productResults,
|
|
252
284
|
applicablePromotions,
|
|
253
285
|
promotionGroups
|
|
254
286
|
};
|
|
255
287
|
}
|
|
288
|
+
|
|
256
289
|
/**
|
|
257
290
|
* 评估购物车并计算定价
|
|
258
291
|
*
|
|
@@ -264,70 +297,76 @@ var PromotionEvaluator = class {
|
|
|
264
297
|
* @returns 带定价的购物车评估结果
|
|
265
298
|
*/
|
|
266
299
|
evaluateCartWithPricing(input) {
|
|
267
|
-
const {
|
|
300
|
+
const {
|
|
301
|
+
products
|
|
302
|
+
} = input;
|
|
303
|
+
|
|
304
|
+
// 1. 先调用 evaluateCart 获取分组结果
|
|
268
305
|
const cartResult = this.evaluateCart(input);
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
306
|
+
|
|
307
|
+
// 2. 按 action priority 排序促销组(从 strategyConfig 获取 priority)
|
|
308
|
+
const sortedGroups = this.sortPromotionGroupsByPriority(cartResult.promotionGroups, cartResult.applicablePromotions);
|
|
309
|
+
|
|
310
|
+
// 3. 追踪已处理的商品(避免重复处理)
|
|
311
|
+
// key: `${product_id}_${product_variant_id}`
|
|
312
|
+
const processedQuantityMap = new Map();
|
|
313
|
+
|
|
314
|
+
// 4. 处理每个促销组,收集结果
|
|
274
315
|
const pricedProducts = [];
|
|
275
316
|
const gifts = [];
|
|
276
|
-
let totalOriginalAmount = new
|
|
277
|
-
let totalFinalAmount = new
|
|
317
|
+
let totalOriginalAmount = new _decimal.default(0);
|
|
318
|
+
let totalFinalAmount = new _decimal.default(0);
|
|
278
319
|
for (const group of sortedGroups) {
|
|
279
|
-
const {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
320
|
+
const {
|
|
321
|
+
actionType
|
|
322
|
+
} = group;
|
|
323
|
+
|
|
324
|
+
// 直接使用 evaluateCart 阶段计算好的 matchedBundleIndexMap
|
|
325
|
+
const matchedBundleIndexMap = group.productMatchedBundleIndexMap || new Map();
|
|
326
|
+
if (actionType === _type.PROMOTION_ACTION_TYPES.X_ITEMS_FOR_Y_PRICE) {
|
|
327
|
+
// X件Y元处理
|
|
328
|
+
const result = this.processXItemsForYPrice(group, processedQuantityMap, matchedBundleIndexMap);
|
|
287
329
|
pricedProducts.push(...result.products);
|
|
288
330
|
totalOriginalAmount = totalOriginalAmount.plus(result.originalAmount);
|
|
289
331
|
totalFinalAmount = totalFinalAmount.plus(result.finalAmount);
|
|
290
|
-
} else if (actionType ===
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
processedQuantityMap,
|
|
294
|
-
matchedBundleIndexMap
|
|
295
|
-
);
|
|
332
|
+
} else if (actionType === _type.PROMOTION_ACTION_TYPES.BUY_X_GET_Y_FREE) {
|
|
333
|
+
// 买X送Y处理
|
|
334
|
+
const result = this.processBuyXGetYFree(group, processedQuantityMap, matchedBundleIndexMap);
|
|
296
335
|
pricedProducts.push(...result.products);
|
|
297
336
|
totalOriginalAmount = totalOriginalAmount.plus(result.originalAmount);
|
|
298
337
|
totalFinalAmount = totalFinalAmount.plus(result.finalAmount);
|
|
338
|
+
|
|
339
|
+
// 添加赠品信息
|
|
299
340
|
if (result.giftInfo) {
|
|
300
341
|
gifts.push(result.giftInfo);
|
|
301
342
|
}
|
|
302
|
-
} else if (actionType ===
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
processedQuantityMap,
|
|
306
|
-
products,
|
|
307
|
-
matchedBundleIndexMap
|
|
308
|
-
);
|
|
343
|
+
} else if (actionType === _type.PROMOTION_ACTION_TYPES.ITEM_REWARD) {
|
|
344
|
+
// 商品奖励处理:条件商品触发,目标商品行享优惠,不自动新增商品
|
|
345
|
+
const result = this.processItemReward(group, processedQuantityMap, products, matchedBundleIndexMap);
|
|
309
346
|
pricedProducts.push(...result.products);
|
|
310
347
|
totalOriginalAmount = totalOriginalAmount.plus(result.originalAmount);
|
|
311
348
|
totalFinalAmount = totalFinalAmount.plus(result.finalAmount);
|
|
312
349
|
}
|
|
313
350
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
);
|
|
351
|
+
|
|
352
|
+
// 5. 处理未参与任何促销的商品
|
|
353
|
+
const remainingProducts = this.getRemainingProducts(products, processedQuantityMap);
|
|
318
354
|
for (const product of remainingProducts) {
|
|
319
|
-
const amount = new
|
|
355
|
+
const amount = new _decimal.default(product.price).mul(product.quantity);
|
|
320
356
|
totalOriginalAmount = totalOriginalAmount.plus(amount);
|
|
321
357
|
totalFinalAmount = totalFinalAmount.plus(amount);
|
|
322
358
|
}
|
|
323
359
|
pricedProducts.push(...remainingProducts);
|
|
360
|
+
|
|
361
|
+
// 6. 计算总优惠金额
|
|
324
362
|
const totalDiscount = totalOriginalAmount.minus(totalFinalAmount);
|
|
325
363
|
return {
|
|
326
364
|
products: pricedProducts,
|
|
327
365
|
gifts,
|
|
328
|
-
totalDiscount:
|
|
366
|
+
totalDiscount: _decimal.default.max(0, totalDiscount).toNumber()
|
|
329
367
|
};
|
|
330
368
|
}
|
|
369
|
+
|
|
331
370
|
/**
|
|
332
371
|
* 获取商品适用的促销列表
|
|
333
372
|
*
|
|
@@ -338,13 +377,13 @@ var PromotionEvaluator = class {
|
|
|
338
377
|
* @returns 适用的促销列表
|
|
339
378
|
*/
|
|
340
379
|
getProductPromotions(product, strategyConfigs) {
|
|
341
|
-
var _a;
|
|
342
380
|
const results = this.evaluateProducts({
|
|
343
381
|
products: [product],
|
|
344
382
|
strategyConfigs
|
|
345
383
|
});
|
|
346
|
-
return
|
|
384
|
+
return results[0]?.applicablePromotions || [];
|
|
347
385
|
}
|
|
386
|
+
|
|
348
387
|
/**
|
|
349
388
|
* 获取商品的促销标签
|
|
350
389
|
*
|
|
@@ -356,12 +395,13 @@ var PromotionEvaluator = class {
|
|
|
356
395
|
*/
|
|
357
396
|
getProductPromotionTags(product, strategyConfigs) {
|
|
358
397
|
const promotions = this.getProductPromotions(product, strategyConfigs);
|
|
359
|
-
return promotions.filter(
|
|
398
|
+
return promotions.filter(promo => promo.display).map(promo => ({
|
|
360
399
|
text: this.getLocalizedName(promo.display.text),
|
|
361
400
|
type: promo.display.type,
|
|
362
401
|
strategyId: promo.strategyId
|
|
363
402
|
}));
|
|
364
403
|
}
|
|
404
|
+
|
|
365
405
|
/**
|
|
366
406
|
* 查找商品适用的策略配置
|
|
367
407
|
*
|
|
@@ -371,10 +411,9 @@ var PromotionEvaluator = class {
|
|
|
371
411
|
*/
|
|
372
412
|
findApplicableStrategies(product, strategyConfigs) {
|
|
373
413
|
const configs = strategyConfigs || this.strategyConfigs;
|
|
374
|
-
return configs.filter(
|
|
375
|
-
(config) => this.isProductInStrategy(product, config)
|
|
376
|
-
);
|
|
414
|
+
return configs.filter(config => this.isProductInStrategy(product, config));
|
|
377
415
|
}
|
|
416
|
+
|
|
378
417
|
/**
|
|
379
418
|
* 批量获取商品列表的适用策略信息
|
|
380
419
|
*
|
|
@@ -386,26 +425,33 @@ var PromotionEvaluator = class {
|
|
|
386
425
|
* @returns 每个商品的适用策略完整数据
|
|
387
426
|
*/
|
|
388
427
|
getProductsApplicableStrategies(input, matchVariant = true) {
|
|
389
|
-
const {
|
|
428
|
+
const {
|
|
429
|
+
products,
|
|
430
|
+
strategyConfigs,
|
|
431
|
+
channel
|
|
432
|
+
} = input;
|
|
390
433
|
const configs = strategyConfigs || this.strategyConfigs;
|
|
391
434
|
const results = [];
|
|
392
435
|
for (const product of products) {
|
|
393
436
|
const applicableStrategies = [];
|
|
437
|
+
|
|
438
|
+
// 遍历所有策略,检查此商品是否适用
|
|
394
439
|
for (const config of configs) {
|
|
395
440
|
if (this.shouldSkipStrategyForProduct(product, config)) {
|
|
396
441
|
continue;
|
|
397
442
|
}
|
|
398
443
|
let contextProduct = product;
|
|
399
444
|
if (!matchVariant) {
|
|
445
|
+
// 只匹配 product_id,忽略 product_variant_id
|
|
400
446
|
const productMatchRule = this.findProductMatchRule(config);
|
|
401
447
|
if (productMatchRule) {
|
|
402
448
|
const configProducts = productMatchRule.value;
|
|
403
|
-
const matchedConfig = configProducts.find(
|
|
404
|
-
(item) => item.product_id === product.product_id
|
|
405
|
-
);
|
|
449
|
+
const matchedConfig = configProducts.find(item => item.product_id === product.product_id);
|
|
406
450
|
if (!matchedConfig) {
|
|
451
|
+
// product_id 不在策略适用范围内,跳过
|
|
407
452
|
continue;
|
|
408
453
|
}
|
|
454
|
+
// 用配置中的 variant_id 构造上下文,确保引擎 product_match 通过
|
|
409
455
|
if (matchedConfig.product_variant_id !== 0 && matchedConfig.product_variant_id !== product.product_variant_id) {
|
|
410
456
|
contextProduct = {
|
|
411
457
|
...product,
|
|
@@ -419,6 +465,8 @@ var PromotionEvaluator = class {
|
|
|
419
465
|
currentProduct: contextProduct,
|
|
420
466
|
channel
|
|
421
467
|
});
|
|
468
|
+
|
|
469
|
+
// 评估策略
|
|
422
470
|
const result = this.engine.evaluate(config, context);
|
|
423
471
|
if (result.applicable && result.matchedActions.length > 0) {
|
|
424
472
|
const action = result.matchedActions[0];
|
|
@@ -428,11 +476,11 @@ var PromotionEvaluator = class {
|
|
|
428
476
|
channel
|
|
429
477
|
});
|
|
430
478
|
if (transformedResult.actionDetail) {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
);
|
|
479
|
+
// 计算所需数量和可参与商品列表
|
|
480
|
+
const {
|
|
481
|
+
requiredQuantity,
|
|
482
|
+
eligibleProducts
|
|
483
|
+
} = this.getPromotionRequirements(action.type, transformedResult.actionDetail, config);
|
|
436
484
|
applicableStrategies.push({
|
|
437
485
|
strategyId: config.metadata.id,
|
|
438
486
|
strategyName: config.metadata.name,
|
|
@@ -455,6 +503,7 @@ var PromotionEvaluator = class {
|
|
|
455
503
|
}
|
|
456
504
|
return results;
|
|
457
505
|
}
|
|
506
|
+
|
|
458
507
|
/**
|
|
459
508
|
* 获取促销所需数量和可参与商品列表
|
|
460
509
|
*
|
|
@@ -465,44 +514,53 @@ var PromotionEvaluator = class {
|
|
|
465
514
|
*/
|
|
466
515
|
getPromotionRequirements(actionType, actionDetail, config) {
|
|
467
516
|
let requiredQuantity = 0;
|
|
468
|
-
|
|
517
|
+
|
|
518
|
+
// 根据促销类型获取所需数量
|
|
519
|
+
if (actionType === 'BUY_X_GET_Y_FREE') {
|
|
469
520
|
const detail = actionDetail;
|
|
470
521
|
requiredQuantity = detail.buyQuantity || 0;
|
|
471
|
-
} else if (actionType ===
|
|
522
|
+
} else if (actionType === 'X_ITEMS_FOR_Y_PRICE') {
|
|
472
523
|
const detail = actionDetail;
|
|
473
524
|
requiredQuantity = detail.x || 0;
|
|
474
|
-
} else if (actionType ===
|
|
525
|
+
} else if (actionType === 'ITEM_REWARD') {
|
|
475
526
|
const detail = actionDetail;
|
|
476
527
|
requiredQuantity = detail.triggerQuantity || 1;
|
|
477
528
|
}
|
|
529
|
+
|
|
530
|
+
// 获取可参与商品列表
|
|
478
531
|
const productMatchRule = this.findProductMatchRule(config);
|
|
479
|
-
const eligibleProducts =
|
|
532
|
+
const eligibleProducts = productMatchRule?.value || [];
|
|
480
533
|
return {
|
|
481
534
|
requiredQuantity,
|
|
482
535
|
eligibleProducts
|
|
483
536
|
};
|
|
484
537
|
}
|
|
538
|
+
|
|
485
539
|
// ============================================
|
|
486
540
|
// 私有辅助方法
|
|
487
541
|
// ============================================
|
|
542
|
+
|
|
488
543
|
/**
|
|
489
544
|
* 按优先级排序促销组
|
|
490
545
|
* 优先级从 strategyConfig.actions[0].priority 获取,数值越小优先级越高
|
|
491
546
|
*/
|
|
492
547
|
sortPromotionGroupsByPriority(groups, promotions) {
|
|
493
|
-
|
|
494
|
-
const priorityMap =
|
|
548
|
+
// 创建策略ID到优先级的映射
|
|
549
|
+
const priorityMap = new Map();
|
|
495
550
|
for (const promo of promotions) {
|
|
496
|
-
const actions =
|
|
497
|
-
const priority =
|
|
551
|
+
const actions = promo.strategyConfig?.actions || [];
|
|
552
|
+
const priority = actions[0]?.priority ?? 999;
|
|
498
553
|
priorityMap.set(promo.strategyId, priority);
|
|
499
554
|
}
|
|
555
|
+
|
|
556
|
+
// 按优先级排序(数值越小优先级越高)
|
|
500
557
|
return [...groups].sort((a, b) => {
|
|
501
558
|
const priorityA = priorityMap.get(a.strategyId) ?? 999;
|
|
502
559
|
const priorityB = priorityMap.get(b.strategyId) ?? 999;
|
|
503
560
|
return priorityA - priorityB;
|
|
504
561
|
});
|
|
505
562
|
}
|
|
563
|
+
|
|
506
564
|
/**
|
|
507
565
|
* 处理 X件Y元 促销
|
|
508
566
|
*
|
|
@@ -514,19 +572,32 @@ var PromotionEvaluator = class {
|
|
|
514
572
|
* 注意:每个商品按 id 独立处理,不会合并不同 id 的商品
|
|
515
573
|
*/
|
|
516
574
|
processXItemsForYPrice(group, processedQuantityMap, matchedBundleIndexMap) {
|
|
517
|
-
const {
|
|
575
|
+
const {
|
|
576
|
+
strategyId,
|
|
577
|
+
strategyMetadata,
|
|
578
|
+
actionDetail,
|
|
579
|
+
products: groupProducts
|
|
580
|
+
} = group;
|
|
518
581
|
const detail = actionDetail;
|
|
519
|
-
const {
|
|
582
|
+
const {
|
|
583
|
+
x,
|
|
584
|
+
price: groupPrice,
|
|
585
|
+
cumulative
|
|
586
|
+
} = detail;
|
|
520
587
|
const result = [];
|
|
521
|
-
let originalAmount = new
|
|
522
|
-
let finalAmount = new
|
|
588
|
+
let originalAmount = new _decimal.default(0);
|
|
589
|
+
let finalAmount = new _decimal.default(0);
|
|
590
|
+
|
|
591
|
+
// 1. 展开所有商品为单件列表(排除已处理的数量)
|
|
592
|
+
// 每个单件记录来自哪个原商品(通过 product.id)
|
|
523
593
|
const unitProducts = [];
|
|
524
594
|
for (const product of groupProducts) {
|
|
525
595
|
const key = this.getProductKey(product);
|
|
526
596
|
const processedQty = processedQuantityMap.get(key) || 0;
|
|
527
597
|
const availableQty = product.quantity - processedQty;
|
|
528
|
-
if (availableQty <= 0)
|
|
529
|
-
|
|
598
|
+
if (availableQty <= 0) continue;
|
|
599
|
+
|
|
600
|
+
// 展开为单件,每个单件都记录来源商品
|
|
530
601
|
for (let i = 0; i < availableQty; i++) {
|
|
531
602
|
unitProducts.push({
|
|
532
603
|
product,
|
|
@@ -537,47 +608,67 @@ var PromotionEvaluator = class {
|
|
|
537
608
|
}
|
|
538
609
|
}
|
|
539
610
|
if (unitProducts.length === 0) {
|
|
540
|
-
return {
|
|
611
|
+
return {
|
|
612
|
+
products: [],
|
|
613
|
+
originalAmount: 0,
|
|
614
|
+
finalAmount: 0
|
|
615
|
+
};
|
|
541
616
|
}
|
|
617
|
+
|
|
618
|
+
// 2. 排序:优先选择来源数量是 x 倍数的商品(可完整凑组不拆分),然后按价格从高到低
|
|
542
619
|
unitProducts.sort((a, b) => {
|
|
620
|
+
// 优先级1:来源购物车项数量是 x 的倍数的排前面(这些商品可以完整凑组,避免拆分)
|
|
543
621
|
const aIsMultiple = a.sourceAvailableQty > 0 && a.sourceAvailableQty % x === 0 ? 1 : 0;
|
|
544
622
|
const bIsMultiple = b.sourceAvailableQty > 0 && b.sourceAvailableQty % x === 0 ? 1 : 0;
|
|
545
|
-
if (aIsMultiple !== bIsMultiple)
|
|
546
|
-
|
|
623
|
+
if (aIsMultiple !== bIsMultiple) return bIsMultiple - aIsMultiple;
|
|
624
|
+
// 优先级2:按价格从高到低(优先使用高价商品)
|
|
547
625
|
return b.price - a.price;
|
|
548
626
|
});
|
|
627
|
+
|
|
628
|
+
// 3. 计算可以凑成多少组(cumulative 缺省视为 true,与 adapter 一致)
|
|
549
629
|
const totalUnits = unitProducts.length;
|
|
550
630
|
const isCumulative = cumulative !== false;
|
|
551
631
|
const groupCount = isCumulative ? Math.floor(totalUnits / x) : totalUnits >= x ? 1 : 0;
|
|
552
632
|
const promotionUnits = groupCount * x;
|
|
633
|
+
|
|
634
|
+
// 4. 分离参与促销的和不参与的商品
|
|
553
635
|
const inPromotionUnits = unitProducts.slice(0, promotionUnits);
|
|
554
636
|
const notInPromotionUnits = unitProducts.slice(promotionUnits);
|
|
555
|
-
|
|
637
|
+
|
|
638
|
+
// 5. 按原商品 id 统计参与促销和不参与的数量
|
|
639
|
+
const promotionQtyByProductId = new Map();
|
|
556
640
|
for (const unit of inPromotionUnits) {
|
|
557
641
|
const id = unit.productId;
|
|
558
642
|
promotionQtyByProductId.set(id, (promotionQtyByProductId.get(id) || 0) + 1);
|
|
559
643
|
}
|
|
560
|
-
const remainingQtyByProductId =
|
|
644
|
+
const remainingQtyByProductId = new Map();
|
|
561
645
|
for (const unit of notInPromotionUnits) {
|
|
562
646
|
const id = unit.productId;
|
|
563
647
|
remainingQtyByProductId.set(id, (remainingQtyByProductId.get(id) || 0) + 1);
|
|
564
648
|
}
|
|
565
|
-
|
|
649
|
+
|
|
650
|
+
// 6. 为每个参与促销的单件计算均摊价格
|
|
651
|
+
// 按组内原价比例均摊
|
|
652
|
+
// 记录每个原商品的均摊价格累加(用于后续聚合)
|
|
653
|
+
const allocatedPricesByProductId = new Map();
|
|
566
654
|
for (let g = 0; g < groupCount; g++) {
|
|
567
655
|
const startIdx = g * x;
|
|
568
656
|
const endIdx = startIdx + x;
|
|
569
657
|
const groupUnits = inPromotionUnits.slice(startIdx, endIdx);
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
658
|
+
|
|
659
|
+
// 计算组内原价总和
|
|
660
|
+
const groupOriginalTotal = groupUnits.reduce((sum, u) => sum.plus(u.price), new _decimal.default(0));
|
|
661
|
+
|
|
662
|
+
// 按原价比例均摊
|
|
663
|
+
const groupPriceDecimal = new _decimal.default(groupPrice);
|
|
664
|
+
let allocatedSum = new _decimal.default(0);
|
|
576
665
|
for (let i = 0; i < groupUnits.length; i++) {
|
|
577
666
|
const unit = groupUnits[i];
|
|
578
667
|
const isLastUnit = i === groupUnits.length - 1;
|
|
579
|
-
const ratio = groupOriginalTotal.eq(0) ? new
|
|
668
|
+
const ratio = groupOriginalTotal.eq(0) ? new _decimal.default(0) : new _decimal.default(unit.price).div(groupOriginalTotal);
|
|
580
669
|
const allocatedDecimal = isLastUnit ? groupPriceDecimal.minus(allocatedSum) : groupPriceDecimal.mul(ratio);
|
|
670
|
+
|
|
671
|
+
// 记录这个单件的均摊价格
|
|
581
672
|
const productId = unit.productId;
|
|
582
673
|
const prices = allocatedPricesByProductId.get(productId) || [];
|
|
583
674
|
prices.push(allocatedDecimal);
|
|
@@ -585,23 +676,25 @@ var PromotionEvaluator = class {
|
|
|
585
676
|
allocatedSum = allocatedSum.plus(allocatedDecimal);
|
|
586
677
|
}
|
|
587
678
|
}
|
|
679
|
+
|
|
680
|
+
// 7. 为每个原商品生成结果
|
|
588
681
|
for (const product of groupProducts) {
|
|
589
682
|
const key = this.getProductKey(product);
|
|
590
683
|
const processedQty = processedQuantityMap.get(key) || 0;
|
|
591
684
|
const availableQty = product.quantity - processedQty;
|
|
592
|
-
if (availableQty <= 0)
|
|
593
|
-
continue;
|
|
685
|
+
if (availableQty <= 0) continue;
|
|
594
686
|
const promoQty = promotionQtyByProductId.get(product.id) || 0;
|
|
595
687
|
const remainingQty = remainingQtyByProductId.get(product.id) || 0;
|
|
596
688
|
const allocatedPrices = allocatedPricesByProductId.get(product.id) || [];
|
|
597
|
-
|
|
689
|
+
|
|
690
|
+
// 获取当前商品的 matchedBundleIndex
|
|
691
|
+
const matchedBundleIndex = matchedBundleIndexMap?.get(product.id);
|
|
598
692
|
if (promoQty > 0 && remainingQty === 0) {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
);
|
|
603
|
-
const
|
|
604
|
-
const actualTotal = import_decimal.default.min(totalAllocated, productOriginalAmount);
|
|
693
|
+
// 全部参与促销:保留原 id,计算总 finalPrice
|
|
694
|
+
const totalAllocated = allocatedPrices.reduce((sum, p) => sum.plus(p), new _decimal.default(0));
|
|
695
|
+
// 取促销价和原价的最小值,避免促销反而加价
|
|
696
|
+
const productOriginalAmount = new _decimal.default(product.price).mul(promoQty);
|
|
697
|
+
const actualTotal = _decimal.default.min(totalAllocated, productOriginalAmount);
|
|
605
698
|
const finalPricePerUnit = this.formatPrice(actualTotal.div(promoQty));
|
|
606
699
|
result.push({
|
|
607
700
|
...product,
|
|
@@ -616,25 +709,26 @@ var PromotionEvaluator = class {
|
|
|
616
709
|
originalAmount = originalAmount.plus(productOriginalAmount);
|
|
617
710
|
finalAmount = finalAmount.plus(actualTotal);
|
|
618
711
|
} else if (promoQty === 0 && remainingQty > 0) {
|
|
712
|
+
// 全部不参与促销:保留原 id,finalPrice = 原价
|
|
619
713
|
result.push({
|
|
620
714
|
...product,
|
|
621
715
|
quantity: remainingQty,
|
|
622
716
|
finalPrice: this.formatPrice(product.price),
|
|
623
|
-
strategyId:
|
|
624
|
-
strategyMetadata:
|
|
717
|
+
strategyId: undefined,
|
|
718
|
+
strategyMetadata: undefined,
|
|
625
719
|
inPromotion: false,
|
|
626
720
|
isSplit: false
|
|
627
721
|
});
|
|
628
|
-
const amount = new
|
|
722
|
+
const amount = new _decimal.default(product.price).mul(remainingQty);
|
|
629
723
|
originalAmount = originalAmount.plus(amount);
|
|
630
724
|
finalAmount = finalAmount.plus(amount);
|
|
631
725
|
} else if (promoQty > 0 && remainingQty > 0) {
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
const promoOriginalAmount = new
|
|
637
|
-
const actualTotal =
|
|
726
|
+
// 部分参与促销,部分不参与:需要拆分
|
|
727
|
+
// 参与促销的部分:新 id
|
|
728
|
+
const totalAllocated = allocatedPrices.reduce((sum, p) => sum.plus(p), new _decimal.default(0));
|
|
729
|
+
// 取促销价和原价的最小值,避免促销反而加价
|
|
730
|
+
const promoOriginalAmount = new _decimal.default(product.price).mul(promoQty);
|
|
731
|
+
const actualTotal = _decimal.default.min(totalAllocated, promoOriginalAmount);
|
|
638
732
|
const finalPricePerUnit = this.formatPrice(actualTotal.div(promoQty));
|
|
639
733
|
result.push({
|
|
640
734
|
...product,
|
|
@@ -650,44 +744,50 @@ var PromotionEvaluator = class {
|
|
|
650
744
|
});
|
|
651
745
|
originalAmount = originalAmount.plus(promoOriginalAmount);
|
|
652
746
|
finalAmount = finalAmount.plus(actualTotal);
|
|
747
|
+
|
|
748
|
+
// 不参与促销的部分:保留原 id
|
|
653
749
|
result.push({
|
|
654
750
|
...product,
|
|
655
751
|
quantity: remainingQty,
|
|
656
752
|
finalPrice: this.formatPrice(product.price),
|
|
657
|
-
strategyId:
|
|
658
|
-
strategyMetadata:
|
|
753
|
+
strategyId: undefined,
|
|
754
|
+
strategyMetadata: undefined,
|
|
659
755
|
inPromotion: false,
|
|
660
756
|
isSplit: true
|
|
661
757
|
});
|
|
662
|
-
const remainingAmount = new
|
|
758
|
+
const remainingAmount = new _decimal.default(product.price).mul(remainingQty);
|
|
663
759
|
originalAmount = originalAmount.plus(remainingAmount);
|
|
664
760
|
finalAmount = finalAmount.plus(remainingAmount);
|
|
665
761
|
}
|
|
762
|
+
|
|
763
|
+
// 更新已处理数量
|
|
666
764
|
processedQuantityMap.set(key, processedQty + availableQty);
|
|
667
765
|
}
|
|
766
|
+
|
|
767
|
+
// 8. 尾差调整:确保促销商品的 finalPrice × quantity 之和精确等于 groupPrice × groupCount
|
|
668
768
|
if (groupCount > 0) {
|
|
669
|
-
const expectedPromoTotal = new
|
|
670
|
-
const promoItems = result.filter(
|
|
671
|
-
let actualPromoTotal = new
|
|
769
|
+
const expectedPromoTotal = new _decimal.default(groupPrice).mul(groupCount);
|
|
770
|
+
const promoItems = result.filter(p => p.inPromotion);
|
|
771
|
+
let actualPromoTotal = new _decimal.default(0);
|
|
672
772
|
for (const p of promoItems) {
|
|
673
|
-
actualPromoTotal = actualPromoTotal.plus(
|
|
674
|
-
new import_decimal.default(p.finalPrice).mul(p.quantity)
|
|
675
|
-
);
|
|
773
|
+
actualPromoTotal = actualPromoTotal.plus(new _decimal.default(p.finalPrice).mul(p.quantity));
|
|
676
774
|
}
|
|
677
775
|
const roundingDiff = actualPromoTotal.minus(expectedPromoTotal);
|
|
678
776
|
if (!roundingDiff.eq(0)) {
|
|
679
|
-
|
|
777
|
+
// 优先找 quantity=1 的促销商品调整(差额直接加减到单价上)
|
|
778
|
+
const adjustTarget = promoItems.find(p => p.quantity === 1);
|
|
680
779
|
if (adjustTarget) {
|
|
681
|
-
adjustTarget.finalPrice = this.formatPrice(
|
|
682
|
-
new import_decimal.default(adjustTarget.finalPrice).minus(roundingDiff)
|
|
683
|
-
);
|
|
780
|
+
adjustTarget.finalPrice = this.formatPrice(new _decimal.default(adjustTarget.finalPrice).minus(roundingDiff));
|
|
684
781
|
} else if (promoItems.length > 0) {
|
|
782
|
+
// 没有 quantity=1 的促销商品,从最后一个促销商品中拆出1件做尾差调整
|
|
685
783
|
const lastPromo = promoItems[promoItems.length - 1];
|
|
686
|
-
const adjustedUnitPrice = this.formatPrice(
|
|
687
|
-
|
|
688
|
-
|
|
784
|
+
const adjustedUnitPrice = this.formatPrice(new _decimal.default(lastPromo.finalPrice).minus(roundingDiff));
|
|
785
|
+
|
|
786
|
+
// 原商品减少1件
|
|
689
787
|
lastPromo.quantity -= 1;
|
|
690
788
|
lastPromo.isSplit = true;
|
|
789
|
+
|
|
790
|
+
// 拆出1件用调整后的价格
|
|
691
791
|
result.push({
|
|
692
792
|
...lastPromo,
|
|
693
793
|
id: this.generateRandomId(),
|
|
@@ -697,11 +797,11 @@ var PromotionEvaluator = class {
|
|
|
697
797
|
isSplit: true
|
|
698
798
|
});
|
|
699
799
|
}
|
|
700
|
-
|
|
800
|
+
|
|
801
|
+
// 重新计算 finalAmount
|
|
802
|
+
finalAmount = new _decimal.default(0);
|
|
701
803
|
for (const p of result) {
|
|
702
|
-
finalAmount = finalAmount.plus(
|
|
703
|
-
new import_decimal.default(p.finalPrice).mul(p.quantity)
|
|
704
|
-
);
|
|
804
|
+
finalAmount = finalAmount.plus(new _decimal.default(p.finalPrice).mul(p.quantity));
|
|
705
805
|
}
|
|
706
806
|
}
|
|
707
807
|
}
|
|
@@ -711,6 +811,7 @@ var PromotionEvaluator = class {
|
|
|
711
811
|
finalAmount: finalAmount.toNumber()
|
|
712
812
|
};
|
|
713
813
|
}
|
|
814
|
+
|
|
714
815
|
/**
|
|
715
816
|
* 处理 买X送Y 促销
|
|
716
817
|
*
|
|
@@ -726,34 +827,56 @@ var PromotionEvaluator = class {
|
|
|
726
827
|
products: groupProducts
|
|
727
828
|
} = group;
|
|
728
829
|
const detail = actionDetail;
|
|
729
|
-
const {
|
|
830
|
+
const {
|
|
831
|
+
buyQuantity,
|
|
832
|
+
freeQuantity,
|
|
833
|
+
cumulative,
|
|
834
|
+
giftProducts
|
|
835
|
+
} = detail;
|
|
730
836
|
const result = [];
|
|
731
|
-
let originalAmount = new
|
|
732
|
-
let finalAmount = new
|
|
837
|
+
let originalAmount = new _decimal.default(0);
|
|
838
|
+
let finalAmount = new _decimal.default(0);
|
|
839
|
+
|
|
840
|
+
// 1. 先计算可用商品总数量(第一次遍历)
|
|
841
|
+
// 对于 bundle 子商品匹配,促销数量 = 主商品数量 × bundle子商品数量
|
|
733
842
|
let totalPromoQty = 0;
|
|
734
|
-
const productInfoMap =
|
|
843
|
+
const productInfoMap = new Map();
|
|
735
844
|
for (const product of groupProducts) {
|
|
736
845
|
const key = this.getProductKey(product);
|
|
737
846
|
const processedQty = processedQuantityMap.get(key) || 0;
|
|
738
847
|
const availableQty = product.quantity - processedQty;
|
|
739
|
-
if (availableQty <= 0)
|
|
740
|
-
|
|
741
|
-
|
|
848
|
+
if (availableQty <= 0) continue;
|
|
849
|
+
|
|
850
|
+
// 获取 matchedBundleIndex
|
|
851
|
+
const matchedBundleIndex = matchedBundleIndexMap?.get(product.id);
|
|
852
|
+
|
|
853
|
+
// 计算用于促销的数量
|
|
742
854
|
let promoQty = availableQty;
|
|
743
|
-
if (matchedBundleIndex !==
|
|
855
|
+
if (matchedBundleIndex !== undefined && product.bundle) {
|
|
744
856
|
const bundleItem = product.bundle[matchedBundleIndex];
|
|
857
|
+
// 促销数量 = 主商品数量 × bundle子商品数量
|
|
745
858
|
promoQty = availableQty * (bundleItem.quantity || 1);
|
|
746
859
|
}
|
|
747
860
|
totalPromoQty += promoQty;
|
|
748
|
-
productInfoMap.set(key, {
|
|
861
|
+
productInfoMap.set(key, {
|
|
862
|
+
availableQty,
|
|
863
|
+
promoQty,
|
|
864
|
+
matchedBundleIndex
|
|
865
|
+
});
|
|
749
866
|
}
|
|
867
|
+
|
|
868
|
+
// 2. 判断是否满足促销条件
|
|
750
869
|
const isFulfilled = totalPromoQty >= buyQuantity;
|
|
870
|
+
|
|
871
|
+
// 3. 生成商品结果(第二次遍历)
|
|
751
872
|
for (const product of groupProducts) {
|
|
752
873
|
const key = this.getProductKey(product);
|
|
753
874
|
const info = productInfoMap.get(key);
|
|
754
|
-
if (!info || info.availableQty <= 0)
|
|
755
|
-
continue;
|
|
875
|
+
if (!info || info.availableQty <= 0) continue;
|
|
756
876
|
const processedQty = processedQuantityMap.get(key) || 0;
|
|
877
|
+
|
|
878
|
+
// 商品本身价格不变,只是触发赠品
|
|
879
|
+
// inPromotion 只有在满足条件时才为 true
|
|
757
880
|
result.push({
|
|
758
881
|
...product,
|
|
759
882
|
quantity: info.availableQty,
|
|
@@ -765,15 +888,21 @@ var PromotionEvaluator = class {
|
|
|
765
888
|
isSplit: info.availableQty < product.quantity,
|
|
766
889
|
matchedBundleIndex: info.matchedBundleIndex
|
|
767
890
|
});
|
|
768
|
-
const amount = new
|
|
891
|
+
const amount = new _decimal.default(product.price).mul(info.availableQty);
|
|
769
892
|
originalAmount = originalAmount.plus(amount);
|
|
770
893
|
finalAmount = finalAmount.plus(amount);
|
|
894
|
+
|
|
895
|
+
// 更新已处理数量(使用主商品数量)
|
|
771
896
|
processedQuantityMap.set(key, processedQty + info.availableQty);
|
|
772
897
|
}
|
|
898
|
+
|
|
899
|
+
// 4. 计算赠品数量(使用促销数量)
|
|
773
900
|
let giftCount = 0;
|
|
774
901
|
if (isFulfilled) {
|
|
775
902
|
giftCount = cumulative ? Math.floor(totalPromoQty / buyQuantity) * freeQuantity : freeQuantity;
|
|
776
903
|
}
|
|
904
|
+
|
|
905
|
+
// 5. 构建赠品信息
|
|
777
906
|
const giftInfo = giftCount > 0 ? {
|
|
778
907
|
strategyId,
|
|
779
908
|
strategyName,
|
|
@@ -787,6 +916,7 @@ var PromotionEvaluator = class {
|
|
|
787
916
|
giftInfo
|
|
788
917
|
};
|
|
789
918
|
}
|
|
919
|
+
|
|
790
920
|
/**
|
|
791
921
|
* 处理 商品奖励 促销
|
|
792
922
|
*
|
|
@@ -806,42 +936,37 @@ var PromotionEvaluator = class {
|
|
|
806
936
|
const detail = actionDetail;
|
|
807
937
|
const triggerQuantity = this.normalizePositiveInteger(detail.triggerQuantity, 1);
|
|
808
938
|
const rewardQuantity = this.normalizePositiveInteger(detail.rewardQuantity, 1);
|
|
809
|
-
const conditionQuantity = this.getAvailablePromotionQuantity(
|
|
810
|
-
triggerProducts,
|
|
811
|
-
processedQuantityMap,
|
|
812
|
-
matchedBundleIndexMap
|
|
813
|
-
);
|
|
939
|
+
const conditionQuantity = this.getAvailablePromotionQuantity(triggerProducts, processedQuantityMap, matchedBundleIndexMap);
|
|
814
940
|
if (conditionQuantity < triggerQuantity) {
|
|
815
|
-
return {
|
|
941
|
+
return {
|
|
942
|
+
products: [],
|
|
943
|
+
originalAmount: 0,
|
|
944
|
+
finalAmount: 0
|
|
945
|
+
};
|
|
816
946
|
}
|
|
817
|
-
const targetProducts = allProducts.filter(
|
|
818
|
-
|
|
819
|
-
);
|
|
820
|
-
const targetUnitCandidates = this.buildItemRewardTargetUnits(
|
|
821
|
-
targetProducts,
|
|
822
|
-
processedQuantityMap,
|
|
823
|
-
detail
|
|
824
|
-
);
|
|
947
|
+
const targetProducts = allProducts.filter(product => this.isProductMatch(product, detail.rewardTargets || []));
|
|
948
|
+
const targetUnitCandidates = this.buildItemRewardTargetUnits(targetProducts, processedQuantityMap, detail);
|
|
825
949
|
if (targetUnitCandidates.length === 0) {
|
|
826
|
-
return {
|
|
950
|
+
return {
|
|
951
|
+
products: [],
|
|
952
|
+
originalAmount: 0,
|
|
953
|
+
finalAmount: 0
|
|
954
|
+
};
|
|
827
955
|
}
|
|
828
|
-
const rewardLimit = this.resolveItemRewardLimit(
|
|
829
|
-
detail,
|
|
830
|
-
conditionQuantity,
|
|
831
|
-
triggerQuantity,
|
|
832
|
-
rewardQuantity,
|
|
833
|
-
targetUnitCandidates.length
|
|
834
|
-
);
|
|
956
|
+
const rewardLimit = this.resolveItemRewardLimit(detail, conditionQuantity, triggerQuantity, rewardQuantity, targetUnitCandidates.length);
|
|
835
957
|
if (rewardLimit <= 0) {
|
|
836
|
-
return {
|
|
958
|
+
return {
|
|
959
|
+
products: [],
|
|
960
|
+
originalAmount: 0,
|
|
961
|
+
finalAmount: 0
|
|
962
|
+
};
|
|
837
963
|
}
|
|
838
964
|
const selectedUnits = [...targetUnitCandidates].sort((a, b) => {
|
|
839
965
|
const discountDiff = b.discountAmount.minus(a.discountAmount).toNumber();
|
|
840
|
-
if (discountDiff !== 0)
|
|
841
|
-
return discountDiff;
|
|
966
|
+
if (discountDiff !== 0) return discountDiff;
|
|
842
967
|
return b.originalPrice.minus(a.originalPrice).toNumber();
|
|
843
968
|
}).slice(0, rewardLimit);
|
|
844
|
-
const selectedByProductId =
|
|
969
|
+
const selectedByProductId = new Map();
|
|
845
970
|
for (const unit of selectedUnits) {
|
|
846
971
|
const existing = selectedByProductId.get(unit.product.id);
|
|
847
972
|
if (existing) {
|
|
@@ -855,19 +980,17 @@ var PromotionEvaluator = class {
|
|
|
855
980
|
}
|
|
856
981
|
}
|
|
857
982
|
const result = [];
|
|
858
|
-
let originalAmount = new
|
|
859
|
-
let finalAmount = new
|
|
983
|
+
let originalAmount = new _decimal.default(0);
|
|
984
|
+
let finalAmount = new _decimal.default(0);
|
|
860
985
|
for (const product of targetProducts) {
|
|
861
986
|
const key = this.getProductKey(product);
|
|
862
987
|
const processedQty = processedQuantityMap.get(key) || 0;
|
|
863
988
|
const availableQty = product.quantity - processedQty;
|
|
864
|
-
if (availableQty <= 0)
|
|
865
|
-
continue;
|
|
989
|
+
if (availableQty <= 0) continue;
|
|
866
990
|
const selected = selectedByProductId.get(product.id);
|
|
867
|
-
const selectedQty =
|
|
991
|
+
const selectedQty = selected?.quantity || 0;
|
|
868
992
|
const remainingQty = availableQty - selectedQty;
|
|
869
|
-
if (selectedQty <= 0)
|
|
870
|
-
continue;
|
|
993
|
+
if (selectedQty <= 0) continue;
|
|
871
994
|
if (selectedQty > 0) {
|
|
872
995
|
result.push({
|
|
873
996
|
...product,
|
|
@@ -880,9 +1003,7 @@ var PromotionEvaluator = class {
|
|
|
880
1003
|
inPromotion: true,
|
|
881
1004
|
isSplit: selectedQty !== product.quantity
|
|
882
1005
|
});
|
|
883
|
-
originalAmount = originalAmount.plus(
|
|
884
|
-
selected.originalPrice.mul(selectedQty)
|
|
885
|
-
);
|
|
1006
|
+
originalAmount = originalAmount.plus(selected.originalPrice.mul(selectedQty));
|
|
886
1007
|
finalAmount = finalAmount.plus(selected.finalPrice.mul(selectedQty));
|
|
887
1008
|
}
|
|
888
1009
|
if (remainingQty > 0) {
|
|
@@ -890,12 +1011,12 @@ var PromotionEvaluator = class {
|
|
|
890
1011
|
...product,
|
|
891
1012
|
quantity: remainingQty,
|
|
892
1013
|
finalPrice: this.formatPrice(product.price),
|
|
893
|
-
strategyId:
|
|
894
|
-
strategyMetadata:
|
|
1014
|
+
strategyId: undefined,
|
|
1015
|
+
strategyMetadata: undefined,
|
|
895
1016
|
inPromotion: false,
|
|
896
1017
|
isSplit: selectedQty > 0
|
|
897
1018
|
});
|
|
898
|
-
const remainingAmount = new
|
|
1019
|
+
const remainingAmount = new _decimal.default(product.price).mul(remainingQty);
|
|
899
1020
|
originalAmount = originalAmount.plus(remainingAmount);
|
|
900
1021
|
finalAmount = finalAmount.plus(remainingAmount);
|
|
901
1022
|
}
|
|
@@ -917,12 +1038,11 @@ var PromotionEvaluator = class {
|
|
|
917
1038
|
const key = this.getProductKey(product);
|
|
918
1039
|
const processedQty = processedQuantityMap.get(key) || 0;
|
|
919
1040
|
const availableQty = product.quantity - processedQty;
|
|
920
|
-
if (availableQty <= 0)
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
if (matchedBundleIndex !== void 0 && product.bundle) {
|
|
1041
|
+
if (availableQty <= 0) continue;
|
|
1042
|
+
const matchedBundleIndex = matchedBundleIndexMap?.get(product.id);
|
|
1043
|
+
if (matchedBundleIndex !== undefined && product.bundle) {
|
|
924
1044
|
const bundleItem = product.bundle[matchedBundleIndex];
|
|
925
|
-
total += availableQty * (
|
|
1045
|
+
total += availableQty * (bundleItem?.quantity || 1);
|
|
926
1046
|
} else {
|
|
927
1047
|
total += availableQty;
|
|
928
1048
|
}
|
|
@@ -935,13 +1055,11 @@ var PromotionEvaluator = class {
|
|
|
935
1055
|
const key = this.getProductKey(product);
|
|
936
1056
|
const processedQty = processedQuantityMap.get(key) || 0;
|
|
937
1057
|
const availableQty = product.quantity - processedQty;
|
|
938
|
-
if (availableQty <= 0)
|
|
939
|
-
|
|
940
|
-
const originalPrice = new import_decimal.default(product.price || 0);
|
|
1058
|
+
if (availableQty <= 0) continue;
|
|
1059
|
+
const originalPrice = new _decimal.default(product.price || 0);
|
|
941
1060
|
const finalPrice = this.resolveItemRewardFinalPrice(originalPrice, detail);
|
|
942
1061
|
const discountAmount = originalPrice.minus(finalPrice);
|
|
943
|
-
if (discountAmount.lte(0))
|
|
944
|
-
continue;
|
|
1062
|
+
if (discountAmount.lte(0)) continue;
|
|
945
1063
|
for (let i = 0; i < availableQty; i++) {
|
|
946
1064
|
units.push({
|
|
947
1065
|
product,
|
|
@@ -954,46 +1072,45 @@ var PromotionEvaluator = class {
|
|
|
954
1072
|
return units;
|
|
955
1073
|
}
|
|
956
1074
|
resolveItemRewardFinalPrice(originalPrice, detail) {
|
|
957
|
-
const rewardValue = new
|
|
1075
|
+
const rewardValue = new _decimal.default(Number(detail.rewardValue) || 0);
|
|
958
1076
|
let finalPrice;
|
|
959
1077
|
switch (detail.rewardMethod) {
|
|
960
|
-
case
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1078
|
+
case 'percent_off':
|
|
1079
|
+
{
|
|
1080
|
+
const percent = _decimal.default.min(100, _decimal.default.max(0, rewardValue));
|
|
1081
|
+
finalPrice = originalPrice.mul(new _decimal.default(100).minus(percent)).div(100);
|
|
1082
|
+
break;
|
|
1083
|
+
}
|
|
1084
|
+
case 'amount_off':
|
|
1085
|
+
finalPrice = originalPrice.minus(_decimal.default.max(0, rewardValue));
|
|
967
1086
|
break;
|
|
968
|
-
case
|
|
969
|
-
finalPrice =
|
|
1087
|
+
case 'fixed_price':
|
|
1088
|
+
finalPrice = _decimal.default.max(0, rewardValue);
|
|
970
1089
|
break;
|
|
971
|
-
case
|
|
1090
|
+
case 'free':
|
|
972
1091
|
default:
|
|
973
|
-
finalPrice = new
|
|
1092
|
+
finalPrice = new _decimal.default(0);
|
|
974
1093
|
break;
|
|
975
1094
|
}
|
|
976
|
-
return
|
|
1095
|
+
return _decimal.default.max(0, _decimal.default.min(originalPrice, finalPrice)).toDecimalPlaces(2, _decimal.default.ROUND_HALF_UP);
|
|
977
1096
|
}
|
|
978
1097
|
resolveItemRewardLimit(detail, conditionQuantity, triggerQuantity, rewardQuantity, targetUnitCount) {
|
|
979
1098
|
let limit = 0;
|
|
980
1099
|
switch (detail.quantityRule) {
|
|
981
|
-
case
|
|
1100
|
+
case 'all_targets':
|
|
982
1101
|
limit = targetUnitCount;
|
|
983
1102
|
break;
|
|
984
|
-
case
|
|
985
|
-
limit = this.normalizePositiveInteger(
|
|
986
|
-
detail.maxRewardQuantity ?? detail.rewardQuantity,
|
|
987
|
-
rewardQuantity
|
|
988
|
-
);
|
|
1103
|
+
case 'max_per_order':
|
|
1104
|
+
limit = this.normalizePositiveInteger(detail.maxRewardQuantity ?? detail.rewardQuantity, rewardQuantity);
|
|
989
1105
|
break;
|
|
990
|
-
case
|
|
1106
|
+
case 'per_condition_quantity':
|
|
991
1107
|
default:
|
|
992
1108
|
limit = Math.floor(conditionQuantity / triggerQuantity) * rewardQuantity;
|
|
993
1109
|
break;
|
|
994
1110
|
}
|
|
995
1111
|
return Math.min(Math.max(0, limit), targetUnitCount);
|
|
996
1112
|
}
|
|
1113
|
+
|
|
997
1114
|
/**
|
|
998
1115
|
* 获取未参与任何促销的商品
|
|
999
1116
|
*/
|
|
@@ -1008,7 +1125,7 @@ var PromotionEvaluator = class {
|
|
|
1008
1125
|
...product,
|
|
1009
1126
|
quantity: remainingQty,
|
|
1010
1127
|
finalPrice: this.formatPrice(product.price),
|
|
1011
|
-
strategyId:
|
|
1128
|
+
strategyId: undefined,
|
|
1012
1129
|
inPromotion: false,
|
|
1013
1130
|
isSplit: remainingQty < product.quantity
|
|
1014
1131
|
});
|
|
@@ -1016,12 +1133,14 @@ var PromotionEvaluator = class {
|
|
|
1016
1133
|
}
|
|
1017
1134
|
return result;
|
|
1018
1135
|
}
|
|
1136
|
+
|
|
1019
1137
|
/**
|
|
1020
1138
|
* 价格统一保留两位小数
|
|
1021
1139
|
*/
|
|
1022
1140
|
formatPrice(value) {
|
|
1023
|
-
return new
|
|
1141
|
+
return new _decimal.default(value).toDecimalPlaces(2, _decimal.default.ROUND_HALF_UP).toNumber();
|
|
1024
1142
|
}
|
|
1143
|
+
|
|
1025
1144
|
/**
|
|
1026
1145
|
* 获取商品唯一标识 key
|
|
1027
1146
|
* 使用商品的 id 作为唯一标识,不同 id 的商品不会被合并
|
|
@@ -1029,38 +1148,51 @@ var PromotionEvaluator = class {
|
|
|
1029
1148
|
getProductKey(product) {
|
|
1030
1149
|
return String(product.id);
|
|
1031
1150
|
}
|
|
1151
|
+
|
|
1032
1152
|
/**
|
|
1033
1153
|
* 生成随机ID
|
|
1034
1154
|
*/
|
|
1035
1155
|
generateRandomId() {
|
|
1036
|
-
return Math.floor(Math.random() *
|
|
1156
|
+
return Math.floor(Math.random() * 1000000000) + Date.now();
|
|
1037
1157
|
}
|
|
1158
|
+
|
|
1038
1159
|
/**
|
|
1039
1160
|
* 商品匹配结果信息
|
|
1040
1161
|
*/
|
|
1041
1162
|
getProductMatchInfo(product, config) {
|
|
1042
1163
|
const productMatchRule = this.findProductMatchRule(config);
|
|
1043
1164
|
if (!productMatchRule) {
|
|
1044
|
-
|
|
1165
|
+
// 没有商品范围条件,默认适用所有商品(匹配主商品)
|
|
1166
|
+
return {
|
|
1167
|
+
isMatch: true
|
|
1168
|
+
};
|
|
1045
1169
|
}
|
|
1046
1170
|
const configProducts = productMatchRule.value;
|
|
1171
|
+
|
|
1172
|
+
// 1. 先检查主商品是否匹配
|
|
1047
1173
|
if (this.isProductIdMatch(product.product_id, product.product_variant_id, configProducts)) {
|
|
1048
|
-
return {
|
|
1174
|
+
return {
|
|
1175
|
+
isMatch: true
|
|
1176
|
+
};
|
|
1049
1177
|
}
|
|
1178
|
+
|
|
1179
|
+
// 2. 主商品不匹配,检查 bundle 子商品
|
|
1050
1180
|
if (product.bundle && product.bundle.length > 0) {
|
|
1051
1181
|
for (let i = 0; i < product.bundle.length; i++) {
|
|
1052
1182
|
const bundleItem = product.bundle[i];
|
|
1053
|
-
if (this.isProductIdMatch(
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
return { isMatch: true, matchedBundleIndex: i };
|
|
1183
|
+
if (this.isProductIdMatch(bundleItem._bundle_product_id, bundleItem.product_variant_id, configProducts)) {
|
|
1184
|
+
return {
|
|
1185
|
+
isMatch: true,
|
|
1186
|
+
matchedBundleIndex: i
|
|
1187
|
+
};
|
|
1059
1188
|
}
|
|
1060
1189
|
}
|
|
1061
1190
|
}
|
|
1062
|
-
return {
|
|
1191
|
+
return {
|
|
1192
|
+
isMatch: false
|
|
1193
|
+
};
|
|
1063
1194
|
}
|
|
1195
|
+
|
|
1064
1196
|
/**
|
|
1065
1197
|
* 检查商品是否在策略的适用范围内
|
|
1066
1198
|
*/
|
|
@@ -1071,59 +1203,61 @@ var PromotionEvaluator = class {
|
|
|
1071
1203
|
return this.getProductMatchInfo(product, config).isMatch;
|
|
1072
1204
|
}
|
|
1073
1205
|
shouldSkipStrategyForProduct(product, config) {
|
|
1074
|
-
return Boolean(product._promotionOnlyForItemReward) && !this.hasActionType(config,
|
|
1206
|
+
return Boolean(product._promotionOnlyForItemReward) && !this.hasActionType(config, _type.PROMOTION_ACTION_TYPES.ITEM_REWARD);
|
|
1075
1207
|
}
|
|
1076
1208
|
hasActionType(config, actionType) {
|
|
1077
|
-
return (config.actions || []).some(
|
|
1209
|
+
return (config.actions || []).some(action => action?.type === actionType);
|
|
1078
1210
|
}
|
|
1211
|
+
|
|
1079
1212
|
/**
|
|
1080
1213
|
* 查找商品匹配规则
|
|
1081
1214
|
*/
|
|
1082
1215
|
findProductMatchRule(config) {
|
|
1083
|
-
|
|
1084
|
-
if (!((_a = config == null ? void 0 : config.conditions) == null ? void 0 : _a.rules)) {
|
|
1216
|
+
if (!config?.conditions?.rules) {
|
|
1085
1217
|
return null;
|
|
1086
1218
|
}
|
|
1087
|
-
return config.conditions.rules.find(
|
|
1088
|
-
(rule) => rule.field === "productIdAndVariantId" && (rule.operator === "product_match" || rule.operator === "object_in")
|
|
1089
|
-
);
|
|
1219
|
+
return config.conditions.rules.find(rule => rule.field === 'productIdAndVariantId' && (rule.operator === 'product_match' || rule.operator === 'object_in'));
|
|
1090
1220
|
}
|
|
1221
|
+
|
|
1091
1222
|
/**
|
|
1092
1223
|
* 检查商品ID是否匹配配置列表
|
|
1093
1224
|
*/
|
|
1094
1225
|
isProductIdMatch(productId, productVariantId, configProducts) {
|
|
1095
|
-
return configProducts.some(
|
|
1226
|
+
return configProducts.some(config => {
|
|
1227
|
+
// 首先检查 product_id
|
|
1096
1228
|
if (config.product_id !== productId) {
|
|
1097
1229
|
return false;
|
|
1098
1230
|
}
|
|
1231
|
+
|
|
1232
|
+
// 如果配置的 product_variant_id = 0,只需要 product_id 匹配
|
|
1099
1233
|
if (config.product_variant_id === 0) {
|
|
1100
1234
|
return true;
|
|
1101
1235
|
}
|
|
1236
|
+
|
|
1237
|
+
// 否则需要 product_variant_id 精确匹配
|
|
1102
1238
|
return config.product_variant_id === productVariantId;
|
|
1103
1239
|
});
|
|
1104
1240
|
}
|
|
1241
|
+
|
|
1105
1242
|
/**
|
|
1106
1243
|
* 检查商品是否匹配配置(兼容旧方法)
|
|
1107
1244
|
*/
|
|
1108
1245
|
isProductMatch(product, configProducts) {
|
|
1109
1246
|
return this.isProductIdMatch(product.product_id, product.product_variant_id, configProducts);
|
|
1110
1247
|
}
|
|
1248
|
+
|
|
1111
1249
|
/**
|
|
1112
1250
|
* 获取展示配置
|
|
1113
1251
|
*/
|
|
1114
1252
|
getDisplayConfig(config) {
|
|
1115
|
-
var _a;
|
|
1116
1253
|
const custom = config.metadata.custom;
|
|
1117
|
-
if (
|
|
1254
|
+
if (custom?.display?.product_card) {
|
|
1118
1255
|
return {
|
|
1119
1256
|
text: custom.display.product_card.text,
|
|
1120
1257
|
type: custom.display.product_card.type
|
|
1121
1258
|
};
|
|
1122
1259
|
}
|
|
1123
|
-
return
|
|
1260
|
+
return undefined;
|
|
1124
1261
|
}
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
0 && (module.exports = {
|
|
1128
|
-
PromotionEvaluator
|
|
1129
|
-
});
|
|
1262
|
+
}
|
|
1263
|
+
exports.PromotionEvaluator = PromotionEvaluator;
|