@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.
Files changed (63) hide show
  1. package/AGENTS.md +16 -0
  2. package/README.md +99 -11
  3. package/dist/chunk-EPQEF43P.js +1581 -0
  4. package/dist/chunk-QWGRWD5C.js +766 -0
  5. package/dist/client/index.d.ts +2 -2
  6. package/dist/client/index.js +1 -1
  7. package/dist/index.d.ts +2 -3
  8. package/dist/index.js +12 -2
  9. package/dist/member-372B74DY.css +1740 -0
  10. package/dist/server/index.d.ts +657 -286
  11. package/dist/server/index.js +11 -1
  12. package/package.json +25 -16
  13. package/skills/member/SKILL.md +141 -0
  14. package/skills/member/references/enrollment.md +34 -0
  15. package/skills/member/references/points-and-card.md +24 -0
  16. package/src/client/cards/index.ts +10 -0
  17. package/src/client/cards/member-card-summary/index.tsx +375 -0
  18. package/src/client/cards/member-enrollment-cta/index.tsx +61 -0
  19. package/src/client/cards/member-enrollment-form/index.tsx +181 -0
  20. package/src/client/cards/member-enrollment-status/index.tsx +65 -0
  21. package/src/client/cards/member-info/index.tsx +60 -0
  22. package/src/client/cards/member-login-cta/index.tsx +81 -0
  23. package/src/client/cards/member-point-usage-list/index.tsx +53 -0
  24. package/src/client/cards/view.ts +39 -0
  25. package/src/client/components/ImageWithFallback.tsx +38 -0
  26. package/src/client/index.ts +6 -4
  27. package/src/client/styles/member.css +1740 -0
  28. package/src/client/types/css.d.ts +2 -0
  29. package/src/server/action.ts +348 -0
  30. package/src/server/cards/index.ts +7 -7
  31. package/src/server/cards/member-card-summary/meta.ts +26 -5
  32. package/src/server/cards/member-card-summary/samples.ts +272 -0
  33. package/src/server/cards/member-enrollment-cta/meta.ts +19 -0
  34. package/src/server/cards/member-enrollment-form/meta.ts +10 -6
  35. package/src/server/cards/member-enrollment-status/meta.ts +5 -6
  36. package/src/server/cards/member-info/meta.ts +1 -1
  37. package/src/server/cards/member-login-cta/meta.ts +16 -0
  38. package/src/server/cards/member-point-usage-list/meta.ts +2 -3
  39. package/src/server/handler.ts +159 -119
  40. package/src/server/index.ts +129 -50
  41. package/src/server/ports.ts +66 -0
  42. package/src/server/service.ts +94 -80
  43. package/src/server/status.ts +87 -0
  44. package/src/server/subject-binding.ts +19 -0
  45. package/src/server/tool.ts +469 -99
  46. package/tbox.module.json +124 -0
  47. package/tests/cards-render.test.tsx +526 -0
  48. package/tests/enrollment.test.ts +121 -0
  49. package/tests/ports-resolve.test.ts +95 -0
  50. package/tests/preference-memory.test.ts +86 -0
  51. package/tests/samples.test.ts +61 -0
  52. package/tests/service.test.ts +593 -35
  53. package/tests/skills.test.ts +65 -0
  54. package/tests/snapshot-service.test.ts +75 -0
  55. package/tests/status.test.ts +127 -0
  56. package/tests/view.test.ts +33 -0
  57. package/tsup.config.ts +9 -1
  58. package/dist/chunk-JGZGLFO6.js +0 -111
  59. package/dist/chunk-ZV6B36AO.js +0 -613
  60. package/src/client/cards/index.tsx +0 -135
  61. package/src/server/cards/member-coupon-detail/meta.ts +0 -19
  62. package/src/server/cards/member-coupon-list/meta.ts +0 -18
  63. package/tbox.component.json +0 -28
