@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.
@@ -0,0 +1,13 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "Mock 营销域实例配置",
4
+ "type": "object",
5
+ "properties": {
6
+ "couponTitle": { "type": "string", "title": "券标题(列表/详情/资产同源)", "default": "满 100 减 20 元券" },
7
+ "couponValueCents": { "type": "integer", "minimum": 0, "title": "券面额(分)", "default": 2000 },
8
+ "couponThresholdCents": { "type": "integer", "minimum": 0, "title": "券门槛(分)", "default": 10000 },
9
+ "couponValidityDays": { "type": "integer", "minimum": 1, "maximum": 365, "title": "券有效期(天)", "default": 30 },
10
+ "grouponEnabled": { "type": "boolean", "title": "团购演示项开关(关闭 = 优惠列表仅券)", "default": true },
11
+ "activityTitle": { "type": "string", "title": "活动标题", "default": "示例商场周年庆" }
12
+ }
13
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "Mock 导购域实例配置",
4
+ "type": "object",
5
+ "properties": {
6
+ "extraShopName": { "type": "string", "title": "追加门店名(空串 = 不追加)", "default": "" },
7
+ "extraShopCategory": { "type": "string", "title": "追加门店类目", "default": "餐饮" },
8
+ "extraShopFloor": { "type": "string", "enum": ["B1", "1 楼", "2 楼", "3 楼", "4 楼"], "title": "追加门店楼层", "default": "3 楼" },
9
+ "extraShopUnit": { "type": "string", "title": "追加门店铺位号", "default": "304" },
10
+ "extraShopOpen": { "type": "boolean", "title": "追加门店营业状态", "default": true }
11
+ }
12
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Mock 实例配置快照(fail-soft 单源):provider-mock 全部 config 消费的唯一读取面。
3
+ * - 只透出 defaults 键(构造性防死字段);defaults 键 ≡ 被消费键;
4
+ * - 非法回退缺省:类型族不符 / NaN / intKeys 键非整数 / 空串(统一「空串 = 缺省」语义);
5
+ * - intKeys 显式列举而非按 default 值推断(JS 中 30.0 === 30,推断式必误拒分式经纬度);
6
+ * - schema 是表单引导非门禁——守卫在消费侧(五层浅合并由平台侧完成,本层零合并逻辑)。
7
+ * 双锁测试(tests/config-schema.test.ts)保证 schema.properties ≡ DEFAULTS 键集与 default 值。
8
+ */
9
+ export type CfgDefaults = Readonly<Record<string, string | number | boolean>>;
10
+
11
+ export function resolveConfig<T extends CfgDefaults>(
12
+ raw: unknown,
13
+ defaults: T,
14
+ intKeys: readonly string[] = [],
15
+ ): T {
16
+ const src = raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : {};
17
+ const out: Record<string, string | number | boolean> = {};
18
+ for (const [key, fallback] of Object.entries(defaults)) {
19
+ const value = src[key];
20
+ const ok = typeof value === typeof fallback
21
+ && (typeof value !== 'number' || (Number.isFinite(value) && (!intKeys.includes(key) || Number.isInteger(value))))
22
+ && (typeof value !== 'string' || value !== '');
23
+ out[key] = ok ? (value as string | number | boolean) : fallback;
24
+ }
25
+ return out as T;
26
+ }
@@ -12,6 +12,13 @@
12
12
  * (catalog 空 + shop/category/directory 纯 mock 数据);assistance/navigation 本地知识库域
13
13
  * 归模块本地 adapter,不属 provider 供给面。
14
14
  *
15
+ * config 消费:五域工厂透传 input.config → 各 impl 构造期 fail-soft 快照
16
+ * (src/server/config.ts;键集/缺省双锁 tests/config-schema.test.ts);零配置 = 零行为变化。
17
+ *
18
+ * 隐式默认单实例(零拓扑部署兜底):register 期经 ctx.integrations.declareProviderInstances
19
+ * 声明 mall-demo——显式 instances/defaultInstanceId 恒赢(融合规则见 contracts-mall
20
+ * mallControlPlaneOf);值与 .real-env mock 档演示语义一致(星河 Mock 小镇 / mock-demo-app)。
21
+ *
15
22
  * 入会体验闭环(C5)追加两件:
