@tbox.cn/app-provider-mock 0.2.0 → 0.3.0
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/README.md +38 -0
- package/dist/server/index.d.ts +41 -38
- package/dist/server/index.js +258 -130
- package/package.json +6 -5
- package/schemas/mall-info.config.json +11 -0
- package/schemas/member.config.json +13 -0
- package/schemas/parking.config.json +19 -0
- package/schemas/promotion.config.json +13 -0
- package/schemas/shopping.config.json +12 -0
- package/src/server/config.ts +26 -0
- package/src/server/index.ts +16 -4
- package/src/server/mall-info.ts +27 -12
- package/src/server/member.ts +45 -17
- package/src/server/parking.ts +78 -49
- package/src/server/promotion.ts +77 -56
- package/src/server/shopping.ts +41 -3
- package/tbox.module.json +34 -17
- package/tests/config-schema.test.ts +92 -0
- package/tests/integrations-e2e.test.ts +117 -2
- package/tests/mall-info.test.ts +11 -2
- package/tests/mock-impl.test.ts +103 -0
package/dist/server/index.js
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
// src/server/index.ts
|
|
2
2
|
import { readLoginMallId } from "@tbox.cn/app-contracts-mall";
|
|
3
3
|
|
|
4
|
+
// src/server/config.ts
|
|
5
|
+
function resolveConfig(raw, defaults, intKeys = []) {
|
|
6
|
+
const src = raw && typeof raw === "object" ? raw : {};
|
|
7
|
+
const out = {};
|
|
8
|
+
for (const [key, fallback] of Object.entries(defaults)) {
|
|
9
|
+
const value = src[key];
|
|
10
|
+
const ok = typeof value === typeof fallback && (typeof value !== "number" || Number.isFinite(value) && (!intKeys.includes(key) || Number.isInteger(value))) && (typeof value !== "string" || value !== "");
|
|
11
|
+
out[key] = ok ? value : fallback;
|
|
12
|
+
}
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
|
|
4
16
|
// src/server/member.ts
|
|
5
|
-
var
|
|
6
|
-
|
|
7
|
-
|
|
17
|
+
var MEMBER_CFG_DEFAULTS = {
|
|
18
|
+
memberName: "\u793A\u4F8B\u4F1A\u5458",
|
|
19
|
+
cardName: "\u793A\u4F8B\u4F1A\u5458\u5361",
|
|
20
|
+
levelName: "",
|
|
21
|
+
// '' 哨兵 = 不展示等级区(MemberCardSnapshot.levelName 可选位)
|
|
22
|
+
initialPoints: 300,
|
|
23
|
+
enrollUrl: "alipays://platformapi/startapp?appId=demo&page=v/index/index",
|
|
24
|
+
loginUrl: "alipays://platformapi/startapp?appId=demo&page=v/login/index"
|
|
25
|
+
};
|
|
26
|
+
var MEMBER_CFG_INT_KEYS = ["initialPoints"];
|
|
8
27
|
var mockMemberState = {
|
|
9
28
|
enrolled: /* @__PURE__ */ new Set(),
|
|
10
29
|
balances: /* @__PURE__ */ new Map(),
|
|
@@ -15,7 +34,6 @@ var MOCK_SHELL_USER_ID = "demo-user";
|
|
|
15
34
|
function enrollMockMember(userId) {
|
|
16
35
|
mockMemberState.enrolled.add(userId);
|
|
17
36
|
mockMemberState.loggedOut.delete(userId);
|
|
18
|
-
if (!mockMemberState.balances.has(userId)) mockMemberState.balances.set(userId, 300);
|
|
19
37
|
}
|
|
20
38
|
function isMockMemberEnrolled(userId) {
|
|
21
39
|
return mockMemberState.enrolled.has(userId);
|
|
@@ -40,11 +58,21 @@ var MockMemberServiceImpl = class {
|
|
|
40
58
|
* 透传素材,mock 出厂面恒配 demo 值)。demo 值厂商中立(appId=demo),点击在壳内
|
|
41
59
|
* navigateTo 失败即无害降级。
|
|
42
60
|
*/
|
|
43
|
-
enrollUrl
|
|
61
|
+
enrollUrl;
|
|
44
62
|
/** 登录页深链(同上,可选透传素材;供未登录引导卡跳转)。 */
|
|
45
|
-
loginUrl
|
|
63
|
+
loginUrl;
|
|
64
|
+
/** 配置快照(fail-soft,构造期一次解析;键集 ≡ MEMBER_CFG_DEFAULTS) */
|
|
65
|
+
cfg;
|
|
66
|
+
/** 积分流水(金额随 initialPoints 派生——与余额同源) */
|
|
67
|
+
usage;
|
|
46
68
|
/** 默认访客(无种子):demo-user 初始 not_enrolled,经入会闭环翻为 active */
|
|
47
|
-
constructor(initial = {}) {
|
|
69
|
+
constructor(initial = {}, config = {}) {
|
|
70
|
+
this.cfg = resolveConfig(config, MEMBER_CFG_DEFAULTS, MEMBER_CFG_INT_KEYS);
|
|
71
|
+
this.enrollUrl = this.cfg.enrollUrl;
|
|
72
|
+
this.loginUrl = this.cfg.loginUrl;
|
|
73
|
+
this.usage = [
|
|
74
|
+
{ usageRecordRef: "demo:usage:1", title: "\u5F00\u5361\u793C", amount: `+${String(this.cfg.initialPoints)}`, usedAt: (/* @__PURE__ */ new Date()).toISOString(), description: "mock \u6F14\u793A\u6D41\u6C34" }
|
|
75
|
+
];
|
|
48
76
|
for (const [userId, balance] of Object.entries(initial)) {
|
|
49
77
|
mockMemberState.enrolled.add(userId);
|
|
50
78
|
mockMemberState.balances.set(userId, balance);
|
|
@@ -60,44 +88,51 @@ var MockMemberServiceImpl = class {
|
|
|
60
88
|
};
|
|
61
89
|
}
|
|
62
90
|
async getProfile(_userId) {
|
|
63
|
-
return { labels: [{ labelRef: "demo:label:1", name:
|
|
91
|
+
return { labels: [{ labelRef: "demo:label:1", name: this.cfg.memberName }], observedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
64
92
|
}
|
|
65
93
|
async getCard(userId) {
|
|
66
94
|
if (!isMockMemberActive(userId)) return null;
|
|
67
95
|
return {
|
|
68
96
|
cardRef: `demo:card:${userId}`,
|
|
69
|
-
cardDisplay:
|
|
97
|
+
cardDisplay: this.cfg.cardName,
|
|
70
98
|
status: "active",
|
|
99
|
+
...this.cfg.levelName !== "" ? { levelName: this.cfg.levelName } : {},
|
|
71
100
|
observedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
72
101
|
};
|
|
73
102
|
}
|
|
74
103
|
async queryPoints(userId) {
|
|
75
|
-
return { userId, balance:
|
|
104
|
+
return { userId, balance: this.currentBalanceOf(userId), membershipLabel: this.cfg.cardName };
|
|
76
105
|
}
|
|
77
|
-
async getBenefits(
|
|
78
|
-
return { availablePoints:
|
|
106
|
+
async getBenefits(userId) {
|
|
107
|
+
return { availablePoints: this.currentBalanceOf(userId), levelName: "\u793A\u4F8B\u7B49\u7EA7", observedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
79
108
|
}
|
|
80
109
|
async listPointUsage(_userId, page, pageSize) {
|
|
81
110
|
const start = (page - 1) * pageSize;
|
|
82
|
-
return { items:
|
|
111
|
+
return { items: this.usage.slice(start, start + pageSize), total: this.usage.length };
|
|
83
112
|
}
|
|
84
113
|
async queryDiscount() {
|
|
85
114
|
return { level: "silver", discount: 0.95 };
|
|
86
115
|
}
|
|
87
116
|
async deductPoints(userId, amount, _key) {
|
|
88
|
-
const balance =
|
|
117
|
+
const balance = this.currentBalanceOf(userId);
|
|
89
118
|
if (balance < amount) return false;
|
|
90
119
|
mockMemberState.balances.set(userId, balance - amount);
|
|
91
120
|
return true;
|
|
92
121
|
}
|
|
93
122
|
async refundPoints(userId, amount, _key) {
|
|
94
|
-
mockMemberState.balances.set(userId,
|
|
123
|
+
mockMemberState.balances.set(userId, this.currentBalanceOf(userId) + amount);
|
|
95
124
|
return true;
|
|
96
125
|
}
|
|
97
126
|
async awardPoints(userId, amount) {
|
|
98
|
-
mockMemberState.balances.set(userId,
|
|
127
|
+
mockMemberState.balances.set(userId, this.currentBalanceOf(userId) + amount);
|
|
99
128
|
return true;
|
|
100
129
|
}
|
|
130
|
+
/** 余额单源:显式增减覆盖值 ??(已入会 ? 配置初值 : 0)——入会不写种子(惰性读) */
|
|
131
|
+
currentBalanceOf(userId) {
|
|
132
|
+
const override = mockMemberState.balances.get(userId);
|
|
133
|
+
if (override !== void 0) return override;
|
|
134
|
+
return isMockMemberEnrolled(userId) ? this.cfg.initialPoints : 0;
|
|
135
|
+
}
|
|
101
136
|
async enroll(input) {
|
|
102
137
|
enrollMockMember(input.userId);
|
|
103
138
|
return { status: "active", memberRef: `demo:member:${input.userId}` };
|
|
@@ -121,46 +156,76 @@ var MockMemberServiceImpl = class {
|
|
|
121
156
|
|
|
122
157
|
// src/server/parking.ts
|
|
123
158
|
var now = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
124
|
-
var
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
revision: 1,
|
|
140
|
-
paymentState: "UNPAID"
|
|
141
|
-
};
|
|
142
|
-
}
|
|
159
|
+
var PARKING_CFG_DEFAULTS = {
|
|
160
|
+
parkName: "\u793A\u4F8B\u505C\u8F66\u573A",
|
|
161
|
+
feeName: "\u505C\u8F66\u8D39\uFF083 \u5C0F\u65F6\uFF09",
|
|
162
|
+
feeCents: 1500,
|
|
163
|
+
remainingSpaces: 128,
|
|
164
|
+
firstHourRate: "\u514D\u8D39",
|
|
165
|
+
memberBenefitEnabled: true,
|
|
166
|
+
memberBenefitDiscountCents: 75,
|
|
167
|
+
pointsBenefitEnabled: true,
|
|
168
|
+
invoiceEnabled: true,
|
|
169
|
+
vehicleFloor: "B1",
|
|
170
|
+
vehicleZone: "A \u533A",
|
|
171
|
+
vehicleSpaceNo: "A-128"
|
|
172
|
+
};
|
|
173
|
+
var PARKING_CFG_INT_KEYS = ["feeCents", "remainingSpaces", "memberBenefitDiscountCents"];
|
|
143
174
|
var MockParkingServiceImpl = class {
|
|
144
175
|
vehicles = /* @__PURE__ */ new Map();
|
|
145
|
-
records
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
amountCents: 1500,
|
|
150
|
-
paidAt: now(),
|
|
151
|
-
status: "PAID",
|
|
152
|
-
parkName: "\u793A\u4F8B\u505C\u8F66\u573A",
|
|
153
|
-
orderCompleted: true
|
|
154
|
-
}
|
|
155
|
-
];
|
|
156
|
-
constructor(mallId = "mall-demo") {
|
|
176
|
+
records;
|
|
177
|
+
/** 配置快照(fail-soft,构造期一次解析;键集 ≡ PARKING_CFG_DEFAULTS) */
|
|
178
|
+
cfg;
|
|
179
|
+
constructor(mallId = "mall-demo", config = {}) {
|
|
157
180
|
this.mallId = mallId;
|
|
158
181
|
void this.mallId;
|
|
182
|
+
this.cfg = resolveConfig(config, PARKING_CFG_DEFAULTS, PARKING_CFG_INT_KEYS);
|
|
183
|
+
this.records = [
|
|
184
|
+
{
|
|
185
|
+
recordRef: "demo:record:1",
|
|
186
|
+
plateNo: "demo-plate",
|
|
187
|
+
amountCents: this.cfg.feeCents,
|
|
188
|
+
paidAt: now(),
|
|
189
|
+
status: "PAID",
|
|
190
|
+
parkName: this.cfg.parkName,
|
|
191
|
+
orderCompleted: true
|
|
192
|
+
}
|
|
193
|
+
];
|
|
159
194
|
}
|
|
160
195
|
mallId;
|
|
196
|
+
/**
|
|
197
|
+
* kind 必须下发:客户端按它分流(discount = 自动生效、不进勾选区;points = 积分入口),
|
|
198
|
+
* 服务端 projectPaymentOptions 也只认 kind==='points'。缺省时整套分流静默失效——
|
|
199
|
+
* 等级权益会退化成一个可勾选项,积分入口则从不出现。
|
|
200
|
+
*/
|
|
201
|
+
benefits() {
|
|
202
|
+
const benefits = [];
|
|
203
|
+
if (this.cfg.memberBenefitEnabled) {
|
|
204
|
+
benefits.push({ benefitRef: "demo:benefit:member-discount", title: "\u4F1A\u5458 95 \u6298", description: "mock \u6F14\u793A\u6743\u76CA", kind: "discount", autoSelected: true, estimatedDiscountCents: this.cfg.memberBenefitDiscountCents });
|
|
205
|
+
}
|
|
206
|
+
if (this.cfg.pointsBenefitEnabled) {
|
|
207
|
+
benefits.push({ benefitRef: "demo:benefit:points-offset", title: "\u79EF\u5206\u62B5 5 \u5143", description: "mock \u6F14\u793A\u6743\u76CA", kind: "points", autoSelected: false, estimatedDiscountCents: 500, requiredPoints: 500, previewPayableCents: 1e3 });
|
|
208
|
+
}
|
|
209
|
+
return benefits;
|
|
210
|
+
}
|
|
211
|
+
quote(q, plateNo) {
|
|
212
|
+
return {
|
|
213
|
+
quoteRef: `demo:quote:${plateNo}`,
|
|
214
|
+
plateNo,
|
|
215
|
+
mallId: q.scope.mallId ?? "demo",
|
|
216
|
+
entryAt: now(),
|
|
217
|
+
feeItems: [{ name: this.cfg.feeName, amountCents: this.cfg.feeCents }],
|
|
218
|
+
totalCents: this.cfg.feeCents,
|
|
219
|
+
discountCents: 0,
|
|
220
|
+
payableCents: this.cfg.feeCents,
|
|
221
|
+
appliedBenefits: [],
|
|
222
|
+
revision: 1,
|
|
223
|
+
paymentState: "UNPAID"
|
|
224
|
+
};
|
|
225
|
+
}
|
|
161
226
|
async queryFee(q, input) {
|
|
162
227
|
const plateNo = input.plateNo ?? (input.vehicleRef ? this.vehicles.get(input.vehicleRef)?.plateNo : void 0) ?? "demo-plate";
|
|
163
|
-
const quote =
|
|
228
|
+
const quote = this.quote(q, plateNo);
|
|
164
229
|
return quote ? [quote] : [];
|
|
165
230
|
}
|
|
166
231
|
async listVehicles() {
|
|
@@ -178,7 +243,7 @@ var MockParkingServiceImpl = class {
|
|
|
178
243
|
return record;
|
|
179
244
|
}
|
|
180
245
|
async getInvoiceSettings() {
|
|
181
|
-
return { parkingInvoiceEnabled:
|
|
246
|
+
return { parkingInvoiceEnabled: this.cfg.invoiceEnabled };
|
|
182
247
|
}
|
|
183
248
|
async listInvoiceableOrders(_q, page, pageSize) {
|
|
184
249
|
const all = this.records.filter((r) => r.orderCompleted === true);
|
|
@@ -188,26 +253,26 @@ var MockParkingServiceImpl = class {
|
|
|
188
253
|
async queryParkingInfo(_q, kind) {
|
|
189
254
|
return {
|
|
190
255
|
kind,
|
|
191
|
-
title:
|
|
256
|
+
title: this.cfg.parkName,
|
|
192
257
|
entries: [
|
|
193
|
-
{ label: "\u5269\u4F59\u8F66\u4F4D", value:
|
|
194
|
-
{ label: "\u9996\u5C0F\u65F6\u8D39\u7387", value:
|
|
195
|
-
{ label: "\u4F1A\u5458\u6743\u76CA", value: "\u4F1A\u5458 95 \u6298" }
|
|
258
|
+
{ label: "\u5269\u4F59\u8F66\u4F4D", value: String(this.cfg.remainingSpaces) },
|
|
259
|
+
{ label: "\u9996\u5C0F\u65F6\u8D39\u7387", value: this.cfg.firstHourRate },
|
|
260
|
+
{ label: "\u4F1A\u5458\u6743\u76CA", value: this.cfg.memberBenefitEnabled ? "\u4F1A\u5458 95 \u6298" : "\u2014" }
|
|
196
261
|
]
|
|
197
262
|
};
|
|
198
263
|
}
|
|
199
264
|
async queryApplicableBenefits(_q, _plateNo) {
|
|
200
|
-
return
|
|
265
|
+
return this.benefits();
|
|
201
266
|
}
|
|
202
267
|
async locateVehicle(_q, _plateNo) {
|
|
203
|
-
return { floorName:
|
|
268
|
+
return { floorName: this.cfg.vehicleFloor, zoneName: this.cfg.vehicleZone, spaceNo: this.cfg.vehicleSpaceNo, walkingRoute: "\u7535\u68AF\u5DE6\u8F6C 50 \u7C73" };
|
|
204
269
|
}
|
|
205
270
|
// ===== 支付三步链(mock:reconcile 恒 PAID 收口) =====
|
|
206
271
|
async createOrder(_q, quoteRef) {
|
|
207
272
|
return { payOrderId: `demo:pay:${quoteRef}`, paymentRef: `demo:payment:${quoteRef}` };
|
|
208
273
|
}
|
|
209
274
|
async getOrderInfo(_q, _paymentRef) {
|
|
210
|
-
return { orderInfo: "demo-order-info", channel: "50", payableFeeCents:
|
|
275
|
+
return { orderInfo: "demo-order-info", channel: "50", payableFeeCents: this.cfg.feeCents };
|
|
211
276
|
}
|
|
212
277
|
async getPaymentSign(_q, _paymentRef) {
|
|
213
278
|
return { orderInfo: "demo-order-info" };
|
|
@@ -218,8 +283,8 @@ var MockParkingServiceImpl = class {
|
|
|
218
283
|
return "PAID";
|
|
219
284
|
}
|
|
220
285
|
async reprice(q, quoteRef, benefitRefs) {
|
|
221
|
-
const base =
|
|
222
|
-
const discount =
|
|
286
|
+
const base = this.quote(q, "demo-plate");
|
|
287
|
+
const discount = this.benefits().filter((b) => benefitRefs.includes(b.benefitRef)).reduce((sum, b) => sum + (b.estimatedDiscountCents ?? 0), 0);
|
|
223
288
|
return {
|
|
224
289
|
...base,
|
|
225
290
|
quoteRef: `${quoteRef}#r${Date.now()}`,
|
|
@@ -251,79 +316,97 @@ var MockParkingServiceImpl = class {
|
|
|
251
316
|
// src/server/promotion.ts
|
|
252
317
|
var now2 = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
253
318
|
var inDays = (d) => new Date(Date.now() + d * 864e5).toISOString();
|
|
254
|
-
var
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
availability: { status: "AVAILABLE", reasonCodes: [], evaluatedAt: now2(), authoritative: false },
|
|
264
|
-
participation: { mode: "CLAIM_THEN_PURCHASE", steps: [{ title: "\u9886\u53D6", description: "\u70B9\u51FB\u9886\u53D6\u540E\u81EA\u52A8\u5B58\u5165\u5361\u5305" }] }
|
|
265
|
-
},
|
|
266
|
-
{
|
|
267
|
-
offerRef: "demo:groupon:2001",
|
|
268
|
-
offerType: "GROUPON",
|
|
269
|
-
title: "\u9910\u996E\u53CC\u4EBA\u5957\u9910 5 \u6298",
|
|
270
|
-
subtitle: "mock \u6F14\u793A\u56E2\u8D2D",
|
|
271
|
-
tags: ["\u9910\u996E"],
|
|
272
|
-
validity: { start: now2(), end: inDays(15) },
|
|
273
|
-
facets: [{ kind: "GROUPON", requiredParticipants: 2 }],
|
|
274
|
-
availability: { status: "AVAILABLE", reasonCodes: [], evaluatedAt: now2(), authoritative: false },
|
|
275
|
-
participation: { mode: "CLAIM_THEN_PURCHASE", steps: [{ title: "\u8D2D\u4E70", description: "\u6309\u56E2\u8D2D\u4EF7\u4E0B\u5355" }] }
|
|
276
|
-
}
|
|
277
|
-
];
|
|
278
|
-
var DEMO_ENTITLEMENTS = [
|
|
279
|
-
{
|
|
280
|
-
entitlementRef: "demo:entitlement:1",
|
|
281
|
-
offerRef: "demo:coupon:1001",
|
|
282
|
-
title: "\u6EE1 100 \u51CF 20 \u5143\u5238",
|
|
283
|
-
entitlementType: "COUPON",
|
|
284
|
-
status: "EFFECTIVE",
|
|
285
|
-
codeMasked: "DEMO-****-1001"
|
|
286
|
-
}
|
|
287
|
-
];
|
|
288
|
-
var DEMO_ACTIVITIES = [
|
|
289
|
-
{
|
|
290
|
-
activityRef: "demo:activity:3001",
|
|
291
|
-
title: "\u793A\u4F8B\u5546\u573A\u5468\u5E74\u5E86",
|
|
292
|
-
subtitle: "mock \u6F14\u793A\u6D3B\u52A8",
|
|
293
|
-
registrationRequired: false,
|
|
294
|
-
offerRefs: ["demo:coupon:1001"]
|
|
295
|
-
}
|
|
296
|
-
];
|
|
319
|
+
var PROMOTION_CFG_DEFAULTS = {
|
|
320
|
+
couponTitle: "\u6EE1 100 \u51CF 20 \u5143\u5238",
|
|
321
|
+
couponValueCents: 2e3,
|
|
322
|
+
couponThresholdCents: 1e4,
|
|
323
|
+
couponValidityDays: 30,
|
|
324
|
+
grouponEnabled: true,
|
|
325
|
+
activityTitle: "\u793A\u4F8B\u5546\u573A\u5468\u5E74\u5E86"
|
|
326
|
+
};
|
|
327
|
+
var PROMOTION_CFG_INT_KEYS = ["couponValueCents", "couponThresholdCents", "couponValidityDays"];
|
|
297
328
|
var MockPromotionServiceImpl = class {
|
|
329
|
+
/** 配置快照(fail-soft,构造期一次解析;键集 ≡ PROMOTION_CFG_DEFAULTS) */
|
|
330
|
+
cfg;
|
|
331
|
+
/** 供/资产/活动三表(构造期按 cfg 构建——请求级实例,成本可忽略) */
|
|
332
|
+
offers;
|
|
333
|
+
entitlements;
|
|
334
|
+
activities;
|
|
335
|
+
constructor(config = {}) {
|
|
336
|
+
this.cfg = resolveConfig(config, PROMOTION_CFG_DEFAULTS, PROMOTION_CFG_INT_KEYS);
|
|
337
|
+
const ts = now2();
|
|
338
|
+
const couponOffer = {
|
|
339
|
+
offerRef: "demo:coupon:1001",
|
|
340
|
+
offerType: "COUPON",
|
|
341
|
+
title: this.cfg.couponTitle,
|
|
342
|
+
subtitle: "\u5168\u573A\u901A\u7528\uFF08mock \u6F14\u793A\uFF09",
|
|
343
|
+
tags: ["\u901A\u7528"],
|
|
344
|
+
validity: { start: ts, end: inDays(this.cfg.couponValidityDays) },
|
|
345
|
+
facets: [{ kind: "COUPON", discountType: "AMOUNT", value: this.cfg.couponValueCents, thresholdCents: this.cfg.couponThresholdCents }],
|
|
346
|
+
availability: { status: "AVAILABLE", reasonCodes: [], evaluatedAt: ts, authoritative: false },
|
|
347
|
+
participation: { mode: "CLAIM_THEN_PURCHASE", steps: [{ title: "\u9886\u53D6", description: "\u70B9\u51FB\u9886\u53D6\u540E\u81EA\u52A8\u5B58\u5165\u5361\u5305" }] }
|
|
348
|
+
};
|
|
349
|
+
const grouponOffer = {
|
|
350
|
+
offerRef: "demo:groupon:2001",
|
|
351
|
+
offerType: "GROUPON",
|
|
352
|
+
title: "\u9910\u996E\u53CC\u4EBA\u5957\u9910 5 \u6298",
|
|
353
|
+
subtitle: "mock \u6F14\u793A\u56E2\u8D2D",
|
|
354
|
+
tags: ["\u9910\u996E"],
|
|
355
|
+
validity: { start: ts, end: inDays(15) },
|
|
356
|
+
facets: [{ kind: "GROUPON", requiredParticipants: 2 }],
|
|
357
|
+
availability: { status: "AVAILABLE", reasonCodes: [], evaluatedAt: ts, authoritative: false },
|
|
358
|
+
participation: { mode: "CLAIM_THEN_PURCHASE", steps: [{ title: "\u8D2D\u4E70", description: "\u6309\u56E2\u8D2D\u4EF7\u4E0B\u5355" }] }
|
|
359
|
+
};
|
|
360
|
+
this.offers = this.cfg.grouponEnabled ? [couponOffer, grouponOffer] : [couponOffer];
|
|
361
|
+
this.entitlements = [
|
|
362
|
+
{
|
|
363
|
+
entitlementRef: "demo:entitlement:1",
|
|
364
|
+
offerRef: "demo:coupon:1001",
|
|
365
|
+
title: this.cfg.couponTitle,
|
|
366
|
+
entitlementType: "COUPON",
|
|
367
|
+
status: "EFFECTIVE",
|
|
368
|
+
codeMasked: "DEMO-****-1001"
|
|
369
|
+
}
|
|
370
|
+
];
|
|
371
|
+
this.activities = [
|
|
372
|
+
{
|
|
373
|
+
activityRef: "demo:activity:3001",
|
|
374
|
+
title: this.cfg.activityTitle,
|
|
375
|
+
subtitle: "mock \u6F14\u793A\u6D3B\u52A8",
|
|
376
|
+
registrationRequired: false,
|
|
377
|
+
offerRefs: ["demo:coupon:1001"]
|
|
378
|
+
}
|
|
379
|
+
];
|
|
380
|
+
}
|
|
298
381
|
async searchOffers(_q, input) {
|
|
299
382
|
const keyword = input.keyword?.trim();
|
|
300
|
-
const items = keyword ?
|
|
383
|
+
const items = keyword ? this.offers.filter((o) => o.title.includes(keyword) || o.tags.some((t) => t.includes(keyword))) : this.offers;
|
|
301
384
|
return { items, hasMore: false };
|
|
302
385
|
}
|
|
303
386
|
async getOfferDetail(_q, input) {
|
|
304
|
-
if (input.offerRef) return
|
|
387
|
+
if (input.offerRef) return this.offers.find((o) => o.offerRef === input.offerRef) ?? null;
|
|
305
388
|
if (input.entitlementRef) {
|
|
306
|
-
const ent =
|
|
307
|
-
if (ent?.offerRef) return
|
|
389
|
+
const ent = this.entitlements.find((e) => e.entitlementRef === input.entitlementRef);
|
|
390
|
+
if (ent?.offerRef) return this.offers.find((o) => o.offerRef === ent.offerRef) ?? null;
|
|
308
391
|
return null;
|
|
309
392
|
}
|
|
310
|
-
if (input.keyword) return
|
|
393
|
+
if (input.keyword) return this.offers.find((o) => o.title.includes(input.keyword)) ?? null;
|
|
311
394
|
return null;
|
|
312
395
|
}
|
|
313
396
|
async recommendOffers() {
|
|
314
|
-
return
|
|
397
|
+
return this.offers;
|
|
315
398
|
}
|
|
316
399
|
async listEntitlements(_q, _input) {
|
|
317
|
-
return { items:
|
|
400
|
+
return { items: this.entitlements, hasMore: false };
|
|
318
401
|
}
|
|
319
402
|
async getEntitlementDetail(_q, entitlementRef) {
|
|
320
|
-
return
|
|
403
|
+
return this.entitlements.find((e) => e.entitlementRef === entitlementRef) ?? null;
|
|
321
404
|
}
|
|
322
405
|
async listActivities() {
|
|
323
|
-
return { items:
|
|
406
|
+
return { items: this.activities, hasMore: false };
|
|
324
407
|
}
|
|
325
408
|
async getActivityDetail(_q, activityRef) {
|
|
326
|
-
const activity =
|
|
409
|
+
const activity = this.activities.find((a) => a.activityRef === activityRef);
|
|
327
410
|
if (!activity) return null;
|
|
328
411
|
return { ...activity, sessions: [], description: "mock \u6F14\u793A\u6D3B\u52A8\u8BE6\u60C5" };
|
|
329
412
|
}
|
|
@@ -345,6 +428,16 @@ var MockPromotionServiceImpl = class {
|
|
|
345
428
|
};
|
|
346
429
|
|
|
347
430
|
// src/server/shopping.ts
|
|
431
|
+
var SHOPPING_CFG_DEFAULTS = {
|
|
432
|
+
extraShopName: "",
|
|
433
|
+
// '' = 不追加门店(空串 = 缺省/不生效语义)
|
|
434
|
+
extraShopCategory: "\u9910\u996E",
|
|
435
|
+
extraShopFloor: "3 \u697C",
|
|
436
|
+
extraShopUnit: "304",
|
|
437
|
+
extraShopOpen: true
|
|
438
|
+
};
|
|
439
|
+
var SHOPPING_CFG_INT_KEYS = [];
|
|
440
|
+
var SHOPPING_FLOORS = ["B1", "1 \u697C", "2 \u697C", "3 \u697C", "4 \u697C"];
|
|
348
441
|
var DEMO_BUILDINGS = [
|
|
349
442
|
{
|
|
350
443
|
buildingName: "A \u680B",
|
|
@@ -425,14 +518,35 @@ function toFloorRecord(floor) {
|
|
|
425
518
|
return { floorCode: floor.floorName, floorName: floor.floorName, businessCategories: cats, merchantCount: floor.shops.length };
|
|
426
519
|
}
|
|
427
520
|
var MockShoppingServiceImpl = class {
|
|
521
|
+
/** 配置快照(fail-soft,构造期一次解析;键集 ≡ SHOPPING_CFG_DEFAULTS) */
|
|
522
|
+
cfg;
|
|
428
523
|
buildings;
|
|
429
524
|
shops;
|
|
430
525
|
categories;
|
|
431
|
-
constructor(seed = DEMO_BUILDINGS) {
|
|
432
|
-
this.
|
|
433
|
-
|
|
526
|
+
constructor(seed = DEMO_BUILDINGS, config = {}) {
|
|
527
|
+
this.cfg = resolveConfig(config, SHOPPING_CFG_DEFAULTS, SHOPPING_CFG_INT_KEYS);
|
|
528
|
+
const withExtra = this.withExtraShop(seed);
|
|
529
|
+
this.buildings = withExtra;
|
|
530
|
+
this.shops = flattenShops(withExtra);
|
|
434
531
|
this.categories = aggregateCategories(this.shops);
|
|
435
532
|
}
|
|
533
|
+
/** extraShopName 非空 → 指定楼层追加演示门店(名称 trim 判空 + 楼层白名单回退——消费点守卫) */
|
|
534
|
+
withExtraShop(seed) {
|
|
535
|
+
const name = this.cfg.extraShopName.trim();
|
|
536
|
+
if (!name) return seed;
|
|
537
|
+
const floor = SHOPPING_FLOORS.includes(this.cfg.extraShopFloor) ? this.cfg.extraShopFloor : "3 \u697C";
|
|
538
|
+
const extra = {
|
|
539
|
+
merchantId: "demo-shop-1013",
|
|
540
|
+
name,
|
|
541
|
+
businessCategories: [this.cfg.extraShopCategory],
|
|
542
|
+
unitName: this.cfg.extraShopUnit,
|
|
543
|
+
status: this.cfg.extraShopOpen ? { code: "enabled", label: "\u8425\u4E1A\u4E2D" } : { code: "disabled", label: "\u4F11\u606F\u4E2D" }
|
|
544
|
+
};
|
|
545
|
+
return seed.map((b) => ({
|
|
546
|
+
...b,
|
|
547
|
+
floors: b.floors.map((f) => f.floorName === floor ? { ...f, shops: [...f.shops, extra] } : f)
|
|
548
|
+
}));
|
|
549
|
+
}
|
|
436
550
|
// ===== ShopDirectoryQueryPort =====
|
|
437
551
|
async searchShops(criteria) {
|
|
438
552
|
const kws = (criteria.keywords ?? (criteria.keyword?.trim() ? [criteria.keyword.trim()] : [])).map((k) => k.trim().toLowerCase()).filter(Boolean);
|
|
@@ -496,19 +610,30 @@ var MockShoppingServiceImpl = class {
|
|
|
496
610
|
// src/server/mall-info.ts
|
|
497
611
|
var MOCK_MALL_INFO_PROVIDER = "mock";
|
|
498
612
|
var MOCK_MALL_INFO_IMPL = "mock-mall-info@1";
|
|
613
|
+
var MALL_INFO_CFG_DEFAULTS = {
|
|
614
|
+
name: "",
|
|
615
|
+
// '' 哨兵 = 动态别名回退
|
|
616
|
+
address: "\u793A\u4F8B\u5546\u573A\u5730\u5740\uFF08demo\uFF09",
|
|
617
|
+
latitude: 30,
|
|
618
|
+
longitude: 120
|
|
619
|
+
};
|
|
620
|
+
var MALL_INFO_CFG_INT_KEYS = [];
|
|
499
621
|
var MALL_ID_RE = /^[\w-]{1,32}$/;
|
|
500
|
-
var mockMallInfoFactory = () =>
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
mallId
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
}
|
|
622
|
+
var mockMallInfoFactory = (input) => {
|
|
623
|
+
const cfg = resolveConfig(input.config, MALL_INFO_CFG_DEFAULTS, MALL_INFO_CFG_INT_KEYS);
|
|
624
|
+
return {
|
|
625
|
+
async getMallInfo(mallId) {
|
|
626
|
+
if (!MALL_ID_RE.test(mallId)) return void 0;
|
|
627
|
+
return {
|
|
628
|
+
mallId,
|
|
629
|
+
name: cfg.name !== "" ? cfg.name : `\u793A\u4F8B\u5546\u573A\uFF08${mallId}\uFF09`,
|
|
630
|
+
address: cfg.address,
|
|
631
|
+
latitude: cfg.latitude,
|
|
632
|
+
longitude: cfg.longitude
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
};
|
|
512
637
|
|
|
513
638
|
// src/server/mock-enroll-route.ts
|
|
514
639
|
import { Router } from "express";
|
|
@@ -558,14 +683,14 @@ var MOCK_MEMBER_IMPL = "mock-member@1";
|
|
|
558
683
|
var MOCK_PARKING_IMPL = "mock-parking@1";
|
|
559
684
|
var MOCK_PROMOTION_IMPL = "mock-promotion@1";
|
|
560
685
|
var MOCK_SHOPPING_IMPL = "mock-shopping@1";
|
|
561
|
-
var memberFactory = () => new MockMemberServiceImpl();
|
|
562
|
-
var parkingFactory = (input) => new MockParkingServiceImpl(input.instanceId || "mall-demo");
|
|
563
|
-
var promotionFactory = () => new MockPromotionServiceImpl();
|
|
686
|
+
var memberFactory = (input) => new MockMemberServiceImpl({}, input.config);
|
|
687
|
+
var parkingFactory = (input) => new MockParkingServiceImpl(input.instanceId || "mall-demo", input.config);
|
|
688
|
+
var promotionFactory = (input) => new MockPromotionServiceImpl(input.config);
|
|
564
689
|
var shoppingFactory = (input) => {
|
|
565
690
|
if (input.service !== "shopping-guide.catalog" && input.service !== "shopping-guide.shops" && input.service !== "shopping-guide.categories" && input.service !== "shopping-guide.directory") {
|
|
566
691
|
throw new Error(`mock guide \u5DE5\u5382\u4E0D\u4F9B\u7ED9\u69FD\uFF1A${input.service}`);
|
|
567
692
|
}
|
|
568
|
-
return new MockShoppingServiceImpl();
|
|
693
|
+
return new MockShoppingServiceImpl(void 0, input.config);
|
|
569
694
|
};
|
|
570
695
|
var mallInfoFactory = (input) => mockMallInfoFactory(input);
|
|
571
696
|
function createMockAlipayLoginExchange(integrations) {
|
|
@@ -610,6 +735,9 @@ var serverModule = {
|
|
|
610
735
|
},
|
|
611
736
|
cards,
|
|
612
737
|
register(ctx) {
|
|
738
|
+
ctx.integrations?.declareProviderInstances?.("mock", [
|
|
739
|
+
{ id: "mall-demo", name: "\u661F\u6CB3 Mock \u5C0F\u9547", attributes: { miniAppId: "mock-demo-app" } }
|
|
740
|
+
]);
|
|
613
741
|
ctx.integrations?.registerAdapter(MOCK_PROVIDER, MOCK_MEMBER_IMPL, memberFactory);
|
|
614
742
|
ctx.integrations?.registerAdapter(MOCK_PROVIDER, MOCK_PARKING_IMPL, parkingFactory);
|
|
615
743
|
ctx.integrations?.registerAdapter(MOCK_PROVIDER, MOCK_PROMOTION_IMPL, promotionFactory);
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tbox.cn/app-provider-mock",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "商圈 Mock Provider 包(17 槽全 local:true:member×5 / parking×3 / promotion×4 / guide×4 + mall.info):模板缺省开箱演示——免传输/凭据/实例配置,裸跑即完整 mock
|
|
5
|
+
"description": "商圈 Mock Provider 包(17 槽全 local:true:member×5 / parking×3 / promotion×4 / guide×4 + mall.info):模板缺省开箱演示——免传输/凭据/实例配置,裸跑即完整 mock 演示;全槽 config schema(schemas/×5 域共享)+ 内存 config 消费(改配置重启即变)。",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@tbox.cn/app-sdk": "0.
|
|
18
|
-
"@tbox.cn/app-contracts": "0.
|
|
19
|
-
"@tbox.cn/app-contracts-mall": "0.
|
|
17
|
+
"@tbox.cn/app-sdk": "0.19.0",
|
|
18
|
+
"@tbox.cn/app-contracts": "0.10.0",
|
|
19
|
+
"@tbox.cn/app-contracts-mall": "0.10.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/express": "4.17.25",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"files": [
|
|
30
30
|
"dist",
|
|
31
|
+
"schemas",
|
|
31
32
|
"src",
|
|
32
33
|
"tests",
|
|
33
34
|
"tbox.module.json",
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "Mock 商场基础信息实例配置",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"properties": {
|
|
6
|
+
"name": { "type": "string", "title": "商场名称(空串 = 动态别名「示例商场(mallId)」回退)", "default": "" },
|
|
7
|
+
"address": { "type": "string", "title": "商场地址", "default": "示例商场地址(demo)" },
|
|
8
|
+
"latitude": { "type": "number", "minimum": -90, "maximum": 90, "title": "纬度(分式合法)", "default": 30 },
|
|
9
|
+
"longitude": { "type": "number", "minimum": -180, "maximum": 180, "title": "经度(分式合法)", "default": 120 }
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "Mock 会员域实例配置",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"properties": {
|
|
6
|
+
"memberName": { "type": "string", "title": "会员名称(资料标签展示)", "default": "示例会员" },
|
|
7
|
+
"cardName": { "type": "string", "title": "会员卡名称(卡面 + 积分标签)", "default": "示例会员卡" },
|
|
8
|
+
"levelName": { "type": "string", "title": "等级名称(空串 = 不展示等级区)", "default": "" },
|
|
9
|
+
"initialPoints": { "type": "integer", "minimum": 0, "title": "初始积分(入会后余额;流水金额同源派生)", "default": 300 },
|
|
10
|
+
"enrollUrl": { "type": "string", "title": "入会落地页深链(alipays:// 或 H5)", "default": "alipays://platformapi/startapp?appId=demo&page=v/index/index" },
|
|
11
|
+
"loginUrl": { "type": "string", "title": "登录页深链(alipays:// 或 H5)", "default": "alipays://platformapi/startapp?appId=demo&page=v/login/index" }
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "Mock 停车域实例配置",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"properties": {
|
|
6
|
+
"parkName": { "type": "string", "title": "停车场名称(信息卡标题 + 缴费记录)", "default": "示例停车场" },
|
|
7
|
+
"feeName": { "type": "string", "title": "费用项名称", "default": "停车费(3 小时)" },
|
|
8
|
+
"feeCents": { "type": "integer", "minimum": 0, "title": "停车费(分)", "default": 1500 },
|
|
9
|
+
"remainingSpaces": { "type": "integer", "minimum": 0, "title": "剩余车位", "default": 128 },
|
|
10
|
+
"firstHourRate": { "type": "string", "title": "首小时费率文案", "default": "免费" },
|
|
11
|
+
"memberBenefitEnabled": { "type": "boolean", "title": "会员折扣权益开关(关闭 = 权益列表少一项 + reprice 不变)", "default": true },
|
|
12
|
+
"memberBenefitDiscountCents": { "type": "integer", "minimum": 0, "title": "会员折扣权益预估减免(分)", "default": 75 },
|
|
13
|
+
"pointsBenefitEnabled": { "type": "boolean", "title": "积分抵扣权益开关", "default": true },
|
|
14
|
+
"invoiceEnabled": { "type": "boolean", "title": "开票入口开关(关闭 = 缴费卡无开票按钮)", "default": true },
|
|
15
|
+
"vehicleFloor": { "type": "string", "title": "寻车楼层", "default": "B1" },
|
|
16
|
+
"vehicleZone": { "type": "string", "title": "寻车区域", "default": "A 区" },
|
|
17
|
+
"vehicleSpaceNo": { "type": "string", "title": "寻车车位号", "default": "A-128" }
|
|
18
|
+
}
|
|
19
|
+
}
|