@@ -0,0 +1,2 @@
1
+ /** 0.4.0 D1:模块自持 CSS(经 tsup loader: { '.css': 'copy' } 出货,样式在应用侧生效) */
2
+ declare module '*.css';
@@ -0,0 +1,348 @@
1
+ /**
2
+ * member 写动作(015 S4:入会协议首验切片)。
3
+ *
4
+ * 四动作(全 singleUse + grant 闭环;cards key = meta.cardType):
5
+ * - member.request-alipay-authorization:返回授权 effect(dispatcher 补签 → client getAuthCode)
6
+ * - member.complete-alipay-authorization:回调动作——消费 SDK createAlipayUserAuthorization
7
+ * (011 J7 下沉物首个消费方),PII 经 ctx.state TTL 30min 最小集暂存(F2)
8
+ * - member.enroll:身份解析(授权 profile 可信 mobile 强制覆盖 / manual 正则)+ consent 校验
9
+ * + 幂等键 sha256(userId\0mallId\0formVersion)(W3:模块顶层 InMemoryIdempotencyStore)
10
+ * - member.query-enrollment-status:显式刷新(R4:POC 自动轮询不迁)
11
+ */
12
+ import { createHash } from 'node:crypto';
13
+ import type { ServerContext } from '@tbox.cn/app-sdk/server';
14
+ import { createAlipayUserAuthorization, InMemoryIdempotencyStore } from '@tbox.cn/app-sdk/server';
15
+ import { parseMallScope } from '@tbox.cn/app-contracts-mall';
16
+ import type { CardActionResult, RequestContext } from '@tbox.cn/app-contracts';
17
+ import { z } from 'zod';
18
+
19
+ /** PII 暂存命名空间(F2:TTL 30min + 最小字段 + enroll 成功即 consume 删除) */
20
+ export const MEMBER_PROFILE_NAMESPACE = 'member.enrollment.profile';
21
+ const PROFILE_TTL_MS = 30 * 60 * 1000;
22
+
23
+ const MOBILE_PATTERN = /^1[3-9]\d{9}$/;
24
+
25
+ /** 授权 profile 最小集(F2:不落完整厂商响应) */
26
+ export interface StoredAuthorizationProfile {
27
+ readonly mobile?: string;
28
+ readonly displayName?: string;
29
+ readonly gender?: string;
30
+ readonly birthday?: string;
31
+ }
32
+
33
+ /** 入会表单形状(formVersion 随表单卡下发;与 contracts memberEnrollmentFormCardDataSchema 对齐) */
34
+ export const ENROLLMENT_FORM_VERSION = 'v2026.08';
35
+ const ENROLLMENT_CONSENTS = [
36
+ { consentId: 'privacy', version: '1', title: '隐私政策', required: true },
37
+ { consentId: 'membership-terms', version: '1', title: '会员服务条款', required: false },
38
+ ] as const;
39
+
40
+ /** 模块顶层幂等单例(W3:IdempotencyStore 挂 HTTP gateway 非 ctx,模块自持) */
41
+ const enrollmentIdempotency = new InMemoryIdempotencyStore();
42
+
43
+ export function buildEnrollmentIdempotencyKey(userId: string, mallId: string, formVersion: string): string {
44
+ return createHash('sha256').update(`${userId}\u0000${mallId}\u0000${formVersion}`).digest('hex');
45
+ }
46
+
47
+ function maskMobile(mobile: string): string {
48
+ return mobile.length >= 7 ? `${mobile.slice(0, 3)}****${mobile.slice(-4)}` : '***';
49
+ }
50
+
51
+ function mallIdOf(ctx: RequestContext): string {
52
+ return parseMallScope(ctx.attributes)?.mallId ?? 'unknown';
53
+ }
54
+
55
+ function buildFormCard(input: {
56
+ userId: string;
57
+ state: 'active' | 'not_enrolled' | 'closed';
58
+ authorization: { status: 'required' | 'authorized' | 'unavailable'; mobileMasked?: string };
59
+ prefilled?: StoredAuthorizationProfile;
60
+ manualNotice?: string;
61
+ }): CardActionResult['card'] {
62
+ return {
63
+ cardType: 'member-enrollment-form',
64
+ data: {
65
+ userId: input.userId,
66
+ state: input.state,
67
+ formVersion: ENROLLMENT_FORM_VERSION,
68
+ fields: [
69
+ {
70
+ fieldId: 'mobile',
71
+ label: '手机号',
72
+ type: 'tel',
73
+ required: true,
74
+ ...(input.authorization.status === 'authorized'
75
+ ? { readOnly: true, prefill: input.prefilled?.mobile ?? '', placeholder: maskMobile(input.authorization.mobileMasked ?? '') }
76
+ : { placeholder: '请输入手机号' }),
77
+ },
78
+ { fieldId: 'displayName', label: '姓名', type: 'text', required: false, ...(input.prefilled?.displayName ? { prefill: input.prefilled.displayName } : {}) },
79
+ ...(input.prefilled?.birthday ? [{ fieldId: 'birthday', label: '生日', type: 'date', required: false, prefill: input.prefilled.birthday }] : []),
80
+ ],
81
+ consents: ENROLLMENT_CONSENTS,
82
+ authorization: input.authorization,
83
+ ...(input.manualNotice ? { manualNotice: input.manualNotice } : {}),
84
+ },
85
+ };
86
+ }
87
+
88
+ /** 动作集工厂:register(ctx) 内调用(闭包 ctx.state——PII/attempt 落 SDK StateStore 实例) */
89
+ export function createMemberActions(
90
+ ctx: ServerContext,
91
+ resolveMember: (reqCtx: RequestContext) => Promise<import('./ports').MemberEnrollmentService>,
92
+ ): Record<string, Parameters<ServerContext['cardActions']['register']>[0]> {
93
+ const state = ctx.state;
94
+
95
+ return {
96
+ /** 1. 发起支付宝授权(效果动作;resultActionId 由 dispatcher 补签校验) */
97
+ requestAuthorization: {
98
+ actionId: 'member.request-alipay-authorization',
99
+ moduleId: 'member',
100
+ cardType: 'member-enrollment-form',
101
+ label: '支付宝授权并填充',
102
+ singleUse: true,
103
+ inputSchema: z.object({}).passthrough(),
104
+ async execute(): Promise<CardActionResult> {
105
+ return {
106
+ text: '正在拉起支付宝授权…',
107
+ effects: [
108
+ {
109
+ type: 'authorization.alipay-user-profile',
110
+ scopes: ['auth_user'],
111
+ resultActionId: 'member.complete-alipay-authorization',
112
+ },
113
+ ],
114
+ };
115
+ },
116
+ },
117
+
118
+ /** 2. 授权回调(client getAuthCode → status/authCode 回传;补签 grant 单次消费;不渲染按钮) */
119
+ completeAuthorization: {
120
+ actionId: 'member.complete-alipay-authorization',
121
+ moduleId: 'member',
122
+ cardType: 'member-enrollment-form',
123
+ label: '授权结果回调',
124
+ singleUse: true,
125
+ hidden: true,
126
+ inputSchema: z.object({
127
+ status: z.enum(['success', 'cancel', 'failure', 'unavailable']),
128
+ authCode: z.string().optional(),
129
+ }),
130
+ async execute(input, reqCtx): Promise<CardActionResult> {
131
+ const { status, authCode } = (input ?? {}) as { status?: string; authCode?: string };
132
+ const userId = reqCtx?.identity?.userId ?? '';
133
+ if (!userId) {
134
+ return { text: '授权失败:缺少会话身份' };
135
+ }
136
+ if (status !== 'success' || !authCode) {
137
+ // cancel/failure/unavailable → manual 模式降级(保留重新授权入口)
138
+ return {
139
+ card: buildFormCard({
140
+ userId,
141
+ state: 'not_enrolled',
142
+ authorization: { status: 'unavailable' },
143
+ manualNotice: status === 'unavailable' ? '当前环境不支持支付宝授权,可手动填写手机号入会' : '授权未完成,可手动填写手机号入会',
144
+ }),
145
+ text: '授权未完成,已切换手动填写模式',
146
+ };
147
+ }
148
+ // 011 J7 下沉物首个消费方:authCode → alipay.user.info.share
149
+ let profile: StoredAuthorizationProfile;
150
+ try {
151
+ const authorized = await createAlipayUserAuthorization().authorizeUserProfile(authCode);
152
+ profile = {
153
+ ...(authorized.mobile ? { mobile: authorized.mobile } : {}),
154
+ ...(authorized.displayName ? { displayName: authorized.displayName } : {}),
155
+ ...(authorized.gender ? { gender: authorized.gender } : {}),
156
+ ...(authorized.birthday ? { birthday: authorized.birthday } : {}),
157
+ };
158
+ } catch (e) {
159
+ ctx.logger?.warn(`授权完成降级手动卡:err=${(e as Error).message}`);
160
+ return {
161
+ card: buildFormCard({
162
+ userId,
163
+ state: 'not_enrolled',
164
+ authorization: { status: 'unavailable' },
165
+ manualNotice: `授权资料获取失败(${(e as Error).message}),可手动填写`,
166
+ }),
167
+ text: '授权资料获取失败,已切换手动填写模式',
168
+ };
169
+ }
170
+ // F2:PII 最小集 TTL 30min 暂存(enroll 时 consume 取走 = 用后即删)
171
+ await state.put(MEMBER_PROFILE_NAMESPACE, userId, profile, PROFILE_TTL_MS);
172
+ return {
173
+ card: buildFormCard({
174
+ userId,
175
+ state: 'not_enrolled',
176
+ authorization: { status: 'authorized', mobileMasked: profile.mobile ?? '' },
177
+ prefilled: profile,
178
+ }),
179
+ text: '授权成功,已为你预填手机号,确认无误后提交入会',
180
+ };
181
+ },
182
+ },
183
+
184
+ /** 3. 提交入会(身份解析 + consent 校验 + 幂等 + PII consume) */
185
+ enroll: {
186
+ actionId: 'member.enroll',
187
+ moduleId: 'member',
188
+ cardType: 'member-enrollment-form',
189
+ label: '提交入会',
190
+ singleUse: true,
191
+ inputSchema: z.object({
192
+ fieldValues: z.record(z.string(), z.string()).optional(),
193
+ acceptedConsentIds: z.array(z.string()).optional(),
194
+ formVersion: z.string().optional(),
195
+ }),
196
+ async execute(input, reqCtx): Promise<CardActionResult> {
197
+ const { fieldValues = {}, acceptedConsentIds = [] } = (input ?? {}) as {
198
+ fieldValues?: Record<string, string>;
199
+ acceptedConsentIds?: string[];
200
+ };
201
+ const userId = reqCtx?.identity?.userId ?? '';
202
+ if (!userId) {
203
+ return {
204
+ card: { cardType: 'member-enrollment-status', data: { userId: '', state: 'failed', message: '缺少会话身份' } },
205
+ text: '入会失败:缺少身份信息',
206
+ };
207
+ }
208
+ const member = await resolveMember(reqCtx);
209
+ if (!member) {
210
+ return {
211
+ card: { cardType: 'member-enrollment-status', data: { userId, state: 'failed', message: '会员服务不可用' } },
212
+ text: '入会失败:会员服务不可用',
213
+ };
214
+ }
215
+
216
+ // consent 必勾校验
217
+ const missing = ENROLLMENT_CONSENTS.filter((c) => c.required && !acceptedConsentIds.includes(c.consentId));
218
+ if (missing.length > 0) {
219
+ return {
220
+ card: { cardType: 'member-enrollment-status', data: { userId, state: 'failed', message: `请先同意:${missing.map((c) => c.title).join('、')}` } },
221
+ text: '入会失败:未完成必选同意项',
222
+ };
223
+ }
224
+
225
+ // 身份解析:授权 profile(consume 取走 = F2 用后即删)可信 mobile 强制覆盖
226
+ const authorized = (await state.consume<StoredAuthorizationProfile>(MEMBER_PROFILE_NAMESPACE, userId)) ?? undefined;
227
+ const manualMobile = fieldValues.mobile ?? '';
228
+ const mobile = authorized?.mobile ?? manualMobile;
229
+ const identity = authorized?.mobile ? 'alipay_authorized' : 'enrollment_submission';
230
+ if (!mobile || !MOBILE_PATTERN.test(mobile)) {
231
+ return {
232
+ card: { cardType: 'member-enrollment-status', data: { userId, state: 'failed', message: '手机号格式不正确(授权预填失效请重新授权或手填)' } },
233
+ text: '入会失败:手机号格式不正确',
234
+ };
235
+ }
236
+
237
+ // 幂等(W3:同键重入返回首果)
238
+ // 评审修复(015 #5):payload 存 sha256 摘要——幂等占位不再驻留明文手机号(F2 单一暂存位语义)
239
+ const idempotencyKey = buildEnrollmentIdempotencyKey(userId, mallIdOf(reqCtx), ENROLLMENT_FORM_VERSION);
240
+ const payloadTag = createHash('sha256').update(`${identity}:${mobile}`).digest('hex').slice(0, 16);
241
+ const acquired = await enrollmentIdempotency.tryAcquire(idempotencyKey, payloadTag);
242
+ if (!acquired.acquired) {
243
+ return {
244
+ card: { cardType: 'member-enrollment-status', data: { userId, state: 'failed', message: '重复提交,请稍后在入会状态中刷新确认结果' } },
245
+ text: '请勿重复提交,可刷新查看入会状态',
246
+ };
247
+ }
248
+
249
+ // 评审修复(015 #3):厂商异常 = 瞬时失败——释放幂等占位可重试,不固化 24h 拒绝
250
+ let result: Awaited<ReturnType<typeof member.enroll>>;
251
+ try {
252
+ result = await member.enroll({
253
+ userId,
254
+ identity,
255
+ fieldValues: {
256
+ ...fieldValues,
257
+ mobile,
258
+ ...(fieldValues.displayName || authorized?.displayName ? { displayName: fieldValues.displayName || authorized?.displayName } : {}),
259
+ ...(fieldValues.birthday || authorized?.birthday ? { birthday: fieldValues.birthday || authorized?.birthday } : {}),
260
+ ...(fieldValues.gender || authorized?.gender ? { gender: fieldValues.gender || authorized?.gender } : {}),
261
+ },
262
+ acceptedConsentIds,
263
+ idempotencyKey,
264
+ formVersion: ENROLLMENT_FORM_VERSION,
265
+ });
266
+ } catch (e) {
267
+ enrollmentIdempotency.release?.(idempotencyKey);
268
+ ctx.logger?.warn(`入会失败(幂等已释放 key=${idempotencyKey.slice(0, 8)}…):err=${(e as Error).message}`);
269
+ return {
270
+ card: { cardType: 'member-enrollment-status', data: { userId, state: 'failed', message: '入会请求失败,请稍后重试' } },
271
+ text: '入会请求失败,请重试',
272
+ };
273
+ }
274
+
275
+ const cardForResult = (r: typeof result): { cardType: string; data: Record<string, unknown> } => ({
276
+ cardType: 'member-enrollment-status',
277
+ data:
278
+ r.status === 'active'
279
+ ? { userId, state: 'active', memberRef: r.memberRef, message: '入会成功' }
280
+ : r.status === 'pending_confirmation'
281
+ ? { userId, attemptRef: r.attemptRef, state: 'pending_confirmation', message: '已提交,等待厂商确认(可点击刷新状态查看结果)' }
282
+ : { userId, state: 'failed', message: r.reason ?? '入会被拒绝' },
283
+ });
284
+ await enrollmentIdempotency.commit(idempotencyKey, result);
285
+ return {
286
+ card: cardForResult(result),
287
+ text:
288
+ result.status === 'active'
289
+ ? '入会成功,欢迎加入!'
290
+ : result.status === 'pending_confirmation'
291
+ ? '已提交入会申请,厂商确认中'
292
+ : `入会失败:${result.status === 'rejected' ? result.reason : '未知原因'}`,
293
+ };
294
+ },
295
+ },
296
+
297
+ /** 4. 入会状态显式刷新(R4:无自动轮询) */
298
+ queryEnrollmentStatus: {
299
+ actionId: 'member.query-enrollment-status',
300
+ moduleId: 'member',
301
+ cardType: 'member-enrollment-status',
302
+ label: '刷新状态',
303
+ singleUse: true,
304
+ inputSchema: z.object({ attemptRef: z.string().min(1) }),
305
+ async execute(input, reqCtx): Promise<CardActionResult> {
306
+ const { attemptRef } = (input ?? {}) as { attemptRef?: string };
307
+ const userId = reqCtx?.identity?.userId ?? '';
308
+ const member = await resolveMember(reqCtx);
309
+ if (!userId || !attemptRef || !member) {
310
+ // P0 原地更新:防御分支返回可见失败态(原 text 在 update 路径无消费者)
311
+ return {
312
+ update: {
313
+ data: {
314
+ userId,
315
+ state: 'failed',
316
+ message: '缺少入会记录引用',
317
+ },
318
+ },
319
+ };
320
+ }
321
+ const status = await member.getEnrollmentStatus(userId, attemptRef);
322
+ return {
323
+ update: {
324
+ data: {
325
+ userId,
326
+ attemptRef,
327
+ state: status.status,
328
+ ...(status.memberRef ? { memberRef: status.memberRef } : {}),
329
+ ...(status.status === 'active'
330
+ ? { message: '入会成功,欢迎加入!' }
331
+ : status.status === 'pending_confirmation'
332
+ ? { message: '厂商确认中,可稍后再刷新' }
333
+ : { message: status.reason ?? '入会未完成,请重新提交' }),
334
+ },
335
+ },
336
+ };
337
+ },
338
+ },
339
+ };
340
+ }
341
+
342
+ /** 供 index.ts 消费的动作 id 常量(装配/测试共用) */
343
+ export const MEMBER_ACTION_IDS = {
344
+ requestAuthorization: 'member.request-alipay-authorization',
345
+ completeAuthorization: 'member.complete-alipay-authorization',
346
+ enroll: 'member.enroll',
347
+ queryEnrollmentStatus: 'member.query-enrollment-status',
348
+ } as const;
@@ -1,31 +1,31 @@
1
1
  /**
2
- * 会员 7 卡 meta 聚合导出(每卡一个 meta.ts,doctor AST 约定)。
2
+ * 会员 7 卡 meta 聚合导出(每卡一个 meta.ts,doctor AST 约定;券面 0.4.0 净删——promotion 承接)。
3
3
  */