16
23
  * - scoped mock 换码器(ctx.authExchanges):固定 demo-user、GUEST/MEMBER 双形态对齐
17
24
  * 真实厂商换码协议(潜客 token=''+openid ↔ 会员 token+memberId)——mock 档冷登走与
@@ -49,9 +56,9 @@ export { MockShoppingServiceImpl } from './shopping';
49
56
  export { MOCK_MALL_INFO_PROVIDER, MOCK_MALL_INFO_IMPL, mockMallInfoFactory } from './mall-info';
50
57
  export { createMockEnrollRouter } from './mock-enroll-route';
51
58
 
52
- const memberFactory: ServiceAdapterFactory = () => new MockMemberServiceImpl();
53
- const parkingFactory: ServiceAdapterFactory = (input) => new MockParkingServiceImpl(input.instanceId || 'mall-demo');
54
- const promotionFactory: ServiceAdapterFactory = () => new MockPromotionServiceImpl();
59
+ const memberFactory: ServiceAdapterFactory = (input) => new MockMemberServiceImpl({}, input.config);
60
+ const parkingFactory: ServiceAdapterFactory = (input) => new MockParkingServiceImpl(input.instanceId || 'mall-demo', input.config);
61
+ const promotionFactory: ServiceAdapterFactory = (input) => new MockPromotionServiceImpl(input.config);
55
62
  /** shopping-guide:四槽分支(017 槽位原子化)——MockShoppingServiceImpl 实现全部四 Port,
56
63
  * 结构类型满足任一槽产物(catalog 槽 = mock 空商品数据,对齐真实档无商品端点形态)。 */
57
64
  const shoppingFactory: ServiceAdapterFactory = (input) => {
@@ -63,7 +70,7 @@ const shoppingFactory: ServiceAdapterFactory = (input) => {
63
70
  ) {
64
71
  throw new Error(`mock guide 工厂不供给槽:${input.service}`);
65
72
  }
66
- return new MockShoppingServiceImpl();
73
+ return new MockShoppingServiceImpl(undefined, input.config);
67
74
  };
68
75
  /** mall.info:动态商场演示源(任意合法 mallId → 确定性别名;对齐真实厂商 mall-info adapter 通用形态) */
69
76
  const mallInfoFactory: ServiceAdapterFactory = (input) => mockMallInfoFactory(input);
@@ -134,6 +141,11 @@ export const serverModule = {
134
141
  },
135
142
  cards,
136
143
  register(ctx: ServerContext): void {
144
+ // 隐式默认单实例声明(零拓扑部署兜底——显式 instances/defaultInstanceId 恒赢;
145
+ // 融合规则见 contracts-mall mallControlPlaneOf。?. 兼容旧 SDK:缺 API 时 no-op 降级)
146
+ ctx.integrations?.declareProviderInstances?.('mock', [
147
+ { id: 'mall-demo', name: '星河 Mock 小镇', attributes: { miniAppId: 'mock-demo-app' } },
148
+ ]);
137
149
  // adapter 注册(v2 双键:provider 厂商名 + implementation)
138
150
  ctx.integrations?.registerAdapter(MOCK_PROVIDER, MOCK_MEMBER_IMPL, memberFactory);
139
151
  ctx.integrations?.registerAdapter(MOCK_PROVIDER, MOCK_PARKING_IMPL, parkingFactory);
@@ -9,26 +9,41 @@
9
9
  * - mallId 形状守卫(/^[\w-]{1,32}$/):畸形 ID 直接返回 undefined(S4 DoS 防线——
10
10
  * 记录≠校验教训:请求形状断言在 provider adapter 单测锁死)
11
11
  * - fail-soft 契约恒满足:本实现无 IO,不可能失败——契约面与真实档保持一致
12
+ * - config 消费:MALL_INFO_CFG_DEFAULTS 为契约单源(name 空串 = 动态别名回退;
13
+ * 双锁测试 tests/config-schema.test.ts)
12
14
  */
15
+ import { resolveConfig, type CfgDefaults } from './config';
13
16
  import type { MallInfo, MallInfoPort } from '@tbox.cn/app-contracts-mall';
14
17
  import type { ServiceAdapterFactory } from '@tbox.cn/app-sdk/server';
15
18
 
16
19
  export const MOCK_MALL_INFO_PROVIDER = 'mock';
17
20
  export const MOCK_MALL_INFO_IMPL = 'mock-mall-info@1';
18
21
 
22
+ /** config 契约单源(键集/缺省/整数约束;lat/lon 分式合法——不进 intKeys) */
23
+ export const MALL_INFO_CFG_DEFAULTS = {
24
+ name: '', // '' 哨兵 = 动态别名回退
25
+ address: '示例商场地址(demo)',
26
+ latitude: 30.0,
27
+ longitude: 120.0,
28
+ } as const satisfies CfgDefaults;
29
+ export const MALL_INFO_CFG_INT_KEYS: readonly string[] = [];
30
+
19
31
  /** mallId 形状守卫(防注入类字符;畸形 → undefined——与真实厂商 adapter 同规则) */
20
32
  const MALL_ID_RE = /^[\w-]{1,32}$/;
21
33
 
22
34
  /** mock mall.info adapter 工厂(对齐真实厂商 adapter 工厂形态) */
23
- export const mockMallInfoFactory: ServiceAdapterFactory = (): MallInfoPort => ({
24
- async getMallInfo(mallId: string): Promise<MallInfo | undefined> {
25
- if (!MALL_ID_RE.test(mallId)) return undefined;
26
- return {
27
- mallId,
28
- name: `示例商场(${mallId})`,
29
- address: '示例商场地址(demo)',
30
- latitude: 30.0,
31
- longitude: 120.0,
32
- };
33
- },
34
- });
35
+ export const mockMallInfoFactory: ServiceAdapterFactory = (input): MallInfoPort => {
36
+ const cfg = resolveConfig(input.config, MALL_INFO_CFG_DEFAULTS, MALL_INFO_CFG_INT_KEYS);
37
+ return {
38
+ async getMallInfo(mallId: string): Promise<MallInfo | undefined> {
39
+ if (!MALL_ID_RE.test(mallId)) return undefined;
40
+ return {
41
+ mallId,
42
+ name: cfg.name !== '' ? cfg.name : `示例商场(${mallId})`,
43
+ address: cfg.address,
44
+ latitude: cfg.latitude,
45
+ longitude: cfg.longitude,
46
+ };
47
+ },
48
+ };
49
+ };
@@ -2,7 +2,9 @@
2
2
  * Mock member 实现(provider-mock 自持——R3-1 裁决:provider 不得依赖业务模块,实现自持
3
3
  * 而非包装 module-member InMemory)。演示面最小集:开户/卡面/积分/流水/入会闭环
4
4
  * (demo ref 恒 `demo:` 前缀,厂商中立)。
5
+ * config 消费:MEMBER_CFG_DEFAULTS 为契约单源(fail-soft 快照;双锁测试 tests/config-schema.test.ts)。
5
6
  */
7
+ import { resolveConfig, type CfgDefaults } from './config';
6
8
  import type {
7
9
  MemberAccountPort,
8
10
  MemberAccountSnapshot,
@@ -27,9 +29,16 @@ export type MockMemberService = MemberAccountPort &
27
29
  MemberBenefitsPort &
28
30
  MemberEnrollmentPort;
29
31
 
30
- const DEMO_USAGE: PointUsageRecord[] = [
31
- { usageRecordRef: 'demo:usage:1', title: '开卡礼', amount: '+300', usedAt: new Date().toISOString(), description: 'mock 演示流水' },
32
- ];
32
+ /** config 契约单源(键集/缺省/整数约束;tests/config-schema.test.ts 双锁) */
33
+ export const MEMBER_CFG_DEFAULTS = {
34
+ memberName: '示例会员',
35
+ cardName: '示例会员卡',
36
+ levelName: '', // '' 哨兵 = 不展示等级区(MemberCardSnapshot.levelName 可选位)
37
+ initialPoints: 300,
38
+ enrollUrl: 'alipays://platformapi/startapp?appId=demo&page=v/index/index',
39
+ loginUrl: 'alipays://platformapi/startapp?appId=demo&page=v/login/index',
40
+ } as const satisfies CfgDefaults;
41
+ export const MEMBER_CFG_INT_KEYS = ['initialPoints'];
33
42
 
34
43
  /**
35
44
  * Mock member 状态单源(模块级共享):factory 每请求新建实例,但状态必须跨实例共享——
@@ -44,11 +53,11 @@ const mockMemberState = {
44
53
  /** mock 壳侧登录用户(换码器固定身份;mock 语义 = 单 demo 用户/部署) */
45
54
  export const MOCK_SHELL_USER_ID = 'demo-user';
46
55
 
47
- /** 入会落地(幂等):enroll 路由/enroll() 共用;余额缺省 300;登录成功即清除登出态 */
56
+ /** 入会落地(幂等):enroll 路由/enroll() 共用;余额不写种子——查询侧按实例配置惰性读
57
+ *(默认 300,见 currentBalanceOf);登录成功即清除登出态 */
48
58
  export function enrollMockMember(userId: string): void {
49
59
  mockMemberState.enrolled.add(userId);
50
60
  mockMemberState.loggedOut.delete(userId);
51
- if (!mockMemberState.balances.has(userId)) mockMemberState.balances.set(userId, 300);
52
61
  }
53
62
 
54
63
  export function isMockMemberEnrolled(userId: string): boolean {
@@ -84,12 +93,23 @@ export class MockMemberServiceImpl implements MockMemberService {
84
93
  * 透传素材,mock 出厂面恒配 demo 值)。demo 值厂商中立(appId=demo),点击在壳内
85
94
  * navigateTo 失败即无害降级。
86
95
  */
87
- readonly enrollUrl = 'alipays://platformapi/startapp?appId=demo&page=v/index/index';
96
+ readonly enrollUrl: string;
88
97
  /** 登录页深链(同上,可选透传素材;供未登录引导卡跳转)。 */
89
- readonly loginUrl = 'alipays://platformapi/startapp?appId=demo&page=v/login/index';
98
+ readonly loginUrl: string;
99
+
100
+ /** 配置快照(fail-soft,构造期一次解析;键集 ≡ MEMBER_CFG_DEFAULTS) */
101
+ private readonly cfg: typeof MEMBER_CFG_DEFAULTS;
102
+ /** 积分流水(金额随 initialPoints 派生——与余额同源) */
103
+ private readonly usage: PointUsageRecord[];
90
104
 
91
105
  /** 默认访客(无种子):demo-user 初始 not_enrolled,经入会闭环翻为 active */
92
- constructor(initial: Record<string, number> = {}) {
106
+ constructor(initial: Record<string, number> = {}, config: unknown = {}) {
107
+ this.cfg = resolveConfig(config, MEMBER_CFG_DEFAULTS, MEMBER_CFG_INT_KEYS);
108
+ this.enrollUrl = this.cfg.enrollUrl;
109
+ this.loginUrl = this.cfg.loginUrl;
110
+ this.usage = [
111
+ { usageRecordRef: 'demo:usage:1', title: '开卡礼', amount: `+${String(this.cfg.initialPoints)}`, usedAt: new Date().toISOString(), description: 'mock 演示流水' },
112
+ ];
93
113
  for (const [userId, balance] of Object.entries(initial)) {
94
114
  mockMemberState.enrolled.add(userId);
95
115
  mockMemberState.balances.set(userId, balance);
@@ -107,30 +127,31 @@ export class MockMemberServiceImpl implements MockMemberService {
107
127
  }
108
128
 
109
129
  async getProfile(_userId: string): Promise<MemberProfileSnapshot> {
110
- return { labels: [{ labelRef: 'demo:label:1', name: '示例会员' }], observedAt: new Date().toISOString() };
130
+ return { labels: [{ labelRef: 'demo:label:1', name: this.cfg.memberName }], observedAt: new Date().toISOString() };
111
131
  }
112
132
 
113
133
  async getCard(userId: string): Promise<MemberCardSnapshot | null> {
114
134
  if (!isMockMemberActive(userId)) return null;
115
135
  return {
116
136
  cardRef: `demo:card:${userId}`,
117
- cardDisplay: '示例会员卡',
137
+ cardDisplay: this.cfg.cardName,
118
138
  status: 'active',
139
+ ...(this.cfg.levelName !== '' ? { levelName: this.cfg.levelName } : {}),
119
140
  observedAt: new Date().toISOString(),
120
141
  };
121
142
  }
122
143
 
123
144
  async queryPoints(userId: string): Promise<Points> {
124
- return { userId, balance: mockMemberState.balances.get(userId) ?? 0, membershipLabel: '示例会员卡' };
145
+ return { userId, balance: this.currentBalanceOf(userId), membershipLabel: this.cfg.cardName };
125
146
  }
126
147
 
127
- async getBenefits(_userId: string): Promise<MemberBenefitsSnapshot> {
128
- return { availablePoints: 300, levelName: '示例等级', observedAt: new Date().toISOString() };
148
+ async getBenefits(userId: string): Promise<MemberBenefitsSnapshot> {
149
+ return { availablePoints: this.currentBalanceOf(userId), levelName: '示例等级', observedAt: new Date().toISOString() };
129
150
  }
130
151
 
131
152
  async listPointUsage(_userId: string, page: number, pageSize: number) {
132
153
  const start = (page - 1) * pageSize;
133
- return { items: DEMO_USAGE.slice(start, start + pageSize), total: DEMO_USAGE.length };
154
+ return { items: this.usage.slice(start, start + pageSize), total: this.usage.length };
134
155
  }
135
156
 
136
157
  async queryDiscount(): Promise<DiscountRule | undefined> {
@@ -138,22 +159,29 @@ export class MockMemberServiceImpl implements MockMemberService {
138
159
  }
139
160
 
140
161
  async deductPoints(userId: string, amount: number, _key: string): Promise<boolean> {
141
- const balance = mockMemberState.balances.get(userId) ?? 0;
162
+ const balance = this.currentBalanceOf(userId);
142
163
  if (balance < amount) return false;
143
164
  mockMemberState.balances.set(userId, balance - amount);
144
165
  return true;
145
166
  }
146
167
 
147
168
  async refundPoints(userId: string, amount: number, _key: string): Promise<boolean> {
148
- mockMemberState.balances.set(userId, (mockMemberState.balances.get(userId) ?? 0) + amount);
169
+ mockMemberState.balances.set(userId, this.currentBalanceOf(userId) + amount);
149
170
  return true;
150
171
  }
151
172
 
152
173
  async awardPoints(userId: string, amount: number): Promise<boolean> {
153
- mockMemberState.balances.set(userId, (mockMemberState.balances.get(userId) ?? 0) + amount);
174
+ mockMemberState.balances.set(userId, this.currentBalanceOf(userId) + amount);
154
175
  return true;
155
176
  }
156
177
 
178
+ /** 余额单源:显式增减覆盖值 ??(已入会 ? 配置初值 : 0)——入会不写种子(惰性读) */
179
+ private currentBalanceOf(userId: string): number {
180
+ const override = mockMemberState.balances.get(userId);
181
+ if (override !== undefined) return override;
182
+ return isMockMemberEnrolled(userId) ? this.cfg.initialPoints : 0;
183
+ }
184
+
157
185
  async enroll(input: MemberEnrollmentInput): Promise<MemberEnrollmentResult> {
158
186
  enrollMockMember(input.userId);
159
187
  return { status: 'active', memberRef: `demo:member:${input.userId}` };
@@ -2,7 +2,9 @@
2
2
  * Mock parking 实现(provider-mock 自持——R3-1 裁决)。演示面最小集:报价/车辆/记录/
3
3
  * 综合信息/权益/寻车 + 三步支付链 mock 闭环(reconcile 恒 PAID 收口)。demo ref 恒
4
4
  * `demo:` 前缀,厂商中立;车牌未提供时以演示车(demo-plate)兜底。
5
+ * config 消费:PARKING_CFG_DEFAULTS 为契约单源(fail-soft 快照;双锁测试 tests/config-schema.test.ts)。
5
6
  */
7
+ import { resolveConfig, type CfgDefaults } from './config';
6
8
  import type {
7
9
  ParkingBenefit,
8
10
  ParkingInfo,
@@ -21,56 +23,83 @@ export type MockParkingService = ParkingQueryPort & ParkingPaymentPort & Parking
21
23
 
22
24
  const now = () => new Date().toISOString();
23
25
 
24
- /**
25
- * kind 必须下发:客户端按它分流(discount = 自动生效、不进勾选区;points = 积分入口),
26
- * 服务端 projectPaymentOptions 也只认 kind==='points'。缺省时整套分流静默失效——
27
- * 等级权益会退化成一个可勾选项,积分入口则从不出现。
28
- */
29
- const DEMO_BENEFITS: ParkingBenefit[] = [
30
- { benefitRef: 'demo:benefit:member-discount', title: '会员 95 折', description: 'mock 演示权益', kind: 'discount', autoSelected: true, estimatedDiscountCents: 75 },
31
- { benefitRef: 'demo:benefit:points-offset', title: '积分抵 5 元', description: 'mock 演示权益', kind: 'points', autoSelected: false, estimatedDiscountCents: 500, requiredPoints: 500, previewPayableCents: 1000 },
32
- ];
33
-
34
- function demoQuote(q: ParkingQueryContext, plateNo: string): QuoteSnapshot {
35
- return {
36
- quoteRef: `demo:quote:${plateNo}`,
37
- plateNo,
38
- mallId: q.scope.mallId ?? 'demo',
39
- entryAt: now(),
40
- feeItems: [{ name: '停车费(3 小时)', amountCents: 1500 }],
41
- totalCents: 1500,
42
- discountCents: 0,
43
- payableCents: 1500,
44
- appliedBenefits: [],
45
- revision: 1,
46
- paymentState: 'UNPAID',
47
- };
48
- }
26
+ /** config 契约单源(键集/缺省/整数约束;tests/config-schema.test.ts 双锁) */
27
+ export const PARKING_CFG_DEFAULTS = {
28
+ parkName: '示例停车场',
29
+ feeName: '停车费(3 小时)',
30
+ feeCents: 1500,
31
+ remainingSpaces: 128,
32
+ firstHourRate: '免费',
33
+ memberBenefitEnabled: true,
34
+ memberBenefitDiscountCents: 75,
35
+ pointsBenefitEnabled: true,
36
+ invoiceEnabled: true,
37
+ vehicleFloor: 'B1',
38
+ vehicleZone: 'A 区',
39
+ vehicleSpaceNo: 'A-128',
40
+ } as const satisfies CfgDefaults;
41
+ export const PARKING_CFG_INT_KEYS = ['feeCents', 'remainingSpaces', 'memberBenefitDiscountCents'];
49
42
 
50
43
  export class MockParkingServiceImpl implements MockParkingService {
51
44
  private vehicles = new Map<string, ParkingVehicle>();
52
- private records: ParkingPaymentRecord[] = [
53
- {
54
- recordRef: 'demo:record:1',
55
- plateNo: 'demo-plate',
56
- amountCents: 1500,
57
- paidAt: now(),
58
- status: 'PAID',
59
- parkName: '示例停车场',
60
- orderCompleted: true,
61
- },
62
- ];
63
-
64
- constructor(mallId = 'mall-demo') {
45
+ private records: ParkingPaymentRecord[];
46
+ /** 配置快照(fail-soft,构造期一次解析;键集 ≡ PARKING_CFG_DEFAULTS) */
47
+ private readonly cfg: typeof PARKING_CFG_DEFAULTS;
48
+
49
+ constructor(mallId = 'mall-demo', config: unknown = {}) {
65
50
  this.mallId = mallId;
66
51
  void this.mallId;
52
+ this.cfg = resolveConfig(config, PARKING_CFG_DEFAULTS, PARKING_CFG_INT_KEYS);
53
+ this.records = [
54
+ {
55
+ recordRef: 'demo:record:1',
56
+ plateNo: 'demo-plate',
57
+ amountCents: this.cfg.feeCents,
58
+ paidAt: now(),
59
+ status: 'PAID',
60
+ parkName: this.cfg.parkName,
61
+ orderCompleted: true,
62
+ },
63
+ ];
67
64
  }
68
65
 
69
66
  private mallId: string;
70
67
 
68
+ /**
69
+ * kind 必须下发:客户端按它分流(discount = 自动生效、不进勾选区;points = 积分入口),
70
+ * 服务端 projectPaymentOptions 也只认 kind==='points'。缺省时整套分流静默失效——
71
+ * 等级权益会退化成一个可勾选项,积分入口则从不出现。
72
+ */
73
+ private benefits(): ParkingBenefit[] {
74
+ const benefits: ParkingBenefit[] = [];
75
+ if (this.cfg.memberBenefitEnabled) {
76
+ benefits.push({ benefitRef: 'demo:benefit:member-discount', title: '会员 95 折', description: 'mock 演示权益', kind: 'discount', autoSelected: true, estimatedDiscountCents: this.cfg.memberBenefitDiscountCents });
77
+ }
78
+ if (this.cfg.pointsBenefitEnabled) {
79
+ benefits.push({ benefitRef: 'demo:benefit:points-offset', title: '积分抵 5 元', description: 'mock 演示权益', kind: 'points', autoSelected: false, estimatedDiscountCents: 500, requiredPoints: 500, previewPayableCents: 1000 });
80
+ }
81
+ return benefits;
82
+ }
83
+
84
+ private quote(q: ParkingQueryContext, plateNo: string): QuoteSnapshot {
85
+ return {
86
+ quoteRef: `demo:quote:${plateNo}`,
87
+ plateNo,
88
+ mallId: q.scope.mallId ?? 'demo',
89
+ entryAt: now(),
90
+ feeItems: [{ name: this.cfg.feeName, amountCents: this.cfg.feeCents }],
91
+ totalCents: this.cfg.feeCents,
92
+ discountCents: 0,
93
+ payableCents: this.cfg.feeCents,
94
+ appliedBenefits: [],
95
+ revision: 1,
96
+ paymentState: 'UNPAID',
97
+ };
98
+ }
99
+
71
100
  async queryFee(q: ParkingQueryContext, input: { plateNo?: string; vehicleRef?: string }): Promise<readonly QuoteSnapshot[]> {
72
101
  const plateNo = input.plateNo ?? (input.vehicleRef ? this.vehicles.get(input.vehicleRef)?.plateNo : undefined) ?? 'demo-plate';
73
- const quote = demoQuote(q, plateNo);
102
+ const quote = this.quote(q, plateNo);
74
103
  return quote ? [quote] : [];
75
104
  }
76
105
 
@@ -93,7 +122,7 @@ export class MockParkingServiceImpl implements MockParkingService {
93
122
  }
94
123
 
95
124
  async getInvoiceSettings(): Promise<{ parkingInvoiceEnabled: boolean }> {
96
- return { parkingInvoiceEnabled: true };
125
+ return { parkingInvoiceEnabled: this.cfg.invoiceEnabled };
97
126
  }
98
127
 
99
128
  async listInvoiceableOrders(_q: ParkingQueryContext, page: number, pageSize: number): Promise<ParkingRecordPage> {
@@ -105,21 +134,21 @@ export class MockParkingServiceImpl implements MockParkingService {
105
134
  async queryParkingInfo(_q: ParkingQueryContext, kind: ParkingInfo['kind']): Promise<ParkingInfo> {
106
135
  return {
107
136
  kind,
108
- title: '示例停车场',
137
+ title: this.cfg.parkName,
109
138
  entries: [
110
- { label: '剩余车位', value: '128' },
111
- { label: '首小时费率', value: '免费' },
112
- { label: '会员权益', value: '会员 95 折' },
139
+ { label: '剩余车位', value: String(this.cfg.remainingSpaces) },
140
+ { label: '首小时费率', value: this.cfg.firstHourRate },
141
+ { label: '会员权益', value: this.cfg.memberBenefitEnabled ? '会员 95 折' : '—' },
113
142
  ],
114
143
  };
115
144
  }
116
145
 
117
146
  async queryApplicableBenefits(_q: ParkingQueryContext, _plateNo: string): Promise<readonly ParkingBenefit[]> {
118
- return DEMO_BENEFITS;
147
+ return this.benefits();
119
148
  }
120
149
 
121
150
  async locateVehicle(_q: ParkingQueryContext, _plateNo: string): Promise<ParkingLocation | null> {
122
- return { floorName: 'B1', zoneName: 'A 区', spaceNo: 'A-128', walkingRoute: '电梯左转 50 米' };
151
+ return { floorName: this.cfg.vehicleFloor, zoneName: this.cfg.vehicleZone, spaceNo: this.cfg.vehicleSpaceNo, walkingRoute: '电梯左转 50 米' };
123
152
  }
124
153
 
125
154
  // ===== 支付三步链(mock:reconcile 恒 PAID 收口) =====
@@ -129,7 +158,7 @@ export class MockParkingServiceImpl implements MockParkingService {
129
158
  }
130
159
 
131
160
  async getOrderInfo(_q: ParkingQueryContext, _paymentRef: string): Promise<{ orderInfo?: string; channel: string; payableFeeCents: number | undefined }> {
132
- return { orderInfo: 'demo-order-info', channel: '50', payableFeeCents: 1500 };
161
+ return { orderInfo: 'demo-order-info', channel: '50', payableFeeCents: this.cfg.feeCents };
133
162
  }
134
163
 
135
164
  async getPaymentSign(_q: ParkingQueryContext, _paymentRef: string): Promise<{ orderInfo: string }> {
@@ -145,8 +174,8 @@ export class MockParkingServiceImpl implements MockParkingService {
145
174
  }
146
175
 
147
176
  async reprice(q: ParkingQueryContext, quoteRef: string, benefitRefs: readonly string[]): Promise<QuoteSnapshot> {
148
- const base = demoQuote(q, 'demo-plate');
149
- const discount = DEMO_BENEFITS.filter((b) => benefitRefs.includes(b.benefitRef))
177
+ const base = this.quote(q, 'demo-plate');
178
+ const discount = this.benefits().filter((b) => benefitRefs.includes(b.benefitRef))
150
179
  .reduce((sum, b) => sum + (b.estimatedDiscountCents ?? 0), 0);
151
180
  return {
152
181
  ...base,