@tbox.cn/app-provider-mock 0.2.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.
@@ -0,0 +1,643 @@
1
+ // src/server/index.ts
2
+ import { readLoginMallId } from "@tbox.cn/app-contracts-mall";
3
+
4
+ // 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
+ ];
8
+ var mockMemberState = {
9
+ enrolled: /* @__PURE__ */ new Set(),
10
+ balances: /* @__PURE__ */ new Map(),
11
+ loggedOut: /* @__PURE__ */ new Set()
12
+ // 登出态:已入会但登录态失效(对齐真实档 silentLogin 非 200 语义)
13
+ };
14
+ var MOCK_SHELL_USER_ID = "demo-user";
15
+ function enrollMockMember(userId) {
16
+ mockMemberState.enrolled.add(userId);
17
+ mockMemberState.loggedOut.delete(userId);
18
+ if (!mockMemberState.balances.has(userId)) mockMemberState.balances.set(userId, 300);
19
+ }
20
+ function isMockMemberEnrolled(userId) {
21
+ return mockMemberState.enrolled.has(userId);
22
+ }
23
+ function logoutMockMember(userId) {
24
+ mockMemberState.loggedOut.add(userId);
25
+ }
26
+ function isMockMemberLoggedOut(userId) {
27
+ return mockMemberState.loggedOut.has(userId);
28
+ }
29
+ function __resetMockMemberState() {
30
+ mockMemberState.enrolled.clear();
31
+ mockMemberState.balances.clear();
32
+ mockMemberState.loggedOut.clear();
33
+ }
34
+ function isMockMemberActive(userId) {
35
+ return mockMemberState.enrolled.has(userId) && !mockMemberState.loggedOut.has(userId);
36
+ }
37
+ var MockMemberServiceImpl = class {
38
+ /**
39
+ * 开卡入会落地页深链(module-member MemberService.enrollUrl 接口面——CTA 卡深链为可选
40
+ * 透传素材,mock 出厂面恒配 demo 值)。demo 值厂商中立(appId=demo),点击在壳内
41
+ * navigateTo 失败即无害降级。
42
+ */
43
+ enrollUrl = "alipays://platformapi/startapp?appId=demo&page=v/index/index";
44
+ /** 登录页深链(同上,可选透传素材;供未登录引导卡跳转)。 */
45
+ loginUrl = "alipays://platformapi/startapp?appId=demo&page=v/login/index";
46
+ /** 默认访客(无种子):demo-user 初始 not_enrolled,经入会闭环翻为 active */
47
+ constructor(initial = {}) {
48
+ for (const [userId, balance] of Object.entries(initial)) {
49
+ mockMemberState.enrolled.add(userId);
50
+ mockMemberState.balances.set(userId, balance);
51
+ }
52
+ }
53
+ async getAccount(userId) {
54
+ const active = isMockMemberActive(userId);
55
+ return {
56
+ authState: "authenticated",
57
+ state: active ? "active" : "not_enrolled",
58
+ ...active ? { memberRef: `demo:member:${userId}` } : {},
59
+ observedAt: (/* @__PURE__ */ new Date()).toISOString()
60
+ };
61
+ }
62
+ async getProfile(_userId) {
63
+ return { labels: [{ labelRef: "demo:label:1", name: "\u793A\u4F8B\u4F1A\u5458" }], observedAt: (/* @__PURE__ */ new Date()).toISOString() };
64
+ }
65
+ async getCard(userId) {
66
+ if (!isMockMemberActive(userId)) return null;
67
+ return {
68
+ cardRef: `demo:card:${userId}`,
69
+ cardDisplay: "\u793A\u4F8B\u4F1A\u5458\u5361",
70
+ status: "active",
71
+ observedAt: (/* @__PURE__ */ new Date()).toISOString()
72
+ };
73
+ }
74
+ async queryPoints(userId) {
75
+ return { userId, balance: mockMemberState.balances.get(userId) ?? 0, membershipLabel: "\u793A\u4F8B\u4F1A\u5458\u5361" };
76
+ }
77
+ async getBenefits(_userId) {
78
+ return { availablePoints: 300, levelName: "\u793A\u4F8B\u7B49\u7EA7", observedAt: (/* @__PURE__ */ new Date()).toISOString() };
79
+ }
80
+ async listPointUsage(_userId, page, pageSize) {
81
+ const start = (page - 1) * pageSize;
82
+ return { items: DEMO_USAGE.slice(start, start + pageSize), total: DEMO_USAGE.length };
83
+ }
84
+ async queryDiscount() {
85
+ return { level: "silver", discount: 0.95 };
86
+ }
87
+ async deductPoints(userId, amount, _key) {
88
+ const balance = mockMemberState.balances.get(userId) ?? 0;
89
+ if (balance < amount) return false;
90
+ mockMemberState.balances.set(userId, balance - amount);
91
+ return true;
92
+ }
93
+ async refundPoints(userId, amount, _key) {
94
+ mockMemberState.balances.set(userId, (mockMemberState.balances.get(userId) ?? 0) + amount);
95
+ return true;
96
+ }
97
+ async awardPoints(userId, amount) {
98
+ mockMemberState.balances.set(userId, (mockMemberState.balances.get(userId) ?? 0) + amount);
99
+ return true;
100
+ }
101
+ async enroll(input) {
102
+ enrollMockMember(input.userId);
103
+ return { status: "active", memberRef: `demo:member:${input.userId}` };
104
+ }
105
+ async getEnrollmentStatus(userId, _attemptRef) {
106
+ return mockMemberState.enrolled.has(userId) ? { status: "active", memberRef: `demo:member:${userId}` } : { status: "failed", reason: "mock\uFF1A\u672A\u5165\u4F1A" };
107
+ }
108
+ /** 入会表单定义(模块 queryEnrollmentForm 工具消费;无参对齐 InMemory 先例) */
109
+ async getEnrollmentFormDefinition() {
110
+ return {
111
+ formVersion: "demo-v1",
112
+ fields: [
113
+ { fieldId: "mobile", label: "\u624B\u673A\u53F7", type: "tel", required: true, placeholder: "\u8BF7\u8F93\u5165\u624B\u673A\u53F7" },
114
+ { fieldId: "displayName", label: "\u59D3\u540D", type: "text", required: false }
115
+ ],
116
+ consents: [{ consentId: "privacy", version: "1", title: "\u9690\u79C1\u653F\u7B56", required: true }],
117
+ authorizationStatus: "required"
118
+ };
119
+ }
120
+ };
121
+
122
+ // src/server/parking.ts
123
+ 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
+ }
143
+ var MockParkingServiceImpl = class {
144
+ 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") {
157
+ this.mallId = mallId;
158
+ void this.mallId;
159
+ }
160
+ mallId;
161
+ async queryFee(q, input) {
162
+ const plateNo = input.plateNo ?? (input.vehicleRef ? this.vehicles.get(input.vehicleRef)?.plateNo : void 0) ?? "demo-plate";
163
+ const quote = demoQuote(q, plateNo);
164
+ return quote ? [quote] : [];
165
+ }
166
+ async listVehicles() {
167
+ const list = [...this.vehicles.values()];
168
+ return { bound: list.map((v, i) => ({ ...v, parked: i === 0 })) };
169
+ }
170
+ async listPaymentRecords(_q, page, pageSize) {
171
+ const start = (page - 1) * pageSize;
172
+ const items = this.records.slice(start, start + pageSize);
173
+ return { items, total: this.records.length, page, pageSize, hasMore: start + pageSize < this.records.length };
174
+ }
175
+ async getPaymentRecordDetail(_q, recordRef) {
176
+ const record = this.records.find((r) => r.recordRef === recordRef);
177
+ if (!record) throw new Error(`mock\uFF1A\u8BB0\u5F55\u4E0D\u5B58\u5728 ${recordRef}`);
178
+ return record;
179
+ }
180
+ async getInvoiceSettings() {
181
+ return { parkingInvoiceEnabled: true };
182
+ }
183
+ async listInvoiceableOrders(_q, page, pageSize) {
184
+ const all = this.records.filter((r) => r.orderCompleted === true);
185
+ const start = (page - 1) * pageSize;
186
+ return { items: all.slice(start, start + pageSize), total: all.length, page, pageSize, hasMore: start + pageSize < all.length };
187
+ }
188
+ async queryParkingInfo(_q, kind) {
189
+ return {
190
+ kind,
191
+ title: "\u793A\u4F8B\u505C\u8F66\u573A",
192
+ 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" }
196
+ ]
197
+ };
198
+ }
199
+ async queryApplicableBenefits(_q, _plateNo) {
200
+ return DEMO_BENEFITS;
201
+ }
202
+ async locateVehicle(_q, _plateNo) {
203
+ return { floorName: "B1", zoneName: "A \u533A", spaceNo: "A-128", walkingRoute: "\u7535\u68AF\u5DE6\u8F6C 50 \u7C73" };
204
+ }
205
+ // ===== 支付三步链(mock:reconcile 恒 PAID 收口) =====
206
+ async createOrder(_q, quoteRef) {
207
+ return { payOrderId: `demo:pay:${quoteRef}`, paymentRef: `demo:payment:${quoteRef}` };
208
+ }
209
+ async getOrderInfo(_q, _paymentRef) {
210
+ return { orderInfo: "demo-order-info", channel: "50", payableFeeCents: 1500 };
211
+ }
212
+ async getPaymentSign(_q, _paymentRef) {
213
+ return { orderInfo: "demo-order-info" };
214
+ }
215
+ async uploadResult() {
216
+ }
217
+ async reconcile(_q, _paymentRef) {
218
+ return "PAID";
219
+ }
220
+ 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);
223
+ return {
224
+ ...base,
225
+ quoteRef: `${quoteRef}#r${Date.now()}`,
226
+ discountCents: discount,
227
+ payableCents: Math.max(0, base.totalCents - discount)
228
+ };
229
+ }
230
+ async applyInvoice(_q, recordRef) {
231
+ if (!this.records.some((r) => r.recordRef === recordRef)) {
232
+ throw new Error(`mock\uFF1A\u5F00\u7968\u8BB0\u5F55\u4E0D\u5B58\u5728 ${recordRef}`);
233
+ }
234
+ }
235
+ // ===== 车辆管理 =====
236
+ async bindVehicle(_q, plateNo) {
237
+ const vehicle = { vehicleRef: `demo:vehicle:${plateNo}`, plateNo, boundAt: now() };
238
+ this.vehicles.set(vehicle.vehicleRef, vehicle);
239
+ return vehicle;
240
+ }
241
+ async unbindVehicle(_q, input) {
242
+ if (input.vehicleRef) this.vehicles.delete(input.vehicleRef);
243
+ else if (input.plateNo) {
244
+ for (const [ref, v] of this.vehicles) {
245
+ if (v.plateNo === input.plateNo) this.vehicles.delete(ref);
246
+ }
247
+ }
248
+ }
249
+ };
250
+
251
+ // src/server/promotion.ts
252
+ var now2 = () => (/* @__PURE__ */ new Date()).toISOString();
253
+ 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
+ ];
297
+ var MockPromotionServiceImpl = class {
298
+ async searchOffers(_q, input) {
299
+ 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;
301
+ return { items, hasMore: false };
302
+ }
303
+ async getOfferDetail(_q, input) {
304
+ if (input.offerRef) return DEMO_OFFERS.find((o) => o.offerRef === input.offerRef) ?? null;
305
+ 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;
308
+ return null;
309
+ }
310
+ if (input.keyword) return DEMO_OFFERS.find((o) => o.title.includes(input.keyword)) ?? null;
311
+ return null;
312
+ }
313
+ async recommendOffers() {
314
+ return DEMO_OFFERS;
315
+ }
316
+ async listEntitlements(_q, _input) {
317
+ return { items: DEMO_ENTITLEMENTS, hasMore: false };
318
+ }
319
+ async getEntitlementDetail(_q, entitlementRef) {
320
+ return DEMO_ENTITLEMENTS.find((e) => e.entitlementRef === entitlementRef) ?? null;
321
+ }
322
+ async listActivities() {
323
+ return { items: DEMO_ACTIVITIES, hasMore: false };
324
+ }
325
+ async getActivityDetail(_q, activityRef) {
326
+ const activity = DEMO_ACTIVITIES.find((a) => a.activityRef === activityRef);
327
+ if (!activity) return null;
328
+ return { ...activity, sessions: [], description: "mock \u6F14\u793A\u6D3B\u52A8\u8BE6\u60C5" };
329
+ }
330
+ async listActivityCategories() {
331
+ return [{ categoryId: "demo:category:1", name: "\u5168\u90E8" }];
332
+ }
333
+ async acquire(_q, input) {
334
+ return {
335
+ operationRef: `demo:operation:${input.offerRef}`,
336
+ status: "SUCCEEDED",
337
+ offerRef: input.offerRef,
338
+ codeMasked: "DEMO-****-0001",
339
+ observedAt: now2()
340
+ };
341
+ }
342
+ async getOperation(_q, operationRef) {
343
+ return { operationRef, status: "SUCCEEDED", offerRef: "demo:coupon:1001", observedAt: now2() };
344
+ }
345
+ };
346
+
347
+ // src/server/shopping.ts
348
+ var DEMO_BUILDINGS = [
349
+ {
350
+ buildingName: "A \u680B",
351
+ floors: [
352
+ {
353
+ floorName: "B1",
354
+ shops: [
355
+ { merchantId: "demo-shop-1001", name: "\u6C38\u8F89\u8D85\u5E02", businessCategories: ["\u8D85\u5E02"], unitName: "B1-01", status: { code: "enabled", label: "\u8425\u4E1A\u4E2D" } }
356
+ ]
357
+ },
358
+ {
359
+ floorName: "1 \u697C",
360
+ shops: [
361
+ { merchantId: "demo-shop-1002", name: "\u661F\u5DF4\u514B", businessCategories: ["\u9910\u996E"], unitName: "101", status: { code: "enabled", label: "\u8425\u4E1A\u4E2D" } },
362
+ { merchantId: "demo-shop-1003", name: "\u5468\u5927\u798F", businessCategories: ["\u73E0\u5B9D"], unitName: "102", status: { code: "enabled", label: "\u8425\u4E1A\u4E2D" } },
363
+ { merchantId: "demo-shop-1004", name: "\u5C48\u81E3\u6C0F", businessCategories: ["\u7F8E\u5986"], unitName: "103", status: { code: "enabled", label: "\u8425\u4E1A\u4E2D" } }
364
+ ]
365
+ },
366
+ {
367
+ floorName: "2 \u697C",
368
+ shops: [
369
+ { merchantId: "demo-shop-1005", name: "\u534E\u4E3A", businessCategories: ["\u6570\u7801"], unitName: "201", status: { code: "enabled", label: "\u8425\u4E1A\u4E2D" } },
370
+ { merchantId: "demo-shop-1006", name: "\u5C0F\u7C73", businessCategories: ["\u6570\u7801"], unitName: "202", status: { code: "enabled", label: "\u8425\u4E1A\u4E2D" } }
371
+ ]
372
+ },
373
+ {
374
+ floorName: "3 \u697C",
375
+ shops: [
376
+ { merchantId: "demo-shop-1007", name: "NIKE \u4E13\u5356\u5E97", brand: "NIKE", businessCategories: ["\u8FD0\u52A8\u670D\u9970"], unitName: "301", status: { code: "enabled", label: "\u8425\u4E1A\u4E2D" } },
377
+ { merchantId: "demo-shop-1008", name: "NIKE \u8DD1\u6B65\u4F53\u9A8C\u5E97", brand: "NIKE", businessCategories: ["\u8FD0\u52A8\u670D\u9970"], unitName: "302", status: { code: "enabled", label: "\u8425\u4E1A\u4E2D" } },
378
+ { merchantId: "demo-shop-1009", name: "\u5B89\u8E0F", businessCategories: ["\u8FD0\u52A8\u670D\u9970"], unitName: "303", status: { code: "enabled", label: "\u8425\u4E1A\u4E2D" } }
379
+ ]
380
+ },
381
+ {
382
+ floorName: "4 \u697C",
383
+ shops: [
384
+ { merchantId: "demo-shop-1010", name: "\u6D77\u5E95\u635E", businessCategories: ["\u9910\u996E"], unitName: "401", status: { code: "enabled", label: "\u8425\u4E1A\u4E2D" } },
385
+ { merchantId: "demo-shop-1011", name: "\u5916\u5A46\u5BB6", businessCategories: ["\u9910\u996E"], unitName: "402", status: { code: "enabled", label: "\u8425\u4E1A\u4E2D" } },
386
+ { merchantId: "demo-shop-1012", name: "\u897F\u8D1D\u839C\u9762\u6751", businessCategories: ["\u9910\u996E"], unitName: "403", status: { code: "disabled", label: "\u4F11\u606F\u4E2D" } }
387
+ ]
388
+ }
389
+ ]
390
+ }
391
+ ];
392
+ function flattenShops(buildings) {
393
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
394
+ return buildings.flatMap(
395
+ (b) => b.floors.flatMap((f) => f.shops.map(
396
+ (seed) => ({
397
+ merchantId: seed.merchantId,
398
+ name: seed.name,
399
+ ...seed.brand ? { brand: seed.brand } : {},
400
+ businessCategories: [...seed.businessCategories],
401
+ location: {
402
+ displayText: [b.buildingName, f.floorName, seed.unitName].filter(Boolean).join(" "),
403
+ buildingName: b.buildingName,
404
+ floorName: f.floorName,
405
+ unitName: seed.unitName
406
+ },
407
+ status: { code: seed.status.code, label: seed.status.label, observedAt: ts },
408
+ coverUrls: [],
409
+ services: [],
410
+ promotionFacts: [],
411
+ observedAt: ts
412
+ })
413
+ ))
414
+ );
415
+ }
416
+ function aggregateCategories(shops) {
417
+ const counts = /* @__PURE__ */ new Map();
418
+ for (const s of shops) {
419
+ for (const c of s.businessCategories) counts.set(c, (counts.get(c) ?? 0) + 1);
420
+ }
421
+ return [...counts.entries()].map(([name, merchantCount]) => ({ code: name, name, merchantCount }));
422
+ }
423
+ function toFloorRecord(floor) {
424
+ const cats = [...new Set(floor.shops.flatMap((s) => s.businessCategories))];
425
+ return { floorCode: floor.floorName, floorName: floor.floorName, businessCategories: cats, merchantCount: floor.shops.length };
426
+ }
427
+ var MockShoppingServiceImpl = class {
428
+ buildings;
429
+ shops;
430
+ categories;
431
+ constructor(seed = DEMO_BUILDINGS) {
432
+ this.buildings = seed;
433
+ this.shops = flattenShops(seed);
434
+ this.categories = aggregateCategories(this.shops);
435
+ }
436
+ // ===== ShopDirectoryQueryPort =====
437
+ async searchShops(criteria) {
438
+ const kws = (criteria.keywords ?? (criteria.keyword?.trim() ? [criteria.keyword.trim()] : [])).map((k) => k.trim().toLowerCase()).filter(Boolean);
439
+ const cat = criteria.businessCategory?.trim();
440
+ const floor = criteria.floorCode?.trim();
441
+ const bld = criteria.buildingCode?.trim();
442
+ const predicates = [];
443
+ if (kws.length > 0) {
444
+ predicates.push(
445
+ (s) => kws.some(
446
+ (k) => s.name.toLowerCase().includes(k) || (s.brand ?? "").toLowerCase().includes(k) || s.businessCategories.some((c) => c.toLowerCase().includes(k))
447
+ )
448
+ );
449
+ }
450
+ if (cat) predicates.push((s) => s.businessCategories.includes(cat));
451
+ if (floor) predicates.push((s) => s.location.floorName === floor);
452
+ if (bld) predicates.push((s) => s.location.buildingName === bld);
453
+ const matched = predicates.length === 0 ? this.shops : this.shops.filter((s) => predicates.every((p) => p(s)));
454
+ const start = (criteria.page - 1) * criteria.pageSize;
455
+ return {
456
+ items: matched.slice(start, start + criteria.pageSize),
457
+ page: criteria.page,
458
+ pageSize: criteria.pageSize,
459
+ total: matched.length,
460
+ hasMore: start + criteria.pageSize < matched.length
461
+ };
462
+ }
463
+ async getShop(shopId, _ctx) {
464
+ const id = shopId.trim();
465
+ return this.shops.find((s) => s.merchantId === id) ?? null;
466
+ }
467
+ // ===== CommercialCategoryQueryPort =====
468
+ async listCategories(_ctx) {
469
+ return this.categories;
470
+ }
471
+ // ===== MallDirectoryQueryPort =====
472
+ async getDirectory(ctx) {
473
+ return {
474
+ mallId: ctx.scope.mallId ?? "mall-demo",
475
+ mallName: "\u793A\u4F8B\u5546\u573A",
476
+ buildings: this.buildings.map((b) => ({
477
+ buildingCode: b.buildingName,
478
+ buildingName: b.buildingName,
479
+ floors: b.floors.map(toFloorRecord)
480
+ })),
481
+ observedAt: (/* @__PURE__ */ new Date()).toISOString()
482
+ };
483
+ }
484
+ // ===== CatalogQueryPort(空实现:演示面不覆盖商品目录,对齐真实档 C 端) =====
485
+ async searchProducts(criteria) {
486
+ return { items: [], page: criteria.page, pageSize: criteria.pageSize, total: 0, hasMore: false };
487
+ }
488
+ async getProduct(_productId, _ctx) {
489
+ return null;
490
+ }
491
+ async getFacets(_criteria) {
492
+ return { brands: [], categories: [], priceRanges: [], floors: [] };
493
+ }
494
+ };
495
+
496
+ // src/server/mall-info.ts
497
+ var MOCK_MALL_INFO_PROVIDER = "mock";
498
+ var MOCK_MALL_INFO_IMPL = "mock-mall-info@1";
499
+ 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
+ });
512
+
513
+ // src/server/mock-enroll-route.ts
514
+ import { Router } from "express";
515
+ import { getRequestContext } from "@tbox.cn/app-sdk/server";
516
+ function resolveMockEnroll(input) {
517
+ if ((input.nodeEnv ?? process.env.NODE_ENV) === "production") {
518
+ return { status: 503, body: { error: "mock-only" } };
519
+ }
520
+ if (!input.identity || input.identity.source === "anonymous") {
521
+ return { status: 401, body: { error: "unauthorized" } };
522
+ }
523
+ const body = input.body ?? {};
524
+ const userId = typeof body.userId === "string" && body.userId.trim() ? body.userId.trim() : MOCK_SHELL_USER_ID;
525
+ enrollMockMember(userId);
526
+ return { status: 200, body: { state: "active", memberRef: `demo:member:${userId}` } };
527
+ }
528
+ function resolveMockLogout(input) {
529
+ if ((input.nodeEnv ?? process.env.NODE_ENV) === "production") {
530
+ return { status: 503, body: { error: "mock-only" } };
531
+ }
532
+ if (!input.identity || input.identity.source === "anonymous") {
533
+ return { status: 401, body: { error: "unauthorized" } };
534
+ }
535
+ const body = input.body ?? {};
536
+ const userId = typeof body.userId === "string" && body.userId.trim() ? body.userId.trim() : MOCK_SHELL_USER_ID;
537
+ logoutMockMember(userId);
538
+ return { status: 200, body: { state: "logged_out" } };
539
+ }
540
+ function createMockEnrollRouter() {
541
+ const router = Router();
542
+ router.post("/demo/member-enroll", (req, res) => {
543
+ const identity = getRequestContext(req)?.identity;
544
+ const outcome = resolveMockEnroll({ body: req.body, identity });
545
+ res.status(outcome.status).json(outcome.body);
546
+ });
547
+ router.post("/demo/member-logout", (req, res) => {
548
+ const identity = getRequestContext(req)?.identity;
549
+ const outcome = resolveMockLogout({ body: req.body, identity });
550
+ res.status(outcome.status).json(outcome.body);
551
+ });
552
+ return router;
553
+ }
554
+
555
+ // src/server/index.ts
556
+ var MOCK_PROVIDER = "mock";
557
+ var MOCK_MEMBER_IMPL = "mock-member@1";
558
+ var MOCK_PARKING_IMPL = "mock-parking@1";
559
+ var MOCK_PROMOTION_IMPL = "mock-promotion@1";
560
+ 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();
564
+ var shoppingFactory = (input) => {
565
+ if (input.service !== "shopping-guide.catalog" && input.service !== "shopping-guide.shops" && input.service !== "shopping-guide.categories" && input.service !== "shopping-guide.directory") {
566
+ throw new Error(`mock guide \u5DE5\u5382\u4E0D\u4F9B\u7ED9\u69FD\uFF1A${input.service}`);
567
+ }
568
+ return new MockShoppingServiceImpl();
569
+ };
570
+ var mallInfoFactory = (input) => mockMallInfoFactory(input);
571
+ function createMockAlipayLoginExchange(integrations) {
572
+ return {
573
+ provider: "alipay",
574
+ matches(context) {
575
+ const mallId = readLoginMallId(context);
576
+ if (!mallId) return true;
577
+ if (!integrations) return false;
578
+ return integrations.hasProviderBinding("member.account", { mallId }, MOCK_PROVIDER, MOCK_MEMBER_IMPL);
579
+ },
580
+ async exchangeByCode(_code, _platform, context) {
581
+ if (!readLoginMallId(context)) {
582
+ throw new Error("AUTH_NOT_CONFIGURED: \u672A\u9009\u62E9\u5546\u573A\uFF08\u767B\u5F55 context \u65E0 mallId\uFF09");
583
+ }
584
+ const userId = MOCK_SHELL_USER_ID;
585
+ if (isMockMemberEnrolled(userId) && !isMockMemberLoggedOut(userId)) {
586
+ return {
587
+ identity: { userId, source: "external" },
588
+ credentials: {
589
+ token: `demo:token:${userId}`,
590
+ provider: "mock",
591
+ attributes: { memberId: userId }
592
+ }
593
+ };
594
+ }
595
+ return {
596
+ identity: { userId, source: "external" },
597
+ credentials: {
598
+ token: "",
599
+ provider: "mock",
600
+ attributes: { openid: `demo:openid:${userId}` }
601
+ }
602
+ };
603
+ }
604
+ };
605
+ }
606
+ var cards = {};
607
+ var serverModule = {
608
+ declaration: {
609
+ moduleId: "provider-mock"
610
+ },
611
+ cards,
612
+ register(ctx) {
613
+ ctx.integrations?.registerAdapter(MOCK_PROVIDER, MOCK_MEMBER_IMPL, memberFactory);
614
+ ctx.integrations?.registerAdapter(MOCK_PROVIDER, MOCK_PARKING_IMPL, parkingFactory);
615
+ ctx.integrations?.registerAdapter(MOCK_PROVIDER, MOCK_PROMOTION_IMPL, promotionFactory);
616
+ ctx.integrations?.registerAdapter(MOCK_PROVIDER, MOCK_SHOPPING_IMPL, shoppingFactory);
617
+ ctx.integrations?.registerAdapter(MOCK_MALL_INFO_PROVIDER, MOCK_MALL_INFO_IMPL, mallInfoFactory);
618
+ ctx.authExchanges.register(createMockAlipayLoginExchange(ctx.integrations));
619
+ ctx.routes.register("provider-mock", createMockEnrollRouter());
620
+ }
621
+ };
622
+ export {
623
+ MOCK_MALL_INFO_IMPL,
624
+ MOCK_MALL_INFO_PROVIDER,
625
+ MOCK_MEMBER_IMPL,
626
+ MOCK_PARKING_IMPL,
627
+ MOCK_PROMOTION_IMPL,
628
+ MOCK_PROVIDER,
629
+ MOCK_SHELL_USER_ID,
630
+ MOCK_SHOPPING_IMPL,
631
+ MockMemberServiceImpl,
632
+ MockParkingServiceImpl,
633
+ MockPromotionServiceImpl,
634
+ MockShoppingServiceImpl,
635
+ __resetMockMemberState,
636
+ createMockEnrollRouter,
637
+ enrollMockMember,
638
+ isMockMemberEnrolled,
639
+ isMockMemberLoggedOut,
640
+ logoutMockMember,
641
+ mockMallInfoFactory,
642
+ serverModule
643
+ };
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@tbox.cn/app-provider-mock",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "description": "商圈 Mock Provider 包(17 槽全 local:true:member×5 / parking×3 / promotion×4 / guide×4 + mall.info):模板缺省开箱演示——免传输/凭据/实例配置,裸跑即完整 mock 演示。",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js"
10
+ },
11
+ "./server": {
12
+ "types": "./dist/server/index.d.ts",
13
+ "import": "./dist/server/index.js"
14
+ }
15
+ },
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"
20
+ },
21
+ "devDependencies": {
22
+ "@types/express": "4.17.25",
23
+ "@types/node": "24.13.3",
24
+ "express": "4.22.2",
25
+ "typescript": "5.9.3",
26
+ "vitest": "4.1.10",
27
+ "tsup": "8.5.1"
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "src",
32
+ "tests",
33
+ "tbox.module.json",
34
+ "tsconfig.json",
35
+ "tsup.config.ts",
36
+ "README.md",
37
+ "LICENSE"
38
+ ],
39
+ "engines": {
40
+ "node": ">=20.0.0"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "license": "MIT",
46
+ "scripts": {
47
+ "typecheck": "tsc --noEmit",
48
+ "test": "vitest run",
49
+ "build": "tsup"
50
+ }
51
+ }