@tbox.cn/app-contracts-mall 0.9.0 → 0.10.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.
@@ -10,7 +10,7 @@
10
10
  * 均支持动态 mallId 会话;实例集限制是 provider 业务限制的表达位(槽 instances 激活表
11
11
  * 显式键集 → 数据面 INSTANCE_NOT_FOUND),非框架限制。WS/HTTP 两形态共享单一形状守卫核。
12
12
  */
13
- import type { RequestContext } from '@tbox.cn/app-contracts';
13
+ import type { RequestContext, ScopeAssemblyContract } from '@tbox.cn/app-contracts';
14
14
  import type { MallDeploymentControlPlane } from './deployment';
15
15
  import { type MallScope } from './scope';
16
16
  /**
@@ -18,6 +18,37 @@ import { type MallScope } from './scope';
18
18
  * provider 包与模板装配点经本常量收敛,禁止各处拼写 'auth.alipay-login' 字面量)。
19
19
  */
20
20
  export declare const AUTH_LOGIN_SERVICE = "auth.alipay-login";
21
+ /**
22
+ * mall 域装配形状(TIER0 ScopeAssemblyContract 的 TIER1 具体化;W3-3 自 module-mall-core 下沉):
23
+ * module-mall-core register 期注册于 ctx.domainAssemblies(SCOPE_ASSEMBLY_KIND),
24
+ * 消费方(模板装配点/module-mall-home/SDK)按本类型泛型断言查表——域模块间零包依赖
25
+ * (workspace-dag 铁律:跨模块走注册表 + 契约类型)。
26
+ * 成员 = 模板装配点(validateContext/scopeSessions/start 装点/WS 双钩)最小必要性合集。
27
+ */
28
+ export interface MallScopeAssembly extends ScopeAssemblyContract {
29
+ /** 控制平面(只读快照;纯元数据参考表——展示/目录/默认解析消费,非闸门) */
30
+ readonly controlPlane: Readonly<MallDeploymentControlPlane>;
31
+ /** root config.miniAppId(登录槽 code2Session 入参真源;enhance 回填 + default 兜底共用) */
32
+ readonly rootMiniAppId: string | undefined;
33
+ /** scope 键提取(SDK ScopeSessionRegistry 注入点) */
34
+ extractScopeKey(ctx: RequestContext): string;
35
+ /** scope 形状校验(SDK ScopeSessionRegistry 注入点 + HTTP validateContext 同核) */
36
+ validateScope(attributes: Record<string, unknown>): boolean;
37
+ /** 提取 MallScope(模块 handler 前置;无 scope → undefined 由调用方降级) */
38
+ extractMallScope(ctx: RequestContext): MallScope | undefined;
39
+ /** 领域扁平 scope(原始形态;ActionGrant/memory 隔离等消费) */
40
+ buildScope(attributes: Record<string, unknown>): Record<string, string>;
41
+ /** 扁平 scope + mallName 回填(scopeSessions 注入点;三级回退见 withMallName) */
42
+ buildScopeWithMallName(attributes: Record<string, unknown>): Record<string, string>;
43
+ /** instanceId 提取(integrations 槽解析轴;SDK 零领域注入点) */
44
+ instanceIdOf(scope: Record<string, string>): string;
45
+ /** 域默认值(SDK ctx.scopeDefaults 视图唯一直接消费面) */
46
+ readonly scopeDefaults: Readonly<Record<string, unknown>> | undefined;
47
+ /** 016:默认商圈 HELLO context(无 URL 参数访问时的服务端兜底;缺省 defaultMallId → undefined) */
48
+ defaultMallContext(): Record<string, unknown> | undefined;
49
+ /** D1:ENHANCE_CTX 双职责(miniAppId 回填 + mall.info 预热;fail-soft——抛错/超时原 context 继续) */
50
+ enhanceHelloContext(context: Record<string, unknown>): Promise<Record<string, unknown>>;
51
+ }
21
52
  /**
22
53
  * 形状守卫核(WS/HTTP 两形态共享单一来源):
23
54
  * - mall 键缺席 → true(非 mall 部署/无 context 放行——校验面不因缺 mall 拒绝;
@@ -52,3 +83,13 @@ export declare function createMallRuntimeAssembly(controlPlane: MallDeploymentCo
52
83
  * WS 面由 enhanceContext 补——两形态不对称有内在一致性,勿补对称)。
53
84
  */
54
85
  export declare function createMallContextValidator(): (attributes: Record<string, unknown>) => boolean;