4
4
  import memberInfoMeta from './member-info/meta';
5
5
  import memberCardSummaryMeta from './member-card-summary/meta';
6
- import memberCouponListMeta from './member-coupon-list/meta';
7
- import memberCouponDetailMeta from './member-coupon-detail/meta';
8
6
  import memberPointUsageListMeta from './member-point-usage-list/meta';
9
7
  import memberEnrollmentFormMeta from './member-enrollment-form/meta';
10
8
  import memberEnrollmentStatusMeta from './member-enrollment-status/meta';
9
+ import memberEnrollmentCtaMeta from './member-enrollment-cta/meta';
10
+ import memberLoginCtaMeta from './member-login-cta/meta';
11
11
 
12
12
  export {
13
13
  memberInfoMeta,
14
14
  memberCardSummaryMeta,
15
- memberCouponListMeta,
16
- memberCouponDetailMeta,
17
15
  memberPointUsageListMeta,
18
16
  memberEnrollmentFormMeta,
19
17
  memberEnrollmentStatusMeta,
18
+ memberEnrollmentCtaMeta,
19
+ memberLoginCtaMeta,
20
20
  };
21
21
 
22
22
  /** 7 卡 meta map(key 约定 = cardType) */
23
23
  export const memberCards = {
24
24
  'member-info': memberInfoMeta,
25
25
  'member-card-summary': memberCardSummaryMeta,
26
- 'member-coupon-list': memberCouponListMeta,
27
- 'member-coupon-detail': memberCouponDetailMeta,
28
26
  'member-point-usage-list': memberPointUsageListMeta,
29
27
  'member-enrollment-form': memberEnrollmentFormMeta,
30
28
  'member-enrollment-status': memberEnrollmentStatusMeta,
29
+ 'member-enrollment-cta': memberEnrollmentCtaMeta,
30
+ 'member-login-cta': memberLoginCtaMeta,
31
31
  } as const;
