@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.
@@ -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 DEMO_USAGE = [
6
- { usageRecordRef: "demo:usage:1", title: "\u5F00\u5361\u793C", amount: "+300", usedAt: (/* @__PURE__ */ new Date()).toISOString(), description: "mock \u6F14\u793A\u6D41\u6C34" }
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 = "alipays://platformapi/startapp?appId=demo&page=v/index/index";
61
+ enrollUrl;
44
62
  /** 登录页深链(同上,可选透传素材;供未登录引导卡跳转)。 */
45
- loginUrl = "alipays://platformapi/startapp?appId=demo&page=v/login/index";
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: "\u793A\u4F8B\u4F1A\u5458" }], observedAt: (/* @__PURE__ */ new Date()).toISOString() };
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: "\u793A\u4F8B\u4F1A\u5458\u5361",
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: mockMemberState.balances.get(userId) ?? 0, membershipLabel: "\u793A\u4F8B\u4F1A\u5458\u5361" };
104
+ return { userId, balance: this.currentBalanceOf(userId), membershipLabel: this.cfg.cardName };
76
105
  }
77
- async getBenefits(_userId) {
78
- return { availablePoints: 300, levelName: "\u793A\u4F8B\u7B49\u7EA7", observedAt: (/* @__PURE__ */ new Date()).toISOString() };
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: DEMO_USAGE.slice(start, start + pageSize), total: DEMO_USAGE.length };
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 = mockMemberState.balances.get(userId) ?? 0;
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, (mockMemberState.balances.get(userId) ?? 0) + amount);
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, (mockMemberState.balances.get(userId) ?? 0) + amount);
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 DEMO_BENEFITS = [
125
- { benefitRef: "demo:benefit:member-discount", title: "\u4F1A\u5458 95 \u6298", description: "mock \u6F14\u793A\u6743\u76CA", kind: "discount", autoSelected: true, estimatedDiscountCents: 75 },
126
- { 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 }
127
- ];
128
- function demoQuote(q, plateNo) {
129
- return {
130
- quoteRef: `demo:quote:${plateNo}`,
131
- plateNo,
132
- mallId: q.scope.mallId ?? "demo",
133
- entryAt: now(),
134
- feeItems: [{ name: "\u505C\u8F66\u8D39\uFF083 \u5C0F\u65F6\uFF09", amountCents: 1500 }],
135
- totalCents: 1500,
136
- discountCents: 0,
137
- payableCents: 1500,
138
- appliedBenefits: [],
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
- recordRef: "demo:record:1",
148
- plateNo: "demo-plate",
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 = demoQuote(q, plateNo);
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: true };
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: "\u793A\u4F8B\u505C\u8F66\u573A",
256
+ title: this.cfg.parkName,
192
257
  entries: [
193
- { label: "\u5269\u4F59\u8F66\u4F4D", value: "128" },
194
- { label: "\u9996\u5C0F\u65F6\u8D39\u7387", value: "\u514D\u8D39" },
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 DEMO_BENEFITS;
265
+ return this.benefits();
201
266
  }
202
267
  async locateVehicle(_q, _plateNo) {
203
- return { floorName: "B1", zoneName: "A \u533A", spaceNo: "A-128", walkingRoute: "\u7535\u68AF\u5DE6\u8F6C 50 \u7C73" };
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: 1500 };
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 = demoQuote(q, "demo-plate");
222
- const discount = DEMO_BENEFITS.filter((b) => benefitRefs.includes(b.benefitRef)).reduce((sum, b) => sum + (b.estimatedDiscountCents ?? 0), 0);
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 DEMO_OFFERS = [
255
- {
256
- offerRef: "demo:coupon:1001",
257
- offerType: "COUPON",
258
- title: "\u6EE1 100 \u51CF 20 \u5143\u5238",
259
- subtitle: "\u5168\u573A\u901A\u7528\uFF08mock \u6F14\u793A\uFF09",
260
- tags: ["\u901A\u7528"],
261
- validity: { start: now2(), end: inDays(30) },
262
- facets: [{ kind: "COUPON", discountType: "AMOUNT", value: 2e3, thresholdCents: 1e4 }],
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 ? DEMO_OFFERS.filter((o) => o.title.includes(keyword) || o.tags.some((t) => t.includes(keyword))) : DEMO_OFFERS;
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 DEMO_OFFERS.find((o) => o.offerRef === input.offerRef) ?? null;
387
+ if (input.offerRef) return this.offers.find((o) => o.offerRef === input.offerRef) ?? null;
305
388
  if (input.entitlementRef) {
306
- const ent = DEMO_ENTITLEMENTS.find((e) => e.entitlementRef === input.entitlementRef);
307
- if (ent?.offerRef) return DEMO_OFFERS.find((o) => o.offerRef === ent.offerRef) ?? null;
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 DEMO_OFFERS.find((o) => o.title.includes(input.keyword)) ?? null;
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 DEMO_OFFERS;
397
+ return this.offers;
315
398
  }
316
399
  async listEntitlements(_q, _input) {
317
- return { items: DEMO_ENTITLEMENTS, hasMore: false };
400
+ return { items: this.entitlements, hasMore: false };
318
401
  }
319
402
  async getEntitlementDetail(_q, entitlementRef) {
320
- return DEMO_ENTITLEMENTS.find((e) => e.entitlementRef === entitlementRef) ?? null;
403
+ return this.entitlements.find((e) => e.entitlementRef === entitlementRef) ?? null;
321
404
  }
322
405
  async listActivities() {
323
- return { items: DEMO_ACTIVITIES, hasMore: false };
406
+ return { items: this.activities, hasMore: false };
324
407
  }
325
408
  async getActivityDetail(_q, activityRef) {
326
- const activity = DEMO_ACTIVITIES.find((a) => a.activityRef === activityRef);
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.buildings = seed;
433
- this.shops = flattenShops(seed);
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
- async getMallInfo(mallId) {
502
- if (!MALL_ID_RE.test(mallId)) return void 0;
503
- return {
504
- mallId,
505
- name: `\u793A\u4F8B\u5546\u573A\uFF08${mallId}\uFF09`,
506
- address: "\u793A\u4F8B\u5546\u573A\u5730\u5740\uFF08demo\uFF09",
507
- latitude: 30,
508
- longitude: 120
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.2.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.17.0",
18
- "@tbox.cn/app-contracts": "0.9.0",
19
- "@tbox.cn/app-contracts-mall": "0.9.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
+ }