@tbox.cn/app-contracts-mall 0.2.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.
- package/README.md +16 -2
- package/dist/alipay-activity/cards.d.ts +305 -0
- package/dist/assembly.d.ts +73 -7
- package/dist/chunk-KIZD6FZ2.js +43 -0
- package/dist/deployment.d.ts +32 -33
- package/dist/domain/activity.d.ts +64 -0
- package/dist/domain/coupon.d.ts +2 -0
- package/dist/domain/entitlement.d.ts +53 -0
- package/dist/domain/offer.d.ts +111 -0
- package/dist/domain/operation.d.ts +22 -0
- package/dist/domain/parking.d.ts +195 -12
- package/dist/domain/points.d.ts +3 -2
- package/dist/domain/shopping-guide.d.ts +201 -0
- package/dist/index.d.ts +15 -2
- package/dist/mall-scope-url.d.ts +13 -0
- package/dist/member/cards.d.ts +636 -213
- package/dist/parking/cards.d.ts +1271 -0
- package/dist/promotion/cards.d.ts +2529 -0
- package/dist/query-context.d.ts +47 -0
- package/dist/runtime.d.ts +12 -2
- package/dist/runtime.js +940 -86
- package/dist/scope.d.ts +42 -15
- package/dist/server/index.d.ts +11 -0
- package/dist/server/index.js +92 -0
- package/dist/server/mall-context.d.ts +24 -0
- package/dist/server/mall-control-plane.d.ts +10 -0
- package/dist/server/request-credentials.d.ts +22 -0
- package/dist/services/customer-service.d.ts +11 -0
- package/dist/services/mall-info.d.ts +39 -0
- package/dist/services/member.d.ts +188 -31
- package/dist/services/parking.d.ts +115 -11
- package/dist/services/promotion.d.ts +100 -4
- package/dist/services/shopping-guide.d.ts +97 -0
- package/dist/shared/cards.d.ts +42 -0
- package/dist/shopping-guide/cards.d.ts +3399 -561
- package/package.json +19 -7
- package/src/alipay-activity/cards.ts +66 -0
- package/src/assembly.ts +159 -0
- package/src/deployment.ts +44 -0
- package/src/domain/activity.ts +66 -0
- package/src/domain/coupon.ts +11 -0
- package/src/domain/discount.ts +6 -0
- package/src/domain/entitlement.ts +56 -0
- package/src/domain/offer.ts +120 -0
- package/src/domain/operation.ts +40 -0
- package/src/domain/parking.ts +207 -0
- package/src/domain/points.ts +15 -0
- package/src/domain/shopping-guide.ts +242 -0
- package/src/events/index.ts +10 -0
- package/src/guard.ts +48 -0
- package/src/index.ts +34 -0
- package/src/mall-scope-url.ts +21 -0
- package/src/member/cards.ts +209 -0
- package/src/parking/cards.ts +233 -0
- package/src/promotion/cards.ts +290 -0
- package/src/query-context.ts +81 -0
- package/src/runtime.ts +43 -0
- package/src/scope.ts +92 -0
- package/src/server/index.ts +11 -0
- package/src/server/mall-context.ts +39 -0
- package/src/server/mall-control-plane.ts +92 -0
- package/src/server/request-credentials.ts +43 -0
- package/src/service-slots.json +26 -0
- package/src/services/customer-service.ts +12 -0
- package/src/services/mall-info.ts +48 -0
- package/src/services/member.ts +386 -0
- package/src/services/parking.ts +182 -0
- package/src/services/promotion.ts +126 -0
- package/src/services/shopping-guide.ts +138 -0
- package/src/shared/cards.ts +25 -0
- package/src/shopping-guide/cards.ts +438 -0
- package/tests/assembly-validate-scope.test.ts +132 -0
- package/tests/cards.test.ts +586 -0
- package/tests/context-unification.test.ts +73 -0
- package/tests/mall-control-plane.test.ts +169 -0
- package/tests/mall-scope-url.test.ts +59 -0
- package/tests/member-status.test.ts +197 -0
- package/tests/query-context.test.ts +111 -0
- package/tests/request-credentials.test.ts +38 -0
- package/tests/resolve-primitives.test.ts +55 -0
- package/tests/service-slots.test.ts +83 -0
- package/tsconfig.build.json +15 -0
- package/tsconfig.json +5 -0
- package/dist/shopping-guide/ports.d.ts +0 -216
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 请求级凭证访问器(F5 单源):从 RequestContext.credentials 提取用户级凭证,
|
|
3
|
+
* 供 provider adapter factory 构造请求级薄壳 service(E2 不变式:用户凭证只入
|
|
4
|
+
* service 请求层,禁止写入被缓存的 transport/authStrategy)。
|
|
5
|
+
*
|
|
6
|
+
* 空 token 返回空串而非抛错——不需要 token 的工具可正常构造 service;
|
|
7
|
+
* 真正需要 token 的方法调用时厂商会拒绝,错误在正确边界产生。
|
|
8
|
+
*/
|
|
9
|
+
import type { RequestContext } from '@tbox.cn/app-contracts';
|
|
10
|
+
|
|
11
|
+
function readString(source: unknown, key: string): string {
|
|
12
|
+
if (source === null || typeof source !== 'object') return '';
|
|
13
|
+
const value = (source as Record<string, unknown>)[key];
|
|
14
|
+
return typeof value === 'string' ? value : '';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** 登录 token(ctx.credentials.token;厂商 body.Header.Token 注入场景) */
|
|
18
|
+
export function credentialsToken(ctx: RequestContext): string {
|
|
19
|
+
return readString(ctx.credentials, 'token');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 厂商 memberId(ctx.credentials.attributes.memberId;openapi 会员域场景) */
|
|
23
|
+
export function credentialsMemberId(ctx: RequestContext): string {
|
|
24
|
+
const attributes = (ctx.credentials as { attributes?: unknown } | undefined)?.attributes;
|
|
25
|
+
return readString(attributes, 'memberId');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 访客令牌(ctx.credentials.attributes.visitortoken;L3 访客层全程头携带) */
|
|
29
|
+
export function credentialsVisitortoken(ctx: RequestContext): string {
|
|
30
|
+
const attributes = (ctx.credentials as { attributes?: unknown } | undefined)?.attributes;
|
|
31
|
+
return readString(attributes, 'visitortoken');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 平台 openId(ctx.credentials.attributes.openid;外部身份绑定场景)。
|
|
36
|
+
* 契约唯一通道:ExternalCredentials.attributes 三元组随 external claim 流动
|
|
37
|
+
*(identity.ts)——曾误读顶层 credentials.openid(契约无此键,恒 undefined,
|
|
38
|
+
* 致绑定动作 OpenId 恒空),勿改回顶层读取。
|
|
39
|
+
*/
|
|
40
|
+
export function credentialsOpenId(ctx: RequestContext): string {
|
|
41
|
+
const attributes = (ctx.credentials as { attributes?: unknown } | undefined)?.attributes;
|
|
42
|
+
return readString(attributes, 'openid');
|
|
43
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "mall 域 service slot 词汇唯一真源。共址 TS 常量并集必须与本文件 slots 逐元素相等(tests/service-slots.test.ts 锁);manifest demand/supply ⊆ slots(scripts/guard-service-slots.mjs 锁)。旧词汇不在此——迁移归 cookbook 配方。",
|
|
3
|
+
"slots": [
|
|
4
|
+
"member.account",
|
|
5
|
+
"member.profile",
|
|
6
|
+
"member.card",
|
|
7
|
+
"member.benefits",
|
|
8
|
+
"member.enrollment",
|
|
9
|
+
"member.snapshot",
|
|
10
|
+
"parking.query",
|
|
11
|
+
"parking.payment",
|
|
12
|
+
"parking.vehicle",
|
|
13
|
+
"parking.snapshot",
|
|
14
|
+
"promotion.offers",
|
|
15
|
+
"promotion.entitlements",
|
|
16
|
+
"promotion.activities",
|
|
17
|
+
"promotion.acquisition",
|
|
18
|
+
"shopping-guide.catalog",
|
|
19
|
+
"shopping-guide.shops",
|
|
20
|
+
"shopping-guide.categories",
|
|
21
|
+
"shopping-guide.directory",
|
|
22
|
+
"auth.alipay-login",
|
|
23
|
+
"mall.info",
|
|
24
|
+
"mall.info-cache"
|
|
25
|
+
]
|
|
26
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 客服承接域 Port 契约(2026-08-31 客服模块拆分:自 shopping-guide AssistanceQueryPort
|
|
3
|
+
* 切出 FAQ/知识库两方法;人工导购 listHumanGuides 留导购域 AssistanceQueryPort)。
|
|
4
|
+
* 客服承接 = 本地知识库域(与 provider 远程检索面正交,恒本地装配)。
|
|
5
|
+
*/
|
|
6
|
+
import type { ShoppingQueryContext } from './shopping-guide';
|
|
7
|
+
import type { FaqEntry, KnowledgeEntry } from '../domain/shopping-guide';
|
|
8
|
+
|
|
9
|
+
export interface CustomerServiceQueryPort {
|
|
10
|
+
searchFaq(ctx: ShoppingQueryContext, query: string, topK?: number): Promise<readonly FaqEntry[]>;
|
|
11
|
+
searchKnowledge(ctx: ShoppingQueryContext, query: string, topK?: number): Promise<readonly KnowledgeEntry[]>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 商场基础信息 Port(mall.info 槽服务契约;动态商场信息源)。
|
|
3
|
+
* 厂商 wire 词汇(如厂商侧 plaza 端点词)不进本层——投影后只余平台词汇。
|
|
4
|
+
* 结构类型子集接口——消费方(模板装配点/模块)仅依赖契约包。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ===== mall.info service slot 词汇(与 service-slots.json 同锁;id 不变,017 常量化) =====
|
|
8
|
+
|
|
9
|
+
export const MALL_INFO_SERVICE = 'mall.info';
|
|
10
|
+
|
|
11
|
+
/** 商场基础信息(动态商场场景下由 mall.info 槽按 mallId 拉取) */
|
|
12
|
+
export interface MallInfo {
|
|
13
|
+
/** 商场 id(= scope.mallId;投影键) */
|
|
14
|
+
mallId: string;
|
|
15
|
+
/** 商场名(品牌区 mallName 动态真源;与 malls 参考表 name 同语义) */
|
|
16
|
+
name: string;
|
|
17
|
+
address?: string;
|
|
18
|
+
latitude?: number;
|
|
19
|
+
longitude?: number;
|
|
20
|
+
/** 城市 id(nearby-mall 动态化预留,本期不消费) */
|
|
21
|
+
cityId?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** mall.info 槽供给端口(provider 包 adapter 实现;模板装配点经 integrations 解析) */
|
|
25
|
+
export interface MallInfoPort {
|
|
26
|
+
/**
|
|
27
|
+
* 查询商场信息;未知/失败 → undefined(fail-soft,调用方负责缓存与降级)。
|
|
28
|
+
* mallId 形状守卫由实现方负责(防 path 注入类字符进上游 URL)。
|
|
29
|
+
*/
|
|
30
|
+
getMallInfo(mallId: string): Promise<MallInfo | undefined>;
|
|
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
|
+
}
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import type { Points } from '../domain/points';
|
|
2
|
+
import type { DiscountRule } from '../domain/discount';
|
|
3
|
+
import type { Identity, RequestContext } from '@tbox.cn/app-contracts';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 会员域 Port(011 K2:6 Port 替换旧 MemberQueryPort/MemberSettlementPort,breaking 0.2.0)。
|
|
7
|
+
* 结构类型子集接口——模块完整实现 extends 本接口,消费方仅依赖契约包。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ===== 会员域 service slot 词汇(与 service-slots.json 同锁) =====
|
|
11
|
+
|
|
12
|
+
export const MEMBER_ACCOUNT_SERVICE = 'member.account';
|
|
13
|
+
export const MEMBER_PROFILE_SERVICE = 'member.profile';
|
|
14
|
+
export const MEMBER_CARD_SERVICE = 'member.card';
|
|
15
|
+
export const MEMBER_BENEFITS_SERVICE = 'member.benefits';
|
|
16
|
+
export const MEMBER_ENROLLMENT_SERVICE = 'member.enrollment';
|
|
17
|
+
|
|
18
|
+
/** 应用登录状态与会员状态相互独立;登录成功不代表已经入会。 */
|
|
19
|
+
export type MemberAuthState = 'authenticated' | 'unauthenticated';
|
|
20
|
+
|
|
21
|
+
/** unknown 表示当前身份无法向会员 Provider 确认状态;其余状态仅描述会员资格。 */
|
|
22
|
+
export type MemberState = 'unknown' | 'active' | 'not_enrolled' | 'closed';
|
|
23
|
+
|
|
24
|
+
// ===== 1. MemberAccountPort:账户/入会状态 =====
|
|
25
|
+
|
|
26
|
+
export interface MemberAccountSnapshot {
|
|
27
|
+
readonly authState: MemberAuthState;
|
|
28
|
+
readonly state: MemberState;
|
|
29
|
+
readonly memberRef?: string;
|
|
30
|
+
readonly primaryMallCardNo?: string;
|
|
31
|
+
readonly registeredAt?: string;
|
|
32
|
+
readonly observedAt: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface MemberAccountPort {
|
|
36
|
+
getAccount(userId: string): Promise<MemberAccountSnapshot>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ===== 2. MemberProfilePort:资料 =====
|
|
40
|
+
|
|
41
|
+
export interface MemberProfileSnapshot {
|
|
42
|
+
readonly displayName?: string;
|
|
43
|
+
readonly avatarUrl?: string;
|
|
44
|
+
readonly contactDisplay?: string;
|
|
45
|
+
readonly birthday?: string;
|
|
46
|
+
readonly gender?: 'unknown' | 'unspecified' | 'male' | 'female';
|
|
47
|
+
readonly labels: ReadonlyArray<{ readonly labelRef: string; readonly name: string }>;
|
|
48
|
+
readonly observedAt: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface MemberProfilePort {
|
|
52
|
+
getProfile(userId: string): Promise<MemberProfileSnapshot>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ===== 3. MemberCardPort:会员卡/卡名 =====
|
|
56
|
+
|
|
57
|
+
/** 通用卡面视觉变体;具体等级到变体的映射由 provider 负责。 */
|
|
58
|
+
export type MemberCardVisualVariant = 'blue' | 'silver' | 'gold' | 'black';
|
|
59
|
+
|
|
60
|
+
export interface MemberCardPresentationSnapshot {
|
|
61
|
+
readonly variant: MemberCardVisualVariant;
|
|
62
|
+
/** 背景资源是否已包含卡名、积分标签等静态文字。 */
|
|
63
|
+
readonly labelsEmbedded?: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 卡面消费进度;金额统一为分,提示文案由 provider 按厂商规则生成。 */
|
|
67
|
+
export interface MemberCardProgressSnapshot {
|
|
68
|
+
readonly daySpentCents: number;
|
|
69
|
+
readonly yearSpentCents: number;
|
|
70
|
+
readonly percent: number;
|
|
71
|
+
readonly message: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 卡片首屏权益摘要;规则正文与跳链不在摘要契约中直接暴露。 */
|
|
75
|
+
export interface MemberBenefitPreview {
|
|
76
|
+
readonly benefitRef: string;
|
|
77
|
+
readonly name: string;
|
|
78
|
+
readonly iconUrl?: string;
|
|
79
|
+
/** 厂商权益摘要标签,例如源码按 parkingFree 拼出的“可免 X 小时”。 */
|
|
80
|
+
readonly description?: string;
|
|
81
|
+
/** 厂商原生权益动作;客户端通过支付宝 startapp 深链交给宿主跳转。 */
|
|
82
|
+
readonly action?: {
|
|
83
|
+
readonly kind: 'miniapp' | 'dialog' | 'toast';
|
|
84
|
+
readonly url?: string;
|
|
85
|
+
readonly title?: string;
|
|
86
|
+
readonly text?: string;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface MemberCardSnapshot {
|
|
91
|
+
readonly cardRef: string;
|
|
92
|
+
readonly cardDisplay: string;
|
|
93
|
+
readonly status: 'active' | 'inactive' | 'frozen' | 'expired' | 'unknown';
|
|
94
|
+
readonly levelName?: string;
|
|
95
|
+
readonly cardImageUrl?: string;
|
|
96
|
+
/** 积分规则长图;客户端以只读图片弹层展示,不作为通用跳链执行。 */
|
|
97
|
+
readonly pointsRuleImageUrl?: string;
|
|
98
|
+
readonly pointsRuleContent?: string;
|
|
99
|
+
/** C 端原生“我的积分”明细页深链。 */
|
|
100
|
+
readonly pointsUrl?: string;
|
|
101
|
+
readonly presentation?: MemberCardPresentationSnapshot;
|
|
102
|
+
readonly progress?: MemberCardProgressSnapshot;
|
|
103
|
+
readonly benefits?: ReadonlyArray<MemberBenefitPreview>;
|
|
104
|
+
readonly observedAt: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface MemberCardPort {
|
|
108
|
+
getCard(userId: string): Promise<MemberCardSnapshot | null>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ===== 4. MemberBenefitsPort:积分/权益(吸收旧 MemberQueryPort + 结算写) =====
|
|
112
|
+
|
|
113
|
+
export interface MemberBenefitsSnapshot {
|
|
114
|
+
readonly availablePoints: number;
|
|
115
|
+
readonly usedPoints?: number;
|
|
116
|
+
readonly expiringPoints?: number;
|
|
117
|
+
readonly levelName?: string;
|
|
118
|
+
readonly observedAt: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface MemberBenefitsPort {
|
|
122
|
+
/** 积分查询(替代旧 MemberQueryPort.queryPoints) */
|
|
123
|
+
queryPoints(userId: string): Promise<Points>;
|
|
124
|
+
/** 权益快照 */
|
|
125
|
+
getBenefits(userId: string): Promise<MemberBenefitsSnapshot>;
|
|
126
|
+
/** 积分流水(points 卡数据源) */
|
|
127
|
+
listPointUsage(userId: string, page: number, pageSize: number): Promise<{
|
|
128
|
+
items: ReadonlyArray<PointUsageRecord>;
|
|
129
|
+
total: number;
|
|
130
|
+
}>;
|
|
131
|
+
/** 折扣查询(替代旧 MemberQueryPort.queryDiscount) */
|
|
132
|
+
queryDiscount(userId: string): Promise<DiscountRule | undefined>;
|
|
133
|
+
/** 结算写(替代旧 MemberSettlementPort;幂等键由调用方生成) */
|
|
134
|
+
deductPoints(userId: string, amount: number, idempotencyKey: string): Promise<boolean>;
|
|
135
|
+
refundPoints(userId: string, amount: number, idempotencyKey: string): Promise<boolean>;
|
|
136
|
+
awardPoints(userId: string, amount: number): Promise<boolean>;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface PointUsageRecord {
|
|
140
|
+
readonly usageRecordRef: string;
|
|
141
|
+
readonly title: string;
|
|
142
|
+
/** 字符串化金额(D11 展示层统一字符串) */
|
|
143
|
+
readonly amount: string;
|
|
144
|
+
readonly usedAt?: string;
|
|
145
|
+
readonly description?: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 会员服务解析工厂(上下文统一化 Stage 2):跨模块 'member.service' 注册面。
|
|
150
|
+
* member 模块 register 期注册 `(reqCtx) => resolveMemberBenefitsPort(reqCtx, ctx)`
|
|
151
|
+
* (017 槽位原子化:本面 = benefits 单槽契约,benefits 方法族);
|
|
152
|
+
* 跨模块消费方经 ctx.resolveService<MemberBenefitsFactory>('member.service')
|
|
153
|
+
* 结构解析后 `await factory(reqCtx)`——替代旧 'member' 全局门面(ctx.auth 并发缺陷)。
|
|
154
|
+
* 身份/会员资格判定请走 'member.status' 注册面(outcome + account),勿经本面取 account 域数据。
|
|
155
|
+
*/
|
|
156
|
+
export type MemberBenefitsFactory = (reqCtx: RequestContext) => Promise<MemberBenefitsPort>;
|
|
157
|
+
|
|
158
|
+
// ===== 5. MemberCouponPort:优惠券(0.4.0 净删——券域由 promotion C 端承接,双真源不留) =====
|
|
159
|
+
|
|
160
|
+
// ===== 6. MemberEnrollmentPort:入会(写通道,enrollment grant 闭环) =====
|
|
161
|
+
|
|
162
|
+
/** 入会身份来源(F2:授权 profile 可信度高于手填) */
|
|
163
|
+
export type MemberEnrollmentIdentity = 'alipay_authorized' | 'enrollment_submission';
|
|
164
|
+
|
|
165
|
+
export interface MemberEnrollmentInput {
|
|
166
|
+
readonly userId: string;
|
|
167
|
+
readonly identity: MemberEnrollmentIdentity;
|
|
168
|
+
/** 表单字段(mobile 等;授权模式下 mobile 强制以授权 profile 为准) */
|
|
169
|
+
readonly fieldValues: Readonly<Record<string, string>>;
|
|
170
|
+
/** 已同意的 consent id 集合(模块侧校验必勾后传入) */
|
|
171
|
+
readonly acceptedConsentIds: readonly string[];
|
|
172
|
+
/** 幂等键(模块生成:sha256(userId\0mallId\0formVersion)) */
|
|
173
|
+
readonly idempotencyKey: string;
|
|
174
|
+
readonly formVersion?: string;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export type MemberEnrollmentResult =
|
|
178
|
+
| { readonly status: 'active'; readonly memberRef: string }
|
|
179
|
+
/** 厂商异步确认(pending 卡 + 显式刷新收口,R4) */
|
|
180
|
+
| { readonly status: 'pending_confirmation'; readonly attemptRef: string }
|
|
181
|
+
| { readonly status: 'rejected'; readonly reason?: string };
|
|
182
|
+
|
|
183
|
+
export type MemberEnrollmentStatus =
|
|
184
|
+
| 'pending_confirmation'
|
|
185
|
+
| 'confirmation_timeout'
|
|
186
|
+
| 'active'
|
|
187
|
+
| 'failed';
|
|
188
|
+
|
|
189
|
+
export interface MemberEnrollmentPort {
|
|
190
|
+
/** 入会(幂等:同 idempotencyKey 重入返回首果) */
|
|
191
|
+
enroll(input: MemberEnrollmentInput): Promise<MemberEnrollmentResult>;
|
|
192
|
+
/** 入会状态查询(attemptRef 归属校验由模块完成) */
|
|
193
|
+
getEnrollmentStatus(userId: string, attemptRef: string): Promise<{ status: MemberEnrollmentStatus; memberRef?: string; reason?: string }>;
|
|
194
|
+
/**
|
|
195
|
+
* 入会表单定义(厂商端点拉取,如厂商 enrollmentConfig 端点);无表单端点厂商可缺省——
|
|
196
|
+
* 调用方 typeof 守卫降级(calculateBestDeal? 同款先例)。017 R13 自 module-member
|
|
197
|
+
* fat 接口收编。
|
|
198
|
+
*/
|
|
199
|
+
getEnrollmentFormDefinition?(userId: string): Promise<MemberEnrollmentFormDefinition>;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ===== 7. SubjectBindingPort:主体绑定解析(跨模块,015 D9) =====
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* 解析平台 userId → 厂商会员外部引用(externalMemberRef)。
|
|
206
|
+
* member 模块注册实现(ctx.services.register('member.subject-binding', ...))。
|
|
207
|
+
* @deprecated 同步派生引用不承载会员身份语义(对任意 userId 恒返回派生值);
|
|
208
|
+
* 会员状态判定与真实外部会员引用请改用 'member.status' 服务(MemberStatusFactory)
|
|
209
|
+
* 与 externalMemberRefOf。保留仅为版本偏斜期兼容。
|
|
210
|
+
*/
|
|
211
|
+
export interface SubjectBindingPort {
|
|
212
|
+
resolveExternalMemberRef(userId: string): string | undefined;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ===== 8. 入会表单定义(F5 自 module-member 下沉:provider 出参 + 模块工具投影共用) =====
|
|
216
|
+
|
|
217
|
+
export interface MemberEnrollmentFormFieldDefinition {
|
|
218
|
+
readonly fieldId: string;
|
|
219
|
+
readonly label: string;
|
|
220
|
+
readonly type: 'text' | 'tel' | 'select' | 'date';
|
|
221
|
+
readonly required: boolean;
|
|
222
|
+
readonly placeholder?: string;
|
|
223
|
+
readonly options?: readonly string[];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export interface MemberEnrollmentConsentDefinition {
|
|
227
|
+
readonly consentId: string;
|
|
228
|
+
readonly version: string;
|
|
229
|
+
readonly title: string;
|
|
230
|
+
readonly required: boolean;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export interface MemberEnrollmentFormDefinition {
|
|
234
|
+
readonly formVersion: string;
|
|
235
|
+
readonly fields: readonly MemberEnrollmentFormFieldDefinition[];
|
|
236
|
+
readonly consents: readonly MemberEnrollmentConsentDefinition[];
|
|
237
|
+
readonly authorizationStatus: 'required' | 'unavailable';
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ===== 9. 会员状态折叠(outcome 单源) =====
|
|
241
|
+
|
|
242
|
+
/** 折叠结果:'unauthenticated'(平台匿名短路或凭证轴未认证)或原始会员资格态。 */
|
|
243
|
+
export type MemberOutcome = 'unauthenticated' | MemberState;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 双轴折叠纯函数(无 IO,唯一真源):identity 轴(平台登录态)优先短路;
|
|
247
|
+
* account 轴(凭证态)独立折叠——凭证轴语义由各 provider 自定义,
|
|
248
|
+
* tokenless 的合法登录可为 unauthenticated。
|
|
249
|
+
*/
|
|
250
|
+
export function classifyMemberOutcome(
|
|
251
|
+
identity: Pick<Identity, 'source'> | null | undefined,
|
|
252
|
+
account: MemberAccountSnapshot | null | undefined,
|
|
253
|
+
): MemberOutcome {
|
|
254
|
+
if (identity?.source === 'anonymous') return 'unauthenticated';
|
|
255
|
+
if (!account) return 'unknown';
|
|
256
|
+
if (account.authState === 'unauthenticated') return 'unauthenticated';
|
|
257
|
+
return account.state;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* 厂商侧会员标识:真实卡号优先,opaque 引用兜底。
|
|
262
|
+
* 兜底引用仅对不校验真实卡号的 provider 安全——需要真实卡号的厂商接口
|
|
263
|
+
* 必须由 provider 提供 primaryMallCardNo。
|
|
264
|
+
*/
|
|
265
|
+
export function externalMemberRefOf(account: MemberAccountSnapshot): string | undefined {
|
|
266
|
+
return account.primaryMallCardNo ?? account.memberRef;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** 跨模块会员状态解析结果(窄读面;实现方注册产物可为超集)。 */
|
|
270
|
+
export interface MemberStatusResolution {
|
|
271
|
+
readonly outcome: MemberOutcome | 'unavailable';
|
|
272
|
+
readonly account?: MemberAccountSnapshot;
|
|
273
|
+
/** 入会引导深链(authenticated 未入会态;未配置缺省) */
|
|
274
|
+
readonly enrollUrl?: string;
|
|
275
|
+
/** 登录引导深链(unauthenticated 态;未配置缺省) */
|
|
276
|
+
readonly loginUrl?: string;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** 跨模块 'member.status' 解析工厂(与 MemberBenefitsFactory 并列的注册面契约)。 */
|
|
280
|
+
export type MemberStatusFactory = (reqCtx: RequestContext) => Promise<MemberStatusResolution>;
|
|
281
|
+
|
|
282
|
+
// ===== 10. 跨模块会员门禁(卡型复用 + 编排单源) =====
|
|
283
|
+
|
|
284
|
+
/** 门禁卡描述子(形状 = SDK ToolCardSpec { cardType, data, note },出卡管线兼容)。 */
|
|
285
|
+
export interface MemberGateCard {
|
|
286
|
+
readonly cardType: 'member-enrollment-cta' | 'member-login-cta';
|
|
287
|
+
readonly data: Record<string, unknown>;
|
|
288
|
+
readonly note: string;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** 门禁 notice 单源(工具 noResultsReason / handler text 两用;closed ≠ not_enrolled——对齐 member 先例语义拆分)。 */
|
|
292
|
+
const MEMBER_GATE_NOTICES: Readonly<Record<'unauthenticated' | 'not_enrolled' | 'closed', string>> = {
|
|
293
|
+
unauthenticated: '请先登录后再使用该服务',
|
|
294
|
+
not_enrolled: '完成入会后即可使用该服务',
|
|
295
|
+
closed: '重新入会后可恢复使用该服务',
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* 纯卡映射:outcome → 门禁卡描述子;硬门禁态恒出卡,深链为**可选透传素材**(未配置
|
|
300
|
+
* 时卡无 URL 字段,客户端禁用按钮/壳能力接管);软放行态 / userId 空 → undefined,
|
|
301
|
+
* 调用方退纯文字引导(notice)。
|
|
302
|
+
* 卡 schema 真源:memberEnrollmentCtaCardDataSchema / memberLoginCtaCardDataSchema(本包 member/cards.ts)。
|
|
303
|
+
*/
|
|
304
|
+
export function memberGateCardOf(
|
|
305
|
+
userId: string,
|
|
306
|
+
status: Pick<MemberStatusResolution, 'outcome' | 'enrollUrl' | 'loginUrl'>,
|
|
307
|
+
): MemberGateCard | undefined {
|
|
308
|
+
const { outcome } = status;
|
|
309
|
+
if (outcome === 'unauthenticated') {
|
|
310
|
+
const loginUrl = status.loginUrl?.trim() || undefined;
|
|
311
|
+
return {
|
|
312
|
+
cardType: 'member-login-cta',
|
|
313
|
+
data: { title: '登录后继续', ...(loginUrl ? { loginUrl } : {}) },
|
|
314
|
+
// note 契约(W7,2026-09-01):主体+动作(模型据此答话转述——「点击卡片登录」)
|
|
315
|
+
note: '未登录 · 点击卡片登录',
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
if (outcome !== 'not_enrolled' && outcome !== 'closed') return undefined;
|
|
319
|
+
if (!userId) return undefined;
|
|
320
|
+
const enrollUrl = status.enrollUrl?.trim() || undefined;
|
|
321
|
+
return {
|
|
322
|
+
cardType: 'member-enrollment-cta',
|
|
323
|
+
data: {
|
|
324
|
+
userId,
|
|
325
|
+
authState: 'authenticated' as const,
|
|
326
|
+
state: outcome,
|
|
327
|
+
title: '入会后继续',
|
|
328
|
+
...(enrollUrl ? { enrollUrl } : {}),
|
|
329
|
+
},
|
|
330
|
+
note: outcome === 'not_enrolled' ? '未入会 · 点击卡片完成入会' : '已退会 · 点击卡片重新入会',
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** resolveMemberGate 返回的拦截块(undefined = 放行)。 */
|
|
335
|
+
export interface MemberGateBlock {
|
|
336
|
+
readonly card?: MemberGateCard;
|
|
337
|
+
readonly notice: string;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* 门禁编排单源:工厂缺席 / 调用抛错 → fail-open(undefined 放行);
|
|
342
|
+
* active / unknown / unavailable → 软放行(读路径软语义——不误伤瞬时故障的会员);
|
|
343
|
+
* 硬门禁(unauthenticated/not_enrolled/closed)→ { card?(未配深链缺省), notice }。
|
|
344
|
+
* 实现方(member.status 工厂)应自收口异常;本函数对抛错实现兜底为软放行(横切面鲁棒性)。
|
|
345
|
+
*/
|
|
346
|
+
export async function resolveMemberGate(
|
|
347
|
+
factory: MemberStatusFactory | undefined,
|
|
348
|
+
reqCtx: RequestContext,
|
|
349
|
+
): Promise<MemberGateBlock | undefined> {
|
|
350
|
+
if (!factory) return undefined;
|
|
351
|
+
let status: MemberStatusResolution;
|
|
352
|
+
try {
|
|
353
|
+
status = await factory(reqCtx);
|
|
354
|
+
} catch {
|
|
355
|
+
return undefined; // fail-open(防御:契约实现方未自收口)
|
|
356
|
+
}
|
|
357
|
+
const { outcome } = status;
|
|
358
|
+
if (outcome !== 'unauthenticated' && outcome !== 'not_enrolled' && outcome !== 'closed') {
|
|
359
|
+
return undefined;
|
|
360
|
+
}
|
|
361
|
+
const card = memberGateCardOf(reqCtx.identity?.userId ?? '', status);
|
|
362
|
+
return {
|
|
363
|
+
notice: MEMBER_GATE_NOTICES[outcome],
|
|
364
|
+
...(card ? { card } : {}),
|
|
365
|
+
};
|
|
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>;
|