@@ -1,21 +1,42 @@
1
1
  import type { CardMeta } from '@tbox.cn/app-contracts';
2
2
  import { memberCardSummaryCardDataSchema } from '@tbox.cn/app-contracts-mall';
3
+ import { samples } from './samples';
3
4
 
4
5
  const meta: CardMeta<typeof memberCardSummaryCardDataSchema> = {
5
6
  cardType: 'member-card-summary',
6
7
  dataSchema: memberCardSummaryCardDataSchema,
7
- allowedTools: ['queryMemberCard'],
8
8
  displayName: '会员卡面',
9
- description: '会员卡信息(等级、积分摘要)',
9
+ description: '会员卡信息(真实卡名、动态卡面、积分与消费进度摘要)',
10
10
  sampleData: {
11
11
  userId: 'u_1001',
12
12
  cardRef: 'card:u_1001',
13
- cardDisplay: '大悦城会员卡',
13
+ cardDisplay: '金卡会员',
14
14
  status: 'active',
15
- levelName: 'gold',
16
15
  availablePoints: 860,
16
+ cardImageUrl: 'https://assets.example.com/member/gold-card.png',
17
+ pointsRuleImageUrl: 'https://assets.example.com/member/points-rule.jpg',
18
+ presentation: { variant: 'gold', labelsEmbedded: true },
19
+ progress: {
20
+ daySpentCents: 12_345,
21
+ yearSpentCents: 500_000,
22
+ percent: 25,
23
+ message: '今日已消费123.45元,年累计已消费5000.00元',
24
+ },
25
+ benefits: [
26
+ { benefitRef: 'benefit:parking', name: '免费停车', iconUrl: 'https://assets.example.com/member/parking.png' },
27
+ { benefitRef: 'benefit:points', name: '消费积分', iconUrl: 'https://assets.example.com/member/points.png' },
28
+ { benefitRef: 'benefit:gifts', name: '积分换礼', iconUrl: 'https://assets.example.com/member/gifts.png' },
29
+ { benefitRef: 'benefit:events', name: '会员活动', iconUrl: 'https://assets.example.com/member/events.png' },
30
+ ],
31
+ preferencePrompts: [
32
+ { value: '我常来买衣服', query: '我常来买衣服' },
33
+ { value: '我常带娃来玩', query: '我常带娃来玩' },
34
+ { value: '我常来吃美食', query: '我常来吃美食' },
35
+ ],
17
36
  },
18
- schemaVersion: 1,
37
+ samples,
38
+ schemaVersion: 5,
39
+ migrate: (oldData) => oldData as Record<string, unknown>,
19
40
  };
20
41
 
21
42
  export default meta;