@tbox.cn/app-module-member 0.2.0 → 0.8.7
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/AGENTS.md +16 -0
- package/README.md +99 -11
- package/dist/chunk-EPQEF43P.js +1581 -0
- package/dist/chunk-QWGRWD5C.js +766 -0
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +1 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.js +12 -2
- package/dist/member-372B74DY.css +1740 -0
- package/dist/server/index.d.ts +657 -286
- package/dist/server/index.js +11 -1
- package/package.json +25 -16
- package/skills/member/SKILL.md +141 -0
- package/skills/member/references/enrollment.md +34 -0
- package/skills/member/references/points-and-card.md +24 -0
- package/src/client/cards/index.ts +10 -0
- package/src/client/cards/member-card-summary/index.tsx +375 -0
- package/src/client/cards/member-enrollment-cta/index.tsx +61 -0
- package/src/client/cards/member-enrollment-form/index.tsx +181 -0
- package/src/client/cards/member-enrollment-status/index.tsx +65 -0
- package/src/client/cards/member-info/index.tsx +60 -0
- package/src/client/cards/member-login-cta/index.tsx +81 -0
- package/src/client/cards/member-point-usage-list/index.tsx +53 -0
- package/src/client/cards/view.ts +39 -0
- package/src/client/components/ImageWithFallback.tsx +38 -0
- package/src/client/index.ts +6 -4
- package/src/client/styles/member.css +1740 -0
- package/src/client/types/css.d.ts +2 -0
- package/src/server/action.ts +348 -0
- package/src/server/cards/index.ts +7 -7
- package/src/server/cards/member-card-summary/meta.ts +26 -5
- package/src/server/cards/member-card-summary/samples.ts +272 -0
- package/src/server/cards/member-enrollment-cta/meta.ts +19 -0
- package/src/server/cards/member-enrollment-form/meta.ts +10 -6
- package/src/server/cards/member-enrollment-status/meta.ts +5 -6
- package/src/server/cards/member-info/meta.ts +1 -1
- package/src/server/cards/member-login-cta/meta.ts +16 -0
- package/src/server/cards/member-point-usage-list/meta.ts +2 -3
- package/src/server/handler.ts +159 -119
- package/src/server/index.ts +129 -50
- package/src/server/ports.ts +66 -0
- package/src/server/service.ts +94 -80
- package/src/server/status.ts +87 -0
- package/src/server/subject-binding.ts +19 -0
- package/src/server/tool.ts +469 -99
- package/tbox.module.json +124 -0
- package/tests/cards-render.test.tsx +526 -0
- package/tests/enrollment.test.ts +121 -0
- package/tests/ports-resolve.test.ts +95 -0
- package/tests/preference-memory.test.ts +86 -0
- package/tests/samples.test.ts +61 -0
- package/tests/service.test.ts +593 -35
- package/tests/skills.test.ts +65 -0
- package/tests/snapshot-service.test.ts +75 -0
- package/tests/status.test.ts +127 -0
- package/tests/view.test.ts +33 -0
- package/tsup.config.ts +9 -1
- package/dist/chunk-JGZGLFO6.js +0 -111
- package/dist/chunk-ZV6B36AO.js +0 -613
- package/src/client/cards/index.tsx +0 -135
- package/src/server/cards/member-coupon-detail/meta.ts +0 -19
- package/src/server/cards/member-coupon-list/meta.ts +0 -18
- package/tbox.component.json +0 -28
package/src/server/service.ts
CHANGED
|
@@ -5,38 +5,62 @@ import type {
|
|
|
5
5
|
MemberBenefitsSnapshot,
|
|
6
6
|
MemberCardPort,
|
|
7
7
|
MemberCardSnapshot,
|
|
8
|
-
|
|
9
|
-
MemberCouponSnapshot,
|
|
8
|
+
MemberEnrollmentInput,
|
|
10
9
|
MemberEnrollmentPort,
|
|
10
|
+
MemberEnrollmentResult,
|
|
11
|
+
MemberEnrollmentStatus,
|
|
11
12
|
MemberProfilePort,
|
|
12
13
|
MemberProfileSnapshot,
|
|
13
14
|
Points,
|
|
14
15
|
PointUsageRecord,
|
|
15
16
|
} from '@tbox.cn/app-contracts-mall';
|
|
16
|
-
import type {
|
|
17
|
+
import type {
|
|
18
|
+
DiscountRule,
|
|
19
|
+
MemberEnrollmentFormDefinition,
|
|
20
|
+
} from '@tbox.cn/app-contracts-mall';
|
|
17
21
|
|
|
18
22
|
/**
|
|
19
|
-
* MemberService:商圈会员完整实现(011 K2
|
|
23
|
+
* MemberService:商圈会员完整实现(011 K2 六 Port + 015 入会升级)。
|
|
20
24
|
* extends 契约 Port(保证满足消费方子集约束);完整接口只在本模块包内。
|
|
21
25
|
*/
|
|
26
|
+
/**
|
|
27
|
+
* InMemory 演示实现(仅供隔离单测;运行时注册与 Provider 解析不得使用)。
|
|
28
|
+
* 017 槽位原子化后非 Port 成员归位:getEnrollmentFormDefinition 已收编
|
|
29
|
+
* MemberEnrollmentPort optional 方法(R13);enrollUrl/loginUrl 为槽产物自由属性
|
|
30
|
+
* (enrollment/account 槽的类型视图见 ports.ts);audit 转 InMemory 私有观测成员。
|
|
31
|
+
*/
|
|
22
32
|
export interface MemberService
|
|
23
33
|
extends MemberAccountPort,
|
|
24
34
|
MemberProfilePort,
|
|
25
35
|
MemberCardPort,
|
|
26
36
|
MemberBenefitsPort,
|
|
27
|
-
MemberCouponPort,
|
|
28
37
|
MemberEnrollmentPort {
|
|
29
|
-
/**
|
|
30
|
-
|
|
38
|
+
/** 入会落地页深链(alipays:// 或 H5 链接),从 deployment provider config 注入;空串表示未配置。 */
|
|
39
|
+
readonly enrollUrl: string;
|
|
40
|
+
/**
|
|
41
|
+
* 登录页深链,同样从 deployment provider config 注入。
|
|
42
|
+
* 可选而非必填——真实 provider 接入前不该被迫实现;缺省时登录引导退回文本。
|
|
43
|
+
*/
|
|
44
|
+
readonly loginUrl?: string;
|
|
31
45
|
}
|
|
32
46
|
|
|
33
|
-
|
|
47
|
+
// Form 族归源 TIER1(F5 下沉:provider 出参与模块投影共用同一契约面)
|
|
48
|
+
export type {
|
|
49
|
+
MemberEnrollmentConsentDefinition,
|
|
50
|
+
MemberEnrollmentFormFieldDefinition,
|
|
51
|
+
MemberEnrollmentFormDefinition,
|
|
52
|
+
} from '@tbox.cn/app-contracts-mall';
|
|
53
|
+
|
|
54
|
+
/** 仅供隔离单测使用的 fixture;运行时注册与 Provider 解析不得使用。 */
|
|
34
55
|
export class InMemoryMemberService implements MemberService {
|
|
56
|
+
readonly enrollUrl: string = '';
|
|
35
57
|
private balances = new Map<string, number>();
|
|
36
|
-
private levels = new Map<string, 'silver' | 'gold' | 'platinum'>();
|
|
37
58
|
private idempotentKeys = new Set<string>();
|
|
38
59
|
private usages: PointUsageRecord[] = [];
|
|
39
60
|
private profiles = new Map<string, MemberProfileSnapshot>();
|
|
61
|
+
private enrollmentAttempts = new Map<string, { result: MemberEnrollmentResult; userId: string }>();
|
|
62
|
+
private pendingConfirmations = new Set<string>();
|
|
63
|
+
private statusQueries = new Map<string, number>();
|
|
40
64
|
audit: { action: string; userId: string; amount: number; key?: string }[] = [];
|
|
41
65
|
|
|
42
66
|
constructor(initial: Record<string, number> = {}) {
|
|
@@ -49,6 +73,7 @@ export class InMemoryMemberService implements MemberService {
|
|
|
49
73
|
|
|
50
74
|
async getAccount(userId: string): Promise<MemberAccountSnapshot> {
|
|
51
75
|
return {
|
|
76
|
+
authState: 'authenticated',
|
|
52
77
|
state: this.balances.has(userId) ? 'active' : 'not_enrolled',
|
|
53
78
|
...(this.balances.has(userId) ? { memberRef: `member:${userId}` } : {}),
|
|
54
79
|
observedAt: new Date().toISOString(),
|
|
@@ -72,9 +97,8 @@ export class InMemoryMemberService implements MemberService {
|
|
|
72
97
|
if (!this.balances.has(userId)) return null;
|
|
73
98
|
return {
|
|
74
99
|
cardRef: `card:${userId}`,
|
|
75
|
-
cardDisplay: '
|
|
100
|
+
cardDisplay: '示例会员卡',
|
|
76
101
|
status: 'active',
|
|
77
|
-
levelName: this.levelOf(userId),
|
|
78
102
|
observedAt: new Date().toISOString(),
|
|
79
103
|
};
|
|
80
104
|
}
|
|
@@ -85,18 +109,29 @@ export class InMemoryMemberService implements MemberService {
|
|
|
85
109
|
return {
|
|
86
110
|
userId,
|
|
87
111
|
balance: this.balances.get(userId) ?? 0,
|
|
88
|
-
|
|
112
|
+
membershipLabel: '测试会员卡',
|
|
89
113
|
};
|
|
90
114
|
}
|
|
91
115
|
|
|
92
116
|
async getBenefits(userId: string): Promise<MemberBenefitsSnapshot> {
|
|
93
117
|
return {
|
|
94
118
|
availablePoints: this.balances.get(userId) ?? 0,
|
|
95
|
-
levelName: this.levelOf(userId),
|
|
96
119
|
observedAt: new Date().toISOString(),
|
|
97
120
|
};
|
|
98
121
|
}
|
|
99
122
|
|
|
123
|
+
async getEnrollmentFormDefinition(): Promise<MemberEnrollmentFormDefinition> {
|
|
124
|
+
return {
|
|
125
|
+
formVersion: 'fixture-v1',
|
|
126
|
+
fields: [
|
|
127
|
+
{ fieldId: 'mobile', label: '手机号', type: 'tel', required: true, placeholder: '请输入手机号' },
|
|
128
|
+
{ fieldId: 'displayName', label: '姓名', type: 'text', required: false },
|
|
129
|
+
],
|
|
130
|
+
consents: [{ consentId: 'privacy', version: '1', title: '隐私政策', required: true }],
|
|
131
|
+
authorizationStatus: 'required',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
100
135
|
async listPointUsage(
|
|
101
136
|
userId: string,
|
|
102
137
|
page: number,
|
|
@@ -108,13 +143,8 @@ export class InMemoryMemberService implements MemberService {
|
|
|
108
143
|
}
|
|
109
144
|
|
|
110
145
|
async queryDiscount(userId: string): Promise<DiscountRule | undefined> {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
silver: { level: 'silver', discount: 0.95 },
|
|
114
|
-
gold: { level: 'gold', discount: 0.9 },
|
|
115
|
-
platinum: { level: 'platinum', discount: 0.85 },
|
|
116
|
-
};
|
|
117
|
-
return map[level];
|
|
146
|
+
void userId;
|
|
147
|
+
return undefined;
|
|
118
148
|
}
|
|
119
149
|
|
|
120
150
|
async deductPoints(userId: string, amount: number, idempotencyKey: string): Promise<boolean> {
|
|
@@ -130,7 +160,6 @@ export class InMemoryMemberService implements MemberService {
|
|
|
130
160
|
amount: `-${amount}`,
|
|
131
161
|
usedAt: new Date().toISOString(),
|
|
132
162
|
});
|
|
133
|
-
this.maybeUpgrade(userId);
|
|
134
163
|
return true;
|
|
135
164
|
}
|
|
136
165
|
|
|
@@ -159,75 +188,60 @@ export class InMemoryMemberService implements MemberService {
|
|
|
159
188
|
amount: `+${amount}`,
|
|
160
189
|
usedAt: new Date().toISOString(),
|
|
161
190
|
});
|
|
162
|
-
this.maybeUpgrade(userId);
|
|
163
191
|
return true;
|
|
164
192
|
}
|
|
165
193
|
|
|
166
|
-
// ===== 5. MemberCouponPort =====
|
|
167
|
-
|
|
168
|
-
async listCoupons(
|
|
169
|
-
userId: string,
|
|
170
|
-
page: number,
|
|
171
|
-
pageSize: number,
|
|
172
|
-
): Promise<{ items: ReadonlyArray<MemberCouponSnapshot>; total: number }> {
|
|
173
|
-
void userId;
|
|
174
|
-
void page;
|
|
175
|
-
void pageSize;
|
|
176
|
-
return { items: [], total: 0 };
|
|
177
|
-
}
|
|
194
|
+
// ===== 5. MemberCouponPort(0.4.0 净删——券域 promotion 承接,恒空面移除) =====
|
|
178
195
|
|
|
179
|
-
|
|
180
|
-
return null;
|
|
181
|
-
}
|
|
196
|
+
// ===== 6. MemberEnrollmentPort(015:授权预填 + 幂等 + pending 收口) =====
|
|
182
197
|
|
|
183
|
-
|
|
198
|
+
async enroll(input: MemberEnrollmentInput): Promise<MemberEnrollmentResult> {
|
|
199
|
+
// 幂等:同 idempotencyKey 重入返回首果(015 W3 语义,模块顶层 IdempotencyStore 之上的双保险)
|
|
200
|
+
const existing = this.enrollmentAttempts.get(input.idempotencyKey);
|
|
201
|
+
if (existing) return existing.result;
|
|
184
202
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
error?: string;
|
|
189
|
-
}> {
|
|
190
|
-
if (!this.balances.has(input.userId)) {
|
|
191
|
-
this.balances.set(input.userId, 0);
|
|
203
|
+
const mobile = input.fieldValues.mobile ?? '';
|
|
204
|
+
if (!/^1[3-9]\d{9}$/.test(mobile)) {
|
|
205
|
+
return { status: 'rejected', reason: '手机号格式不正确' };
|
|
192
206
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
// ===== 内部 =====
|
|
210
|
-
|
|
211
|
-
private levelOf(userId: string): 'silver' | 'gold' | 'platinum' {
|
|
212
|
-
const cached = this.levels.get(userId);
|
|
213
|
-
if (cached) return cached;
|
|
214
|
-
const balance = this.balances.get(userId) ?? 0;
|
|
215
|
-
if (balance >= 1000) return 'platinum';
|
|
216
|
-
if (balance >= 300) return 'gold';
|
|
217
|
-
return 'silver';
|
|
207
|
+
const result: MemberEnrollmentResult =
|
|
208
|
+
this.balances.has(input.userId) || this.pendingConfirmations.has(input.userId)
|
|
209
|
+
? { status: 'active', memberRef: `member:${input.userId}` }
|
|
210
|
+
: { status: 'pending_confirmation', attemptRef: input.idempotencyKey };
|
|
211
|
+
if (result.status === 'active') {
|
|
212
|
+
this.profiles.set(input.userId, {
|
|
213
|
+
...(input.fieldValues.displayName ? { displayName: input.fieldValues.displayName } : {}),
|
|
214
|
+
contactDisplay: mobile.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2'),
|
|
215
|
+
labels: [{ labelRef: 'member', name: '会员' }],
|
|
216
|
+
observedAt: new Date().toISOString(),
|
|
217
|
+
});
|
|
218
|
+
} else {
|
|
219
|
+
this.pendingConfirmations.add(input.userId);
|
|
220
|
+
}
|
|
221
|
+
this.enrollmentAttempts.set(input.idempotencyKey, { result, userId: input.userId });
|
|
222
|
+
return result;
|
|
218
223
|
}
|
|
219
224
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
225
|
+
async getEnrollmentStatus(
|
|
226
|
+
userId: string,
|
|
227
|
+
attemptRef: string,
|
|
228
|
+
): Promise<{ status: MemberEnrollmentStatus; memberRef?: string; reason?: string }> {
|
|
229
|
+
const attempt = this.enrollmentAttempts.get(attemptRef);
|
|
230
|
+
if (!attempt || attempt.userId !== userId) {
|
|
231
|
+
return { status: 'failed', reason: '入会记录不存在或不属于当前用户' };
|
|
232
|
+
}
|
|
233
|
+
if (this.balances.has(userId)) {
|
|
234
|
+
return { status: 'active', memberRef: `member:${userId}` };
|
|
226
235
|
}
|
|
236
|
+
// InMemory 模拟:首次查询 pending,第二次查询确认(厂商异步确认的演示语义)
|
|
237
|
+
const queried = this.statusQueries.get(attemptRef) ?? 0;
|
|
238
|
+
this.statusQueries.set(attemptRef, queried + 1);
|
|
239
|
+
if (queried >= 1) {
|
|
240
|
+
this.balances.set(userId, 0);
|
|
241
|
+
this.pendingConfirmations.delete(userId);
|
|
242
|
+
return { status: 'active', memberRef: `member:${userId}` };
|
|
243
|
+
}
|
|
244
|
+
return { status: 'pending_confirmation' };
|
|
227
245
|
}
|
|
228
246
|
|
|
229
|
-
private levelChangeListeners = new Set<(e: { userId: string; newLevel: 'silver' | 'gold' | 'platinum' }) => void>();
|
|
230
|
-
onLevelChange(fn: (e: { userId: string; newLevel: 'silver' | 'gold' | 'platinum' }) => void): void {
|
|
231
|
-
this.levelChangeListeners.add(fn);
|
|
232
|
-
}
|
|
233
247
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会员状态解析(IO 唯一入口)+ outcome 话术单源。
|
|
3
|
+
* soft 单档:任何失败收口 unavailable + warn 留痕——不伪装状态、不臆测未入会。
|
|
4
|
+
*/
|
|
5
|
+
import type {
|
|
6
|
+
MemberAccountSnapshot,
|
|
7
|
+
MemberOutcome,
|
|
8
|
+
MemberStatusResolution,
|
|
9
|
+
} from '@tbox.cn/app-contracts-mall';
|
|
10
|
+
import { classifyMemberOutcome } from '@tbox.cn/app-contracts-mall';
|
|
11
|
+
import type { RequestContext } from '@tbox.cn/app-contracts';
|
|
12
|
+
import type { ServerContext } from '@tbox.cn/app-sdk/server';
|
|
13
|
+
import { resolveMemberAccountPort, resolveMemberEnrollmentPort } from './ports';
|
|
14
|
+
|
|
15
|
+
/** 模块内超集(跨模块窄面 = 契约 MemberStatusResolution)。
|
|
16
|
+
* 017 槽位原子化:service 字段退役(跨槽 fat 类型谎言)——深链素材随 resolution 携带,
|
|
17
|
+
* 调用方(handler/跨模块 gate)按槽另行解析;厂商快照 memo 由共享 impl 的请求级
|
|
18
|
+
* transport 缓存承接(多次构造廉价,行为变化声明④)。 */
|
|
19
|
+
export interface ResolvedMemberStatus extends MemberStatusResolution {}
|
|
20
|
+
|
|
21
|
+
/** 模型面话术(工具 noResultsReason 消费;与现状逐字节一致) */
|
|
22
|
+
export const MEMBER_LOGIN_NOTICE = '请先登录;登录不要求开通会员';
|
|
23
|
+
export const MEMBER_UNKNOWN_NOTICE = '当前无法确认会员状态,请稍后重试';
|
|
24
|
+
/** 已登录未入会(GUEST/潜客):下一步是入会引导而非「无积分」——与已退会拆分 */
|
|
25
|
+
export const MEMBER_NOT_ENROLLED_NOTICE = '尚未入会,入会后可查询积分与服务';
|
|
26
|
+
/** 已退会/资格关闭:账户在但积分语义为空(入会引导卡可另出,话术不混同) */
|
|
27
|
+
export const MEMBER_NO_POINTS_NOTICE = '当前没有可查询的会员积分';
|
|
28
|
+
|
|
29
|
+
/** 直聊面穷尽表(handler 消费;active 走业务路径不查表——Exclude 编译锁死) */
|
|
30
|
+
export const MEMBER_QUERY_NOTICES: Record<Exclude<MemberOutcome | 'unavailable', 'active'>, string> = {
|
|
31
|
+
unauthenticated: '请先登录后再查询会员信息',
|
|
32
|
+
unknown: MEMBER_UNKNOWN_NOTICE,
|
|
33
|
+
not_enrolled: MEMBER_NOT_ENROLLED_NOTICE,
|
|
34
|
+
closed: MEMBER_NO_POINTS_NOTICE,
|
|
35
|
+
unavailable: '会员服务暂时不可用,请稍后再试',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** 匿名短路不触 provider;上下文缺席/resolve/getAccount 失败 → unavailable + warn 留痕
|
|
39
|
+
* (管路故障收口可用性语义,不冒充身份语义)。 */
|
|
40
|
+
export async function resolveMemberStatus(
|
|
41
|
+
reqCtx: RequestContext | undefined,
|
|
42
|
+
ctx: ServerContext,
|
|
43
|
+
): Promise<ResolvedMemberStatus> {
|
|
44
|
+
if (!reqCtx) {
|
|
45
|
+
// 上下文缺席 = 调用方管路断裂(匿名身份语义经 identity.source 表达,与此无关)
|
|
46
|
+
ctx.logger?.warn('会员状态解析失败:运行上下文缺席');
|
|
47
|
+
return { outcome: 'unavailable' };
|
|
48
|
+
}
|
|
49
|
+
if (reqCtx.identity?.source === 'anonymous') {
|
|
50
|
+
// 匿名也 attempt 取 loginUrl(登录引导卡降级深链):工厂建实例只读部署配置、零网络 IO;
|
|
51
|
+
// 未配置/无 mallId/registry 缺失 → fail-soft 无深链(消费方退文字)
|
|
52
|
+
let loginUrl: string | undefined;
|
|
53
|
+
try {
|
|
54
|
+
const accountPort = await resolveMemberAccountPort(reqCtx, ctx);
|
|
55
|
+
loginUrl = accountPort.loginUrl || undefined;
|
|
56
|
+
} catch {
|
|
57
|
+
/* fail-soft */
|
|
58
|
+
}
|
|
59
|
+
return loginUrl ? { outcome: 'unauthenticated', loginUrl } : { outcome: 'unauthenticated' };
|
|
60
|
+
}
|
|
61
|
+
// N1 双槽编排(017):account 槽承载身份判定 + loginUrl;enrollment 槽仅承载
|
|
62
|
+
// enrollUrl 素材——缺席/失败不拖累身份判定(缺省 = 未配置恒出卡、无 URL 字段)。
|
|
63
|
+
let accountPort;
|
|
64
|
+
let account: MemberAccountSnapshot;
|
|
65
|
+
try {
|
|
66
|
+
accountPort = await resolveMemberAccountPort(reqCtx, ctx);
|
|
67
|
+
account = await accountPort.getAccount(reqCtx.identity.userId);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
ctx.logger?.warn(`会员状态解析失败:err=${(error as Error).message}`);
|
|
70
|
+
return { outcome: 'unavailable' };
|
|
71
|
+
}
|
|
72
|
+
let enrollUrl: string | undefined;
|
|
73
|
+
try {
|
|
74
|
+
const enrollmentPort = await resolveMemberEnrollmentPort(reqCtx, ctx);
|
|
75
|
+
enrollUrl = enrollmentPort.enrollUrl || undefined;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
// enrollment 槽缺席/失败 → 无深链素材(CTA 卡仍可出,memberGateCardOf 既有设计);
|
|
78
|
+
// warn 留痕供诊断(配置类错误不静默),不拖累身份判定。
|
|
79
|
+
ctx.logger?.warn(`会员入会深链解析失败:err=${(error as Error).message}`);
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
outcome: classifyMemberOutcome(reqCtx.identity, account),
|
|
83
|
+
account,
|
|
84
|
+
...(enrollUrl ? { enrollUrl } : {}),
|
|
85
|
+
...(accountPort.loginUrl ? { loginUrl: accountPort.loginUrl } : {}),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SubjectBindingPort 兼容实现(已 deprecated——见 contracts-mall 契约注释):
|
|
3
|
+
* 同步签名对任意非空 userId 恒返回平台侧派生引用,不承载会员身份语义。
|
|
4
|
+
* 会员状态判定与真实外部会员引用请消费 'member.status' 服务 + externalMemberRefOf。
|
|
5
|
+
* 保留注册仅为版本偏斜期兼容(旧消费方混装)。
|
|
6
|
+
*/
|
|
7
|
+
import type { SubjectBindingPort } from '@tbox.cn/app-contracts-mall';
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import type { ServerContext } from '@tbox.cn/app-sdk/server';
|
|
10
|
+
|
|
11
|
+
export function createSubjectBinding(ctx: ServerContext): SubjectBindingPort {
|
|
12
|
+
return {
|
|
13
|
+
resolveExternalMemberRef(userId: string): string | undefined {
|
|
14
|
+
void ctx;
|
|
15
|
+
// 同步签名无法访问异步 Provider,这里只提供稳定派生 ref。
|
|
16
|
+
return userId ? `member-ref:${createHash('sha256').update(userId).digest('hex').slice(0, 24)}` : undefined;
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|