86
+ /**
87
+ * 014 T1:buildScope 包装——从控制平面参考表回填 mallName(自 mall 模板 deployment.ts
88
+ * 迁入,单一模板统一化 W1-①)。
89
+ * scopeFlat(mallId[+miniAppId])→ HELLO_ACK.mallScope 回显,mallName 为壳品牌区
90
+ * (ChatHeader)服务端真源。
91
+ *
92
+ * 三级回退(动态商场):参考表命中 → mall.name;未命中 → fallbackName?.(mallId)
93
+ * (mall.info 槽缓存)→ 双 miss 原样返回(降级)。
94
+ */
95
+ export declare function withMallName(assembly: MallRuntimeAssembly, controlPlane: MallDeploymentControlPlane, fallbackName?: (mallId: string) => string | undefined): (attributes: Record<string, unknown>) => Record<string, string>;
@@ -19,7 +19,7 @@ export interface MallDeploymentMall {
19
19
  /** 小程序 appId(可选拓扑值;mall 级共享——HELLO context.mall.miniAppId 的配置承载,供默认商圈拼装) */
20
20
  miniAppId?: string;
21
21
  /**
22
- * 地理坐标(可选;有值时参与 /api/nearby-mall 就近推荐;缺省不参与排序)。
22
+ * 地理坐标(可选;有值时参与 /api/m/mall-home/nearby-mall 就近推荐;缺省不参与排序)。
23
23
  * 坐标系:WGS-84(高德/支付宝 getLocation 默认输出)。
24
24
  */
25
25
  location?: {
package/dist/index.d.ts CHANGED
@@ -21,6 +21,7 @@ export * from './scope';
21
21
  export * from './query-context';
22
22
  export * from './deployment';
23
23
  export * from './assembly';
24
+ export * from './mall-scope-url';
24
25
  export * from './guard';
25
26
  export * from './shopping-guide/cards';
26
27
  export * from './shared/cards';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * mall scope URL 读源(客户端单一真源;自 mall 模板 platform/ 迁入,单一模板统一化 W1-①;
3
+ * search-only——hash 内参数不读,壳侧 URL 改写层若移参进 hash 则失效,接入契约显式声明)。
4
+ *
5
+ * mallId = 契约键(优先);plazaId = 商场通用别名兜底(壳侧词汇兼容,非 vendor 特判——
6
+ * 正式小程序壳 webview 传 ?…&plazaId=1105288,本 helper 将其归一为 mallId 读源)。
7
+ *
8
+ * 仅客户端消费(浏览器环境);服务端不调用(本包其余 runtime 同构,本文件为浏览器面例外,
9
+ * 函数体内联 window 访问——Node 侧调用会抛,属契约外用法)。
10
+ */
11
+ export declare function readMallIdFromUrl(): string | null;
12
+ /** 小程序 appId 读源(可选;服务端按 root config 回填,客户端缺省不阻塞) */
13
+ export declare function readMiniAppIdFromUrl(): string | null;
package/dist/runtime.d.ts CHANGED
@@ -6,12 +6,13 @@
6
6
  */
7
7
  export { MALL_SCOPE_KEY, mallScopeSchema, mallSlot, parseMallScope, extractMallScope, mallScopeOf, readLoginMallId } from './scope';
8
8
  export { mallScopeOrEmpty, mallSubjectIdOf, withMallScopeFromQuery, toMallQueryContext } from './query-context';
9
- export { createMallRuntimeAssembly, createMallContextValidator, AUTH_LOGIN_SERVICE } from './assembly';
10
- export { MEMBER_ACCOUNT_SERVICE, MEMBER_PROFILE_SERVICE, MEMBER_CARD_SERVICE, MEMBER_BENEFITS_SERVICE, MEMBER_ENROLLMENT_SERVICE, classifyMemberOutcome, externalMemberRefOf, memberGateCardOf, resolveMemberGate, } from './services/member';
11
- export { PARKING_QUERY_SERVICE, PARKING_PAYMENT_SERVICE, PARKING_VEHICLE_SERVICE } from './services/parking';
9
+ export { createMallRuntimeAssembly, createMallContextValidator, withMallName, AUTH_LOGIN_SERVICE } from './assembly';
10
+ export { readMallIdFromUrl, readMiniAppIdFromUrl } from './mall-scope-url';
11
+ export { MEMBER_ACCOUNT_SERVICE, MEMBER_PROFILE_SERVICE, MEMBER_CARD_SERVICE, MEMBER_BENEFITS_SERVICE, MEMBER_ENROLLMENT_SERVICE, MEMBER_SNAPSHOT_SERVICE, classifyMemberOutcome, externalMemberRefOf, memberGateCardOf, resolveMemberGate, } from './services/member';
12
+ export { PARKING_QUERY_SERVICE, PARKING_PAYMENT_SERVICE, PARKING_VEHICLE_SERVICE, PARKING_SNAPSHOT_SERVICE } from './services/parking';
12
13
  export { PROMOTION_OFFERS_SERVICE, PROMOTION_ENTITLEMENTS_SERVICE, PROMOTION_ACTIVITIES_SERVICE, PROMOTION_ACQUISITION_SERVICE, PROMOTION_ACTIVITY_ROUTING_POLICY, } from './services/promotion';
13
14
  export { SHOPPING_CATALOG_SERVICE, SHOPPING_SHOPS_SERVICE, SHOPPING_CATEGORIES_SERVICE, SHOPPING_DIRECTORY_SERVICE, } from './services/shopping-guide';
14
- export { MALL_INFO_SERVICE } from './services/mall-info';
15
+ export { MALL_INFO_SERVICE, MALL_INFO_CACHE_SERVICE } from './services/mall-info';
15
16
  export { mallLocationSchema, parseMallLocation, mallUserRefSchema, parseMallUserRef } from './guard';
16
17
  export * from './shopping-guide/cards';
17
18
  export * from './member/cards';
package/dist/runtime.js CHANGED
@@ -75,6 +75,27 @@ function createMallRuntimeAssembly(controlPlane) {
75
75
  function createMallContextValidator() {
76
76
  return isValidMallShape;
77
77
  }
78
+ function withMallName(assembly, controlPlane, fallbackName) {
79
+ return (attributes) => {
80
+ const scope = assembly.buildScope(attributes);
81
+ const mallId = parseMallScope(attributes)?.mallId;
82
+ const mall = mallId ? controlPlane.malls.find((m) => m.mallId === mallId) : void 0;
83
+ if (mall) return { ...scope, mallName: mall.name };
84
+ const fallback = mallId ? fallbackName?.(mallId) : void 0;
85
+ return fallback ? { ...scope, mallName: fallback } : scope;
86
+ };
87
+ }
88
+
89
+ // src/mall-scope-url.ts
90
+ function readMallIdFromUrl() {
91
+ const params = new URLSearchParams(window.location.search);
92
+ const mallId = params.get("mallId");
93
+ if (mallId) return mallId;
94
+ return params.get("plazaId");
95
+ }
96
+ function readMiniAppIdFromUrl() {
97
+ return new URLSearchParams(window.location.search).get("miniAppId");
98
+ }
78
99
 
79
100
  // src/services/member.ts
80
101
  var MEMBER_ACCOUNT_SERVICE = "member.account";
@@ -140,11 +161,13 @@ async function resolveMemberGate(factory, reqCtx) {
140
161
  ...card ? { card } : {}
141
162
  };
142
163
  }
164
+ var MEMBER_SNAPSHOT_SERVICE = "member.snapshot";
143
165
 
144
166
  // src/services/parking.ts
145
167
  var PARKING_QUERY_SERVICE = "parking.query";
146
168
  var PARKING_PAYMENT_SERVICE = "parking.payment";
147
169
  var PARKING_VEHICLE_SERVICE = "parking.vehicle";
170
+ var PARKING_SNAPSHOT_SERVICE = "parking.snapshot";
148
171
 
149
172
  // src/services/promotion.ts
150
173
  var PROMOTION_OFFERS_SERVICE = "promotion.offers";
@@ -161,6 +184,7 @@ var SHOPPING_DIRECTORY_SERVICE = "shopping-guide.directory";
161
184
 
162
185
  // src/services/mall-info.ts
163
186
  var MALL_INFO_SERVICE = "mall.info";
187
+ var MALL_INFO_CACHE_SERVICE = "mall.info-cache";
164
188
 
165
189
  // src/guard.ts
166
190
  import { z } from "zod";
@@ -1180,6 +1204,7 @@ var parkingCardDataSchemas = {
1180
1204
  };
1181
1205
  export {
1182
1206
  AUTH_LOGIN_SERVICE,
1207
+ MALL_INFO_CACHE_SERVICE,
1183
1208
  MALL_INFO_SERVICE,
1184
1209
  MALL_SCOPE_KEY,
1185
1210
  MEMBER_ACCOUNT_SERVICE,
@@ -1190,9 +1215,11 @@ export {
1190
1215
  MEMBER_PREFERENCE_RECOMMENDATION_QUERIES,
1191
1216
  MEMBER_PREFERENCE_VALUES,
1192
1217
  MEMBER_PROFILE_SERVICE,
1218
+ MEMBER_SNAPSHOT_SERVICE,
1193
1219
  PARKING_PAYMENT_SERVICE,
1194
1220
  PARKING_PLATE_PATTERN,
1195
1221
  PARKING_QUERY_SERVICE,
1222
+ PARKING_SNAPSHOT_SERVICE,
1196
1223
  PARKING_VEHICLE_SERVICE,
1197
1224
  PROMOTION_ACQUISITION_SERVICE,
1198
1225
  PROMOTION_ACTIVITIES_SERVICE,
@@ -1267,6 +1294,8 @@ export {
1267
1294
  promotionOfferDetailCardDataSchema,
1268
1295
  promotionOfferListCardDataSchema,
1269
1296
  readLoginMallId,
1297
+ readMallIdFromUrl,
1298
+ readMiniAppIdFromUrl,
1270
1299
  reasonItemSchema,
1271
1300
  recommendMerchantsCardDataSchema,
1272
1301
  refinementSchema,
@@ -1277,5 +1306,6 @@ export {
1277
1306
  shopStatusSchema,
1278
1307
  shoppingGuideCardDataSchemas,
1279
1308
  toMallQueryContext,
1309
+ withMallName,
1280
1310
  withMallScopeFromQuery
1281
1311
  };
@@ -46,7 +46,7 @@ var mallAttributesSchema = z.strictObject({
46
46
  miniAppId: z.string().min(1).optional(),
47
47
  /** 客服电话(shopping-guide customer-service-faq 卡 link 拨打条目消费,缺省不渲染) */
48
48
  contactPhone: z.string().min(1).optional(),
49
- /** 地理坐标(WGS-84;有值时参与 /api/nearby-mall 就近推荐,缺省不参与排序) */
49
+ /** 地理坐标(WGS-84;有值时参与 /api/m/mall-home/nearby-mall 就近推荐,缺省不参与排序) */
50
50
  location: z.object({
51
51
  lat: z.number(),
52
52
  lon: z.number(),
@@ -65,8 +65,20 @@ function parseMallAttributes(inst) {
65
65
  }
66
66
  return { mallId: inst.id, name: inst.name, ...result.data };
67
67
  }
68
- function mallControlPlaneOf(config) {
69
- const { instances, defaultInstanceId } = normalizeInstances(config);
68
+ function mergeDeclaredInstances(config, declared) {
69
+ if (!declared) return config;
70
+ if (config.instances && config.instances.length > 0) return config;
71
+ if (config.defaultInstanceId !== void 0) return config;
72
+ const rootProvider = config.provider;
73
+ if (!rootProvider) return config;
74
+ const instances = declared[rootProvider];
75
+ if (!instances || instances.length === 0) return config;
76
+ return { ...config, instances: [...instances] };
77
+ }
78
+ function mallControlPlaneOf(config, declared) {
79
+ const { instances, defaultInstanceId } = normalizeInstances(
80
+ mergeDeclaredInstances(config, declared)
81
+ );
70
82
  const malls = instances.map(parseMallAttributes);
71
83
  return { malls, ...defaultInstanceId !== null ? { defaultMallId: defaultInstanceId } : {} };
72
84
  }
@@ -1,8 +1,10 @@
1
- import type { IntegrationsConfig } from '@tbox.cn/app-contracts';
1
+ import type { IntegrationsConfig, ProviderInstanceDeclarations } from '@tbox.cn/app-contracts';
2
2
  import type { MallDeploymentControlPlane } from '../deployment';
3
3
  /**
4
4
  * integrations config → mall 控制平面(装配链唯一入口):
5
- * - instances 注册表逐条投影(id→mallId、name、attributes 赋义展开);
5
+ * - declared = provider 声明快照(缺省关闭——既有调用方零变化;消费方 = mall-core project());
6
+ * - instances 注册表逐条投影(id→mallId、name、attributes 赋义展开——声明实例过同一
7
+ * zod strict 校验,坏键 fail-fast 与显式配置同律);
6
8
  * - defaultMallId = normalizeInstances 解析(显式 ?? 单实例唯一解;无 → undefined)。
7
9
  */
8
- export declare function mallControlPlaneOf(config: IntegrationsConfig): MallDeploymentControlPlane;
10
+ export declare function mallControlPlaneOf(config: IntegrationsConfig, declared?: ProviderInstanceDeclarations): MallDeploymentControlPlane;
@@ -24,3 +24,16 @@ export interface MallInfoPort {
24
24
  */
25
25
  getMallInfo(mallId: string): Promise<MallInfo | undefined>;
26
26
  }
27
+ /** 跨模块 'mall.info-cache' 词汇(与 service-slots.json 同锁)。 */
28
+ export declare const MALL_INFO_CACHE_SERVICE = "mall.info-cache";
29
+ /**
30
+ * mall.info 缓存 Port(缓存 → single-flight → mall.info 槽的读面)。
31
+ * module-mall-core register 期注册(闭包 integrations);消费方 resolveService 直读。
32
+ * 缓存纪律(TTL/负缓存/LRU/single-flight/形状守卫)由实现方收口。
33
+ */
34
+ export interface MallInfoCachePort {
35
+ /** 缓存名读取(同步回退通道;过期/负缓存 → undefined) */
36
+ cachedMallName(mallId: string): string | undefined;
37
+ /** 缓存 → single-flight → mall.info 槽拉取;失败/未知 → undefined(fail-soft) */
38
+ resolveMallInfo(mallId: string): Promise<MallInfo | undefined>;
39
+ }
@@ -251,3 +251,18 @@ export interface MemberGateBlock {
251
251
  * 实现方(member.status 工厂)应自收口异常;本函数对抛错实现兜底为软放行(横切面鲁棒性)。
252
252
  */
253
253
  export declare function resolveMemberGate(factory: MemberStatusFactory | undefined, reqCtx: RequestContext): Promise<MemberGateBlock | undefined>;
254
+ /** 跨模块 'member.snapshot' 词汇(与 service-slots.json 同锁)。 */
255
+ export declare const MEMBER_SNAPSHOT_SERVICE = "member.snapshot";
256
+ /**
257
+ * 会员快览 Port(linked-panel 消费面:账户/授权三态 + 积分 + 入会引导)。
258
+ * member 模块 register 期注册工厂('member.service' 工厂先例);消费方
259
+ * resolveService 后 await factory(reqCtx) 取 Port——enrollUrl 随请求态解析。
260
+ */
261
+ export interface MemberSnapshotPort {
262
+ getAccount(userId: string): Promise<MemberAccountSnapshot>;
263
+ queryPoints(userId: string): Promise<Points>;
264
+ /** 入会引导深链(未入会态引导;enrollment 槽缺席/失败时缺省——fail-soft 不拖垮主面) */
265
+ readonly enrollUrl?: string;
266
+ }
267
+ /** 跨模块 'member.snapshot' 解析工厂(register 期注册,运行期调用)。 */
268
+ export type MemberSnapshotFactory = (reqCtx: RequestContext) => Promise<MemberSnapshotPort>;
@@ -105,3 +105,14 @@ export interface ParkingVehiclePort {
105
105
  vehicleRef?: string;
106
106
  }): Promise<void>;
107
107
  }
108
+ /** 跨模块 'parking.snapshot' 词汇(与 service-slots.json 同锁)。 */
109
+ export declare const PARKING_SNAPSHOT_SERVICE = "parking.snapshot";
110
+ /**
111
+ * 停车快览 Port(linked-panel 消费面:绑定车辆列表 + 在场探测)。
112
+ * Pick 派生 = ParkingQueryPort 方法面(签名即契约,不手抄字段);可选能力口
113
+ * (probeParkedVehicles)缺席性随 Pick 保留。parking 模块 register 期注册工厂;
114
+ * 消费方 resolveService 后 await factory(reqCtx) 取 Port。
115
+ */
116
+ export type ParkingSnapshotPort = Pick<ParkingQueryPort, 'listVehicles' | 'probeParkedVehicles'>;
117
+ /** 跨模块 'parking.snapshot' 解析工厂(register 期注册,运行期调用)。 */
118
+ export type ParkingSnapshotFactory = (reqCtx: RequestContext) => Promise<ParkingSnapshotPort>;
@@ -3251,6 +3251,7 @@ export declare const mallDirectoryCardDataSchema: z.ZodObject<{
3251
3251
  observedAt: z.ZodString;
3252
3252
  }, "strip", z.ZodTypeAny, {
3253
3253
  mallId: string;
3254
+ mallName: string;
3254
3255
  observedAt: string;
3255
3256
  buildings: {
3256
3257
  buildingName: string;
@@ -3272,9 +3273,9 @@ export declare const mallDirectoryCardDataSchema: z.ZodObject<{
3272
3273
  })[] | undefined;
3273
3274
  }[];
3274
3275
  }[];
3275
- mallName: string;
3276
3276
  }, {
3277
3277
  mallId: string;
3278
+ mallName: string;
3278
3279
  observedAt: string;
3279
3280
  buildings: {
3280
3281
  buildingName: string;
@@ -3296,7 +3297,6 @@ export declare const mallDirectoryCardDataSchema: z.ZodObject<{
3296
3297
  })[] | undefined;
3297
3298
  }[];
3298
3299
  }[];
3299
- mallName: string;
3300
3300
  }>;
3301
3301
  export type MallDirectoryCardData = z.infer<typeof mallDirectoryCardDataSchema>;
3302
3302
  /** nearby-mall-list */
@@ -6285,6 +6285,7 @@ export declare const shoppingGuideCardDataSchemas: {
6285
6285
  observedAt: z.ZodString;
6286
6286
  }, "strip", z.ZodTypeAny, {
6287
6287
  mallId: string;
6288
+ mallName: string;
6288
6289
  observedAt: string;
6289
6290
  buildings: {
6290
6291
  buildingName: string;
@@ -6306,9 +6307,9 @@ export declare const shoppingGuideCardDataSchemas: {
6306
6307
  })[] | undefined;
6307
6308
  }[];
6308
6309
  }[];
6309
- mallName: string;
6310
6310
  }, {
6311
6311
  mallId: string;
6312
+ mallName: string;
6312
6313
  observedAt: string;
6313
6314
  buildings: {
6314
6315
  buildingName: string;
@@ -6330,7 +6331,6 @@ export declare const shoppingGuideCardDataSchemas: {
6330
6331
  })[] | undefined;
6331
6332
  }[];
6332
6333
  }[];
6333
- mallName: string;
6334
6334
  }>;
6335
6335
  readonly 'nearby-mall-list': z.ZodObject<{
6336
6336
  query: z.ZodOptional<z.ZodString>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tbox.cn/app-contracts-mall",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "description": "商圈领域契约(TIER1,types + runtime):Port 接口 + MallEventMap + 领域 DTO + scope/装配器运行时。",
6
6
  "exports": {
@@ -29,14 +29,15 @@
29
29
  "access": "public"
30
30
  },
31
31
  "dependencies": {
32
- "@tbox.cn/app-contracts": "0.9.0",
32
+ "@tbox.cn/app-contracts": "0.10.0",
33
33
  "zod": "3.25.76"
34
34
  },
35
35
  "devDependencies": {
36
- "@tbox.cn/app-sdk": "0.17.0",
36
+ "@tbox.cn/app-sdk": "0.19.0",
37
37
  "tsup": "8.5.1",
38
38
  "typescript": "5.9.3",
39
- "vitest": "4.1.10"
39
+ "vitest": "4.1.10",
40
+ "happy-dom": "20.11.2"
40
41
  },
41
42
  "license": "MIT",
42
43
  "scripts": {
package/src/assembly.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  * 均支持动态 mallId 会话;实例集限制是 provider 业务限制的表达位(槽 instances 激活表
11
11
  * 显式键集 → 数据面 INSTANCE_NOT_FOUND),非框架限制。WS/HTTP 两形态共享单一形状守卫核。
12
12
  */
13
- import type { RequestContext } from '@tbox.cn/app-contracts';
13
+ import type { RequestContext, ScopeAssemblyContract } from '@tbox.cn/app-contracts';
14
14
  import type { MallDeploymentControlPlane } from './deployment';
15
15
  import {
16
16
  extractMallScope,
@@ -25,6 +25,38 @@ import {
25
25
  */
26
26
  export const AUTH_LOGIN_SERVICE = 'auth.alipay-login';
27
27
 
28
+ /**
29
+ * mall 域装配形状(TIER0 ScopeAssemblyContract 的 TIER1 具体化;W3-3 自 module-mall-core 下沉):
30
+ * module-mall-core register 期注册于 ctx.domainAssemblies(SCOPE_ASSEMBLY_KIND),
31
+ * 消费方(模板装配点/module-mall-home/SDK)按本类型泛型断言查表——域模块间零包依赖
32
+ * (workspace-dag 铁律:跨模块走注册表 + 契约类型)。
33
+ * 成员 = 模板装配点(validateContext/scopeSessions/start 装点/WS 双钩)最小必要性合集。
34
+ */
35
+ export interface MallScopeAssembly extends ScopeAssemblyContract {
36
+ /** 控制平面(只读快照;纯元数据参考表——展示/目录/默认解析消费,非闸门) */
37
+ readonly controlPlane: Readonly<MallDeploymentControlPlane>;
38
+ /** root config.miniAppId(登录槽 code2Session 入参真源;enhance 回填 + default 兜底共用) */
39
+ readonly rootMiniAppId: string | undefined;
40
+ /** scope 键提取(SDK ScopeSessionRegistry 注入点) */
41
+ extractScopeKey(ctx: RequestContext): string;
42
+ /** scope 形状校验(SDK ScopeSessionRegistry 注入点 + HTTP validateContext 同核) */
43
+ validateScope(attributes: Record<string, unknown>): boolean;
44
+ /** 提取 MallScope(模块 handler 前置;无 scope → undefined 由调用方降级) */
45
+ extractMallScope(ctx: RequestContext): MallScope | undefined;
46
+ /** 领域扁平 scope(原始形态;ActionGrant/memory 隔离等消费) */
47
+ buildScope(attributes: Record<string, unknown>): Record<string, string>;
48
+ /** 扁平 scope + mallName 回填(scopeSessions 注入点;三级回退见 withMallName) */
49
+ buildScopeWithMallName(attributes: Record<string, unknown>): Record<string, string>;
50
+ /** instanceId 提取(integrations 槽解析轴;SDK 零领域注入点) */
51
+ instanceIdOf(scope: Record<string, string>): string;
52
+ /** 域默认值(SDK ctx.scopeDefaults 视图唯一直接消费面) */
53
+ readonly scopeDefaults: Readonly<Record<string, unknown>> | undefined;
54
+ /** 016:默认商圈 HELLO context(无 URL 参数访问时的服务端兜底;缺省 defaultMallId → undefined) */
55
+ defaultMallContext(): Record<string, unknown> | undefined;
56
+ /** D1:ENHANCE_CTX 双职责(miniAppId 回填 + mall.info 预热;fail-soft——抛错/超时原 context 继续) */
57
+ enhanceHelloContext(context: Record<string, unknown>): Promise<Record<string, unknown>>;
58
+ }
59
+
28
60
  /**
29
61
  * 形状守卫核(WS/HTTP 两形态共享单一来源):
30
62
  * - mall 键缺席 → true(非 mall 部署/无 context 放行——校验面不因缺 mall 拒绝;
@@ -101,3 +133,27 @@ export function createMallRuntimeAssembly(
101
133
  export function createMallContextValidator(): (attributes: Record<string, unknown>) => boolean {
102
134
  return isValidMallShape;
103
135
  }
136
+
137
+ /**
138
+ * 014 T1:buildScope 包装——从控制平面参考表回填 mallName(自 mall 模板 deployment.ts
139
+ * 迁入,单一模板统一化 W1-①)。
140
+ * scopeFlat(mallId[+miniAppId])→ HELLO_ACK.mallScope 回显,mallName 为壳品牌区
141
+ * (ChatHeader)服务端真源。
142
+ *
143
+ * 三级回退(动态商场):参考表命中 → mall.name;未命中 → fallbackName?.(mallId)
144
+ * (mall.info 槽缓存)→ 双 miss 原样返回(降级)。
145
+ */
146
+ export function withMallName(
147
+ assembly: MallRuntimeAssembly,
148
+ controlPlane: MallDeploymentControlPlane,
149
+ fallbackName?: (mallId: string) => string | undefined,
150
+ ): (attributes: Record<string, unknown>) => Record<string, string> {
151
+ return (attributes) => {
152
+ const scope = assembly.buildScope(attributes);
153
+ const mallId = parseMallScope(attributes)?.mallId;
154
+ const mall = mallId ? controlPlane.malls.find((m) => m.mallId === mallId) : undefined;
155
+ if (mall) return { ...scope, mallName: mall.name };
156
+ const fallback = mallId ? fallbackName?.(mallId) : undefined;
157
+ return fallback ? { ...scope, mallName: fallback } : scope;
158
+ };
159
+ }
package/src/deployment.ts CHANGED
@@ -20,7 +20,7 @@ export interface MallDeploymentMall {
20
20
  /** 小程序 appId(可选拓扑值;mall 级共享——HELLO context.mall.miniAppId 的配置承载,供默认商圈拼装) */
21
21
  miniAppId?: string;
22
22
  /**
23
- * 地理坐标(可选;有值时参与 /api/nearby-mall 就近推荐;缺省不参与排序)。
23
+ * 地理坐标(可选;有值时参与 /api/m/mall-home/nearby-mall 就近推荐;缺省不参与排序)。
24
24
  * 坐标系:WGS-84(高德/支付宝 getLocation 默认输出)。
25
25
  */
26
26
  location?: {
package/src/index.ts CHANGED
@@ -21,6 +21,7 @@ export * from './scope';
21
21
  export * from './query-context';
22
22
  export * from './deployment';
23
23
  export * from './assembly';
24
+ export * from './mall-scope-url';
24
25
  export * from './guard';
25
26
  export * from './shopping-guide/cards';
26
27
  export * from './shared/cards';
@@ -0,0 +1,21 @@
1
+ /**
2
+ * mall scope URL 读源(客户端单一真源;自 mall 模板 platform/ 迁入,单一模板统一化 W1-①;
3
+ * search-only——hash 内参数不读,壳侧 URL 改写层若移参进 hash 则失效,接入契约显式声明)。
4
+ *
5
+ * mallId = 契约键(优先);plazaId = 商场通用别名兜底(壳侧词汇兼容,非 vendor 特判——
6
+ * 正式小程序壳 webview 传 ?…&plazaId=1105288,本 helper 将其归一为 mallId 读源)。
7
+ *
8
+ * 仅客户端消费(浏览器环境);服务端不调用(本包其余 runtime 同构,本文件为浏览器面例外,
9
+ * 函数体内联 window 访问——Node 侧调用会抛,属契约外用法)。
10
+ */
11
+ export function readMallIdFromUrl(): string | null {
12
+ const params = new URLSearchParams(window.location.search);
13
+ const mallId = params.get('mallId');
14
+ if (mallId) return mallId; // 空串视为缺席(?mallId=&plazaId=x → plazaId 生效)
15
+ return params.get('plazaId');
16
+ }
17
+
18
+ /** 小程序 appId 读源(可选;服务端按 root config 回填,客户端缺省不阻塞) */
19
+ export function readMiniAppIdFromUrl(): string | null {
20
+ return new URLSearchParams(window.location.search).get('miniAppId');
21
+ }
package/src/runtime.ts CHANGED
@@ -6,19 +6,21 @@
6
6
  */
7
7
  export { MALL_SCOPE_KEY, mallScopeSchema, mallSlot, parseMallScope, extractMallScope, mallScopeOf, readLoginMallId } from './scope';
8
8
  export { mallScopeOrEmpty, mallSubjectIdOf, withMallScopeFromQuery, toMallQueryContext } from './query-context';
9
- export { createMallRuntimeAssembly, createMallContextValidator, AUTH_LOGIN_SERVICE } from './assembly';
9
+ export { createMallRuntimeAssembly, createMallContextValidator, withMallName, AUTH_LOGIN_SERVICE } from './assembly';
10
+ export { readMallIdFromUrl, readMiniAppIdFromUrl } from './mall-scope-url';
10
11
  export {
11
12
  MEMBER_ACCOUNT_SERVICE,
12
13
  MEMBER_PROFILE_SERVICE,
13
14
  MEMBER_CARD_SERVICE,
14
15
  MEMBER_BENEFITS_SERVICE,
15
16
  MEMBER_ENROLLMENT_SERVICE,
17
+ MEMBER_SNAPSHOT_SERVICE,
16
18
  classifyMemberOutcome,
17
19
  externalMemberRefOf,
18
20
  memberGateCardOf,
19
21
  resolveMemberGate,
20
22
  } from './services/member';
21
- export { PARKING_QUERY_SERVICE, PARKING_PAYMENT_SERVICE, PARKING_VEHICLE_SERVICE } from './services/parking';
23
+ export { PARKING_QUERY_SERVICE, PARKING_PAYMENT_SERVICE, PARKING_VEHICLE_SERVICE, PARKING_SNAPSHOT_SERVICE } from './services/parking';
22
24
  export {
23
25
  PROMOTION_OFFERS_SERVICE,
24
26
  PROMOTION_ENTITLEMENTS_SERVICE,
@@ -32,7 +34,7 @@ export {
32
34
  SHOPPING_CATEGORIES_SERVICE,
33
35
  SHOPPING_DIRECTORY_SERVICE,
34
36
  } from './services/shopping-guide';
35
- export { MALL_INFO_SERVICE } from './services/mall-info';
37
+ export { MALL_INFO_SERVICE, MALL_INFO_CACHE_SERVICE } from './services/mall-info';
36
38
  export { mallLocationSchema, parseMallLocation, mallUserRefSchema, parseMallUserRef } from './guard';
37
39
  export * from './shopping-guide/cards';
38
40
  export * from './member/cards';
@@ -4,10 +4,14 @@
4
4
  * attributes 赋义 zod 单源(原双模板 deployment.ts 内嵌 zod 上收——消灭 base/mall 双份漂移):
5
5
  * strict 拒拼错键(strip 会让消费点恒 undefined——静默配置失效),错误消息列允许键清单。
6
6
  * TIER0 attributes 全程 opaque,域赋义唯一发生地 = 本文件(011 B1 边界保持)。
7
+ *
8
+ * 隐式默认单实例融合(provider 声明):零拓扑 config(无 instances/defaultInstanceId)+
9
+ * root.provider 命中声明键 → 注入声明实例(default 经 normalizeInstances 单实例唯一解派生
10
+ * ——零新规则)。显式配置恒赢;规则细节见 mergeDeclaredInstances。
7
11
  */
8
12
  import { z } from 'zod';
9
13
  import { normalizeInstances } from '@tbox.cn/app-contracts';
10
- import type { DeploymentInstance, IntegrationsConfig } from '@tbox.cn/app-contracts';
14
+ import type { DeploymentInstance, IntegrationsConfig, ProviderInstanceDeclarations } from '@tbox.cn/app-contracts';
11
15
  import type { MallDeploymentControlPlane, MallDeploymentMall } from '../deployment';
12
16
 
13
17
  /** mall 域 attributes 赋义(strict——拼错键 fail-fast,报错附允许键清单) */
@@ -16,7 +20,7 @@ const mallAttributesSchema = z.strictObject({
16
20
  miniAppId: z.string().min(1).optional(),
17
21
  /** 客服电话(shopping-guide customer-service-faq 卡 link 拨打条目消费,缺省不渲染) */
18
22
  contactPhone: z.string().min(1).optional(),
19
- /** 地理坐标(WGS-84;有值时参与 /api/nearby-mall 就近推荐,缺省不参与排序) */
23
+ /** 地理坐标(WGS-84;有值时参与 /api/m/mall-home/nearby-mall 就近推荐,缺省不参与排序) */
20
24
  location: z
21
25
  .object({
22
26
  lat: z.number(),
@@ -44,13 +48,45 @@ function parseMallAttributes(
44
48
  return { mallId: inst.id, name: inst.name, ...result.data };
45
49
  }
46
50
 
51
+ /**
52
+ * 隐式默认单实例融合(五规则;no-op 分支零分配——原 config 引用返回):
53
+ * 1. declared 缺席 → 原样;
54
+ * 2. config.instances 非空 → 原样(显式表赢);
55
+ * 3. config.defaultInstanceId 显式在场(含 null——「显式无默认」即真值)→ 原样;
56
+ * 4. 零拓扑 + root.provider(root 层恒 string——inline 仅 service 层)命中声明键且声明
57
+ * 非空 → 注入声明实例(default 走 normalizeInstances 既有唯一解);
58
+ * 5. 其余(root.provider 缺席 / 声明无该键 / 空声明数组)→ 原样。
59
+ * 空声明数组短路守 wire 空表不变量(zod 拒空 instances 数组——「无表 = 键缺席唯一写法」;
60
+ * 融合发生在加载校验后,注入空表会绕过 zod 制造非法形态)。
61
+ */
62
+ function mergeDeclaredInstances(
63
+ config: IntegrationsConfig,
64
+ declared: ProviderInstanceDeclarations | undefined,
65
+ ): IntegrationsConfig {
66
+ if (!declared) return config;
67
+ if (config.instances && config.instances.length > 0) return config;
68
+ if (config.defaultInstanceId !== undefined) return config;
69
+ const rootProvider = config.provider;
70
+ if (!rootProvider) return config;
71
+ const instances = declared[rootProvider];
72
+ if (!instances || instances.length === 0) return config;
73
+ return { ...config, instances: [...instances] };
74
+ }
75
+
47
76
  /**
48
77
  * integrations config → mall 控制平面(装配链唯一入口):
49
- * - instances 注册表逐条投影(id→mallId、name、attributes 赋义展开);
78
+ * - declared = provider 声明快照(缺省关闭——既有调用方零变化;消费方 = mall-core project());
79
+ * - instances 注册表逐条投影(id→mallId、name、attributes 赋义展开——声明实例过同一
80
+ * zod strict 校验,坏键 fail-fast 与显式配置同律);
50
81
  * - defaultMallId = normalizeInstances 解析(显式 ?? 单实例唯一解;无 → undefined)。
51
82
  */
52
- export function mallControlPlaneOf(config: IntegrationsConfig): MallDeploymentControlPlane {
53
- const { instances, defaultInstanceId } = normalizeInstances(config);
83
+ export function mallControlPlaneOf(
84
+ config: IntegrationsConfig,
85
+ declared?: ProviderInstanceDeclarations,
86
+ ): MallDeploymentControlPlane {
87
+ const { instances, defaultInstanceId } = normalizeInstances(
88
+ mergeDeclaredInstances(config, declared),
89
+ );
54
90
  const malls: MallDeploymentMall[] = instances.map(parseMallAttributes);
55
91
  return { malls, ...(defaultInstanceId !== null ? { defaultMallId: defaultInstanceId } : {}) };
56
92
  }
@@ -6,9 +6,11 @@
6
6
  "member.card",
7
7
  "member.benefits",
8
8
  "member.enrollment",
9
+ "member.snapshot",
9
10
  "parking.query",
10
11
  "parking.payment",
11
12
  "parking.vehicle",
13
+ "parking.snapshot",
12
14
  "promotion.offers",
13
15
  "promotion.entitlements",
14
16
  "promotion.activities",
@@ -18,6 +20,7 @@
18
20
  "shopping-guide.categories",
19
21
  "shopping-guide.directory",
20
22
  "auth.alipay-login",
21
- "mall.info"
23
+ "mall.info",
24
+ "mall.info-cache"
22
25
  ]
23
26
  }
@@ -29,3 +29,20 @@ export interface MallInfoPort {
29
29
  */
30
30
  getMallInfo(mallId: string): Promise<MallInfo | undefined>;
31
31
  }
32
+
33
+ // ===== mall.info 缓存面(W3-3:缓存装饰跨模块暴露) =====
34
+
35
+ /** 跨模块 'mall.info-cache' 词汇(与 service-slots.json 同锁)。 */
36
+ export const MALL_INFO_CACHE_SERVICE = 'mall.info-cache';
37
+
38
+ /**
39
+ * mall.info 缓存 Port(缓存 → single-flight → mall.info 槽的读面)。
40
+ * module-mall-core register 期注册(闭包 integrations);消费方 resolveService 直读。
41
+ * 缓存纪律(TTL/负缓存/LRU/single-flight/形状守卫)由实现方收口。
42
+ */
43
+ export interface MallInfoCachePort {
44
+ /** 缓存名读取(同步回退通道;过期/负缓存 → undefined) */
45
+ cachedMallName(mallId: string): string | undefined;
46
+ /** 缓存 → single-flight → mall.info 槽拉取;失败/未知 → undefined(fail-soft) */
47
+ resolveMallInfo(mallId: string): Promise<MallInfo | undefined>;
48
+ }
@@ -364,3 +364,23 @@ export async function resolveMemberGate(
364
364
  ...(card ? { card } : {}),
365
365
  };
366
366
  }
367
+
368
+ // ===== 11. 跨模块会员快览(首页联单面板消费面;W3-3 单一模板统一化) =====
369
+
370
+ /** 跨模块 'member.snapshot' 词汇(与 service-slots.json 同锁)。 */
371
+ export const MEMBER_SNAPSHOT_SERVICE = 'member.snapshot';
372
+
373
+ /**
374
+ * 会员快览 Port(linked-panel 消费面:账户/授权三态 + 积分 + 入会引导)。
375
+ * member 模块 register 期注册工厂('member.service' 工厂先例);消费方
376
+ * resolveService 后 await factory(reqCtx) 取 Port——enrollUrl 随请求态解析。
377
+ */
378
+ export interface MemberSnapshotPort {
379
+ getAccount(userId: string): Promise<MemberAccountSnapshot>;
380
+ queryPoints(userId: string): Promise<Points>;
381
+ /** 入会引导深链(未入会态引导;enrollment 槽缺席/失败时缺省——fail-soft 不拖垮主面) */
382
+ readonly enrollUrl?: string;
383
+ }
384
+
385
+ /** 跨模块 'member.snapshot' 解析工厂(register 期注册,运行期调用)。 */
386
+ export type MemberSnapshotFactory = (reqCtx: RequestContext) => Promise<MemberSnapshotPort>;
@@ -164,3 +164,19 @@ export interface ParkingVehiclePort {
164
164
  input: { plateNo?: string; vehicleRef?: string },
165
165
  ): Promise<void>;
166
166
  }
167
+
168
+ // ===== 4. 跨模块停车快览(首页联单面板消费面;W3-3 单一模板统一化) =====
169
+
170
+ /** 跨模块 'parking.snapshot' 词汇(与 service-slots.json 同锁)。 */
171
+ export const PARKING_SNAPSHOT_SERVICE = 'parking.snapshot';
172
+
173
+ /**
174
+ * 停车快览 Port(linked-panel 消费面:绑定车辆列表 + 在场探测)。
175
+ * Pick 派生 = ParkingQueryPort 方法面(签名即契约,不手抄字段);可选能力口
176
+ * (probeParkedVehicles)缺席性随 Pick 保留。parking 模块 register 期注册工厂;
177
+ * 消费方 resolveService 后 await factory(reqCtx) 取 Port。
178
+ */
179
+ export type ParkingSnapshotPort = Pick<ParkingQueryPort, 'listVehicles' | 'probeParkedVehicles'>;
180
+
181
+ /** 跨模块 'parking.snapshot' 解析工厂(register 期注册,运行期调用)。 */
182
+ export type ParkingSnapshotFactory = (reqCtx: RequestContext) => Promise<ParkingSnapshotPort>;
@@ -2,11 +2,12 @@
2
2
  * mallControlPlaneOf 投影单测(integrations v4):
3
3
  * root instances 注册表 → MallDeploymentControlPlane。
4
4
  * 覆盖:无表 → 空平面 / 全字段投影 / attributes strict 拒拼错键(报允许键)/
5
- * defaultInstanceId 解析(显式 / 单实例唯一解 / 多实例无默认 → undefined)/ 无表 + 独立默认。
5
+ * defaultInstanceId 解析(显式 / 单实例唯一解 / 多实例无默认 → undefined)/ 无表 + 独立默认 /
6
+ * 隐式默认单实例融合(provider 声明——五规则矩阵)。
6
7
  */
7
8
  import { describe, expect, it } from 'vitest';
8
9
  import { mallControlPlaneOf } from '../src/server/mall-control-plane';
9
- import type { IntegrationsConfig } from '@tbox.cn/app-contracts';
10
+ import type { ProviderInstanceDeclarations, IntegrationsConfig } from '@tbox.cn/app-contracts';
10
11
 
11
12
  function config(overrides: Partial<IntegrationsConfig> = {}): IntegrationsConfig {
12
13
  return { services: {}, ...overrides };
@@ -84,3 +85,85 @@ describe('mallControlPlaneOf:参考表投影', () => {
84
85
  expect(cp.defaultMallId).toBeUndefined();
85
86
  });
86
87
  });
88
+
89
+ // ── 隐式默认单实例融合(provider 声明——五规则矩阵) ──────────────────────────
90
+
91
+ type Declared = ProviderInstanceDeclarations;
92
+
93
+ const DECLARED_MOCK: Declared = {
94
+ mock: [{ id: 'mall-demo', name: '星河 Mock 小镇', attributes: { miniAppId: 'mock-demo-app' } }],
95
+ };
96
+
97
+ describe('mallControlPlaneOf:隐式默认单实例融合(provider 声明)', () => {
98
+ it('declared 缺席 → 原样(零拓扑空平面)', () => {
99
+ const cp = mallControlPlaneOf(config({ provider: 'mock' }));
100
+ expect(cp.malls).toEqual([]);
101
+ expect(cp.defaultMallId).toBeUndefined();
102
+ });
103
+
104
+ it('显式 instances 赢:声明被忽略', () => {
105
+ const cp = mallControlPlaneOf(config({
106
+ provider: 'mock',
107
+ instances: [{ id: 'mall-real', name: '真实商场' }],
108
+ }), DECLARED_MOCK);
109
+ expect(cp.malls.map((m) => m.mallId)).toEqual(['mall-real']);
110
+ expect(cp.defaultMallId).toBe('mall-real');
111
+ });
112
+
113
+ it('显式 defaultInstanceId(string)赢:声明被忽略(env 注入动态默认形态)', () => {
114
+ const cp = mallControlPlaneOf(config({ provider: 'mock', defaultInstanceId: 'env-injected' }), DECLARED_MOCK);
115
+ expect(cp.malls).toEqual([]);
116
+ expect(cp.defaultMallId).toBe('env-injected');
117
+ });
118
+
119
+ it('显式 defaultInstanceId: null 赢(显式无默认即真值)', () => {
120
+ const cp = mallControlPlaneOf(config({ provider: 'mock', defaultInstanceId: null }), DECLARED_MOCK);
121
+ expect(cp.malls).toEqual([]);
122
+ expect(cp.defaultMallId).toBeUndefined();
123
+ });
124
+
125
+ it('零拓扑 + root.provider 命中 → 注入声明实例 + 单实例唯一解', () => {
126
+ const cp = mallControlPlaneOf(config({ provider: 'mock' }), DECLARED_MOCK);
127
+ expect(cp.malls).toEqual([
128
+ { mallId: 'mall-demo', name: '星河 Mock 小镇', miniAppId: 'mock-demo-app' },
129
+ ]);
130
+ expect(cp.defaultMallId).toBe('mall-demo');
131
+ });
132
+
133
+ it('零拓扑 + root.provider 缺席 → 原样(异构生成器形态边界)', () => {
134
+ const cp = mallControlPlaneOf(config(), DECLARED_MOCK);
135
+ expect(cp.malls).toEqual([]);
136
+ expect(cp.defaultMallId).toBeUndefined();
137
+ });
138
+
139
+ it('零拓扑 + 声明无该键(其他厂商档)→ 原样', () => {
140
+ const cp = mallControlPlaneOf(config({ provider: 'globex' }), DECLARED_MOCK);
141
+ expect(cp.malls).toEqual([]);
142
+ expect(cp.defaultMallId).toBeUndefined();
143
+ });
144
+
145
+ it('零拓扑 + 空声明数组 → 原样(wire 空表不变量)', () => {
146
+ const cp = mallControlPlaneOf(config({ provider: 'mock' }), { mock: [] });
147
+ expect(cp.malls).toEqual([]);
148
+ expect(cp.defaultMallId).toBeUndefined();
149
+ });
150
+
151
+ it('多实例声明 → 注入两实例 + defaultMallId undefined(既有唯一解规则不派生)', () => {
152
+ const cp = mallControlPlaneOf(config({ provider: 'mock' }), {
153
+ mock: [
154
+ { id: 'mall-a', name: 'A' },
155
+ { id: 'mall-b', name: 'B' },
156
+ ],
157
+ });
158
+ expect(cp.malls.map((m) => m.mallId)).toEqual(['mall-a', 'mall-b']);
159
+ expect(cp.defaultMallId).toBeUndefined();
160
+ });
161
+
162
+ it('声明实例坏 attributes 键 → throw(与显式配置同律)', () => {
163
+ expect(() =>
164
+ mallControlPlaneOf(config({ provider: 'mock' }), {
165
+ mock: [{ id: 'mall-demo', name: 'x', attributes: { miniAppid: 'typo' } }],
166
+ }),
167
+ ).toThrow(/miniAppId|contactPhone|location/);
168
+ });
169
+ });
@@ -0,0 +1,59 @@
1
+ // @vitest-environment happy-dom
2
+ /**
3
+ * mall scope URL 读源专项(CS-8 读源收敛;自 mall 模板 platform/ 随迁,单一模板统一化 W1-①):
4
+ * - mallId 优先 / plazaId 别名兜底 / 空串缺席语义
5
+ * - search-only:hash 内参数不读(壳 URL 改写层契约)
6
+ */
7
+ import { describe, expect, it, afterEach } from 'vitest';
8
+ import { readMallIdFromUrl, readMiniAppIdFromUrl } from '../src/mall-scope-url';
9
+
10
+ const ORIGINAL_URL = window.location.href;
11
+
12
+ function setSearch(search: string): void {
13
+ window.history.replaceState({}, '', `${window.location.pathname}${search}`);
14
+ }
15
+
16
+ afterEach(() => {
17
+ window.history.replaceState({}, '', ORIGINAL_URL);
18
+ });
19
+
20
+ describe('readMallIdFromUrl', () => {
21
+ it('仅 mallId → 取 mallId', () => {
22
+ setSearch('?mallId=1102588');
23
+ expect(readMallIdFromUrl()).toBe('1102588');
24
+ });
25
+
26
+ it('仅 plazaId → 取 plazaId(壳侧别名兜底)', () => {
27
+ setSearch('?container=alipay-miniapp&key=AliAgent&plazaId=1105288');
28
+ expect(readMallIdFromUrl()).toBe('1105288');
29
+ });
30
+
31
+ it('mallId + plazaId 并存 → mallId 优先', () => {
32
+ setSearch('?mallId=1102588&plazaId=1105288');
33
+ expect(readMallIdFromUrl()).toBe('1102588');
34
+ });
35
+
36
+ it('mallId 空串 + plazaId 有值 → plazaId(空串缺席语义)', () => {
37
+ setSearch('?mallId=&plazaId=1105288');
38
+ expect(readMallIdFromUrl()).toBe('1105288');
39
+ });
40
+
41
+ it('均缺 → null', () => {
42
+ setSearch('?container=alipay-miniapp');
43
+ expect(readMallIdFromUrl()).toBeNull();
44
+ });
45
+
46
+ it('hash 内参数不读(search-only 纪律)', () => {
47
+ window.history.replaceState({}, '', `${window.location.pathname}?container=x#/?mallId=1102588`);
48
+ expect(readMallIdFromUrl()).toBeNull();
49
+ });
50
+ });
51
+
52
+ describe('readMiniAppIdFromUrl', () => {
53
+ it('有值 → 取回;缺省 → null', () => {
54
+ setSearch('?miniAppId=2021002175601342&mallId=1102588');
55
+ expect(readMiniAppIdFromUrl()).toBe('2021002175601342');
56
+ setSearch('?mallId=1102588');
57
+ expect(readMiniAppIdFromUrl()).toBeNull();
58
+ });
59
+ });
@@ -11,11 +11,13 @@ import {
11
11
  MEMBER_CARD_SERVICE,
12
12
  MEMBER_BENEFITS_SERVICE,
13
13
  MEMBER_ENROLLMENT_SERVICE,
14
+ MEMBER_SNAPSHOT_SERVICE,
14
15
  } from '../src/services/member';
15
16
  import {
16
17
  PARKING_QUERY_SERVICE,
17
18
  PARKING_PAYMENT_SERVICE,
18
19
  PARKING_VEHICLE_SERVICE,
20
+ PARKING_SNAPSHOT_SERVICE,
19
21
  } from '../src/services/parking';
20
22
  import {
21
23
  PROMOTION_OFFERS_SERVICE,
@@ -29,7 +31,7 @@ import {
29
31
  SHOPPING_CATEGORIES_SERVICE,
30
32
  SHOPPING_DIRECTORY_SERVICE,
31
33
  } from '../src/services/shopping-guide';
32
- import { MALL_INFO_SERVICE } from '../src/services/mall-info';
34
+ import { MALL_INFO_SERVICE, MALL_INFO_CACHE_SERVICE } from '../src/services/mall-info';
33
35
  import { AUTH_LOGIN_SERVICE } from '../src/assembly';
34
36
 
35
37
  const constants = [
@@ -51,19 +53,22 @@ const constants = [
51
53
  SHOPPING_DIRECTORY_SERVICE,
52
54
  AUTH_LOGIN_SERVICE,
53
55
  MALL_INFO_SERVICE,
56
+ MEMBER_SNAPSHOT_SERVICE,
57
+ PARKING_SNAPSHOT_SERVICE,
58
+ MALL_INFO_CACHE_SERVICE,
54
59
  ];
55
60
 
56
61
  describe('service slot 词汇交叉锁(JSON 真源 ↔ TS 共址常量)', () => {
57
- it('计数 = 18(auth.alipay-login 与 mall.info 含在内)', () => {
58
- expect(slotsJson.slots).toHaveLength(18);
59
- expect(constants).toHaveLength(18);
62
+ it('计数 = 21(auth.alipay-login 与 mall.info 族含在内)', () => {
63
+ expect(slotsJson.slots).toHaveLength(21);
64
+ expect(constants).toHaveLength(21);
60
65
  });
61
66
 
62
67
  it('常量并集 ≡ JSON slots(互为子集;无重复无缺漏)', () => {
63
68
  const jsonSet = new Set(slotsJson.slots);
64
69
  const constSet = new Set(constants);
65
- expect(jsonSet.size).toBe(18);
66
- expect(constSet.size).toBe(18);
70
+ expect(jsonSet.size).toBe(21);
71
+ expect(constSet.size).toBe(21);
67
72
  for (const id of jsonSet) expect(constSet.has(id), `常量缺 ${id}`).toBe(true);
68
73
  for (const id of constSet) expect(jsonSet.has(id), `JSON 缺 ${id}`).toBe(true);
69
74
  });