@tbox.cn/app-module-member 0.1.0 → 0.2.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.
@@ -1,20 +1,42 @@
1
- import type { MemberQueryPort, MemberSettlementPort, Points } from '@tbox.cn/app-contracts-mall';
1
+ import type {
2
+ MemberAccountPort,
3
+ MemberAccountSnapshot,
4
+ MemberBenefitsPort,
5
+ MemberBenefitsSnapshot,
6
+ MemberCardPort,
7
+ MemberCardSnapshot,
8
+ MemberCouponPort,
9
+ MemberCouponSnapshot,
10
+ MemberEnrollmentPort,
11
+ MemberProfilePort,
12
+ MemberProfileSnapshot,
13
+ Points,
14
+ PointUsageRecord,
15
+ } from '@tbox.cn/app-contracts-mall';
2
16
  import type { DiscountRule } from '@tbox.cn/app-contracts-mall';
3
17
 
4
18
  /**
5
- * MemberService:商圈会员完整实现。
19
+ * MemberService:商圈会员完整实现(011 K2:6 Port 全量)。
6
20
  * extends 契约 Port(保证满足消费方子集约束);完整接口只在本模块包内。
7
21
  */
8
- export interface MemberService extends MemberQueryPort, MemberSettlementPort {
22
+ export interface MemberService
23
+ extends MemberAccountPort,
24
+ MemberProfilePort,
25
+ MemberCardPort,
26
+ MemberBenefitsPort,
27
+ MemberCouponPort,
28
+ MemberEnrollmentPort {
9
29
  /** 积分变动记录(单测/观测用) */
10
30
  audit: { action: string; userId: string; amount: number; key?: string }[];
11
31
  }
12
32
 
13
- /** 内存实现(PoC */
33
+ /** 内存实现(PoC;真实数据源 = joycity-membership provider 接入点,二期) */
14
34
  export class InMemoryMemberService implements MemberService {
15
35
  private balances = new Map<string, number>();
16
36
  private levels = new Map<string, 'silver' | 'gold' | 'platinum'>();
17
37
  private idempotentKeys = new Set<string>();
38
+ private usages: PointUsageRecord[] = [];
39
+ private profiles = new Map<string, MemberProfileSnapshot>();
18
40
  audit: { action: string; userId: string; amount: number; key?: string }[] = [];
19
41
 
20
42
  constructor(initial: Record<string, number> = {}) {
@@ -23,6 +45,42 @@ export class InMemoryMemberService implements MemberService {
23
45
  }
24
46
  }
25
47
 
48
+ // ===== 1. MemberAccountPort =====
49
+
50
+ async getAccount(userId: string): Promise<MemberAccountSnapshot> {
51
+ return {
52
+ state: this.balances.has(userId) ? 'active' : 'not_enrolled',
53
+ ...(this.balances.has(userId) ? { memberRef: `member:${userId}` } : {}),
54
+ observedAt: new Date().toISOString(),
55
+ };
56
+ }
57
+
58
+ // ===== 2. MemberProfilePort =====
59
+
60
+ async getProfile(userId: string): Promise<MemberProfileSnapshot> {
61
+ return (
62
+ this.profiles.get(userId) ?? {
63
+ labels: [],
64
+ observedAt: new Date().toISOString(),
65
+ }
66
+ );
67
+ }
68
+
69
+ // ===== 3. MemberCardPort =====
70
+
71
+ async getCard(userId: string): Promise<MemberCardSnapshot | null> {
72
+ if (!this.balances.has(userId)) return null;
73
+ return {
74
+ cardRef: `card:${userId}`,
75
+ cardDisplay: '大悦城会员卡',
76
+ status: 'active',
77
+ levelName: this.levelOf(userId),
78
+ observedAt: new Date().toISOString(),
79
+ };
80
+ }
81
+
82
+ // ===== 4. MemberBenefitsPort =====
83
+
26
84
  async queryPoints(userId: string): Promise<Points> {
27
85
  return {
28
86
  userId,
@@ -31,6 +89,24 @@ export class InMemoryMemberService implements MemberService {
31
89
  };
32
90
  }
33
91
 
92
+ async getBenefits(userId: string): Promise<MemberBenefitsSnapshot> {
93
+ return {
94
+ availablePoints: this.balances.get(userId) ?? 0,
95
+ levelName: this.levelOf(userId),
96
+ observedAt: new Date().toISOString(),
97
+ };
98
+ }
99
+
100
+ async listPointUsage(
101
+ userId: string,
102
+ page: number,
103
+ pageSize: number,
104
+ ): Promise<{ items: ReadonlyArray<PointUsageRecord>; total: number }> {
105
+ const all = this.usages.filter((u) => u.usageRecordRef.startsWith(userId));
106
+ const start = (page - 1) * pageSize;
107
+ return { items: all.slice(start, start + pageSize), total: all.length };
108
+ }
109
+
34
110
  async queryDiscount(userId: string): Promise<DiscountRule | undefined> {
35
111
  const level = this.levelOf(userId);
36
112
  const map: Record<string, DiscountRule> = {
@@ -48,6 +124,12 @@ export class InMemoryMemberService implements MemberService {
48
124
  this.balances.set(userId, current - amount);
49
125
  this.idempotentKeys.add(idempotencyKey);
50
126
  this.audit.push({ action: 'deduct', userId, amount, key: idempotencyKey });
127
+ this.usages.push({
128
+ usageRecordRef: `${userId}:${Date.now()}`,
129
+ title: '积分抵扣',
130
+ amount: `-${amount}`,
131
+ usedAt: new Date().toISOString(),
132
+ });
51
133
  this.maybeUpgrade(userId);
52
134
  return true;
53
135
  }
@@ -58,6 +140,12 @@ export class InMemoryMemberService implements MemberService {
58
140
  this.balances.set(userId, current + amount);
59
141
  this.idempotentKeys.add(`refund:${idempotencyKey}`);
60
142
  this.audit.push({ action: 'refund', userId, amount, key: idempotencyKey });
143
+ this.usages.push({
144
+ usageRecordRef: `${userId}:${Date.now()}`,
145
+ title: '积分退回',
146
+ amount: `+${amount}`,
147
+ usedAt: new Date().toISOString(),
148
+ });
61
149
  return true;
62
150
  }
63
151
 
@@ -65,10 +153,61 @@ export class InMemoryMemberService implements MemberService {
65
153
  const current = this.balances.get(userId) ?? 0;
66
154
  this.balances.set(userId, current + amount);
67
155
  this.audit.push({ action: 'award', userId, amount });
156
+ this.usages.push({
157
+ usageRecordRef: `${userId}:${Date.now()}`,
158
+ title: '消费奖励',
159
+ amount: `+${amount}`,
160
+ usedAt: new Date().toISOString(),
161
+ });
68
162
  this.maybeUpgrade(userId);
69
163
  return true;
70
164
  }
71
165
 
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
+ }
178
+
179
+ async getCoupon(_userId: string, _couponRef: string): Promise<MemberCouponSnapshot | null> {
180
+ return null;
181
+ }
182
+
183
+ // ===== 6. MemberEnrollmentPort =====
184
+
185
+ async enroll(input: { userId: string; name?: string; phone?: string }): Promise<{
186
+ success: boolean;
187
+ memberRef?: string;
188
+ error?: string;
189
+ }> {
190
+ if (!this.balances.has(input.userId)) {
191
+ this.balances.set(input.userId, 0);
192
+ }
193
+ this.profiles.set(input.userId, {
194
+ ...(input.name ? { displayName: input.name } : {}),
195
+ ...(input.phone ? { contactDisplay: input.phone } : {}),
196
+ labels: [{ labelRef: 'member', name: '会员' }],
197
+ observedAt: new Date().toISOString(),
198
+ });
199
+ return { success: true, memberRef: `member:${input.userId}` };
200
+ }
201
+
202
+ async completeAuthorization(input: { userId: string; scopes: readonly string[] }): Promise<{
203
+ success: boolean;
204
+ }> {
205
+ void input;
206
+ return { success: true };
207
+ }
208
+
209
+ // ===== 内部 =====
210
+
72
211
  private levelOf(userId: string): 'silver' | 'gold' | 'platinum' {
73
212
  const cached = this.levels.get(userId);
74
213
  if (cached) return cached;
@@ -83,7 +222,6 @@ export class InMemoryMemberService implements MemberService {
83
222
  const prev = this.levels.get(userId);
84
223
  this.levels.set(userId, newLevel);
85
224
  if (prev && prev !== newLevel) {
86
- // 等级变更事件由 serverModule.register 订阅侧转发(保持 service 与 bus 解耦)
87
225
  this.levelChangeListeners.forEach((fn) => fn({ userId, newLevel }));
88
226
  }
89
227
  }
@@ -1,15 +1,17 @@
1
- import { createTool } from '@tbox.cn/app-sdk/server';
1
+ import { createTool, coerceStringifiedFields } from '@tbox.cn/app-sdk/server';
2
2
  import { z } from 'zod';
3
- import type { MemberQueryPort } from '@tbox.cn/app-contracts-mall';
3
+ import type { MemberBenefitsPort, MemberAccountPort, MemberCardPort, MemberCouponPort } from '@tbox.cn/app-contracts-mall';
4
+ import { nowIso } from './util';
4
5
 
5
- /**
6
- * 积分查询工具(LLM function calling;只读查询)。
7
- * 经 ToolRegistry 注册,LLM 调用后由 cardResolver(allowedTools 绑定)自动出 points 卡。
8
- */
9
- export function createQueryPointsTool(getMember: () => MemberQueryPort | undefined) {
6
+ function coerce<T extends z.ZodTypeAny>(schema: T): T {
7
+ return z.preprocess(coerceStringifiedFields, schema) as unknown as T;
8
+ }
9
+
10
+ /** 积分查询工具(011 K3:出卡 member-point-usage-list——吸收教学 points 卡) */
11
+ export function createQueryPointsTool(getMember: () => MemberBenefitsPort | undefined) {
10
12
  return createTool({
11
13
  id: 'queryPoints',
12
- description: '查询会员当前积分余额与等级',
14
+ description: '查询会员当前积分余额、等级与积分流水',
13
15
  inputSchema: z.object({
14
16
  userId: z.string().describe('会员 userId'),
15
17
  }),
@@ -17,15 +19,138 @@ export function createQueryPointsTool(getMember: () => MemberQueryPort | undefin
17
19
  userId: z.string(),
18
20
  balance: z.number(),
19
21
  level: z.enum(['silver', 'gold', 'platinum']),
22
+ items: z.array(
23
+ z.object({
24
+ usageRecordRef: z.string(),
25
+ title: z.string(),
26
+ amount: z.string(),
27
+ usedAt: z.string().optional(),
28
+ description: z.string().optional(),
29
+ }),
30
+ ),
31
+ total: z.number(),
20
32
  }),
21
- execute: async ({ userId }: { userId: string }): Promise<{
22
- userId: string;
23
- balance: number;
24
- level: 'silver' | 'gold' | 'platinum';
25
- }> => {
33
+ execute: async ({ userId }: { userId: string }) => {
34
+ const member = getMember();
35
+ const points = member
36
+ ? await member.queryPoints(userId)
37
+ : { userId, balance: 0, level: 'silver' as const };
38
+ const usage = member ? await member.listPointUsage(userId, 1, 10) : { items: [], total: 0 };
39
+ return {
40
+ userId,
41
+ balance: points.balance,
42
+ level: points.level,
43
+ items: [...usage.items],
44
+ total: usage.total,
45
+ };
46
+ },
47
+ });
48
+ }
49
+
50
+ /** 会员信息查询 → member-info 卡 */
51
+ export function createQueryMemberInfoTool(
52
+ getMember: () => (MemberAccountPort & { getProfile(userId: string): Promise<unknown> }) | undefined,
53
+ ) {
54
+ return createTool({
55
+ id: 'queryMemberInfo',
56
+ description: '查询会员账户状态与个人资料(是否入会、昵称、联系方式、标签)',
57
+ inputSchema: coerce(z.object({ userId: z.string().describe('会员 userId') })),
58
+ execute: async ({ userId }: { userId: string }) => {
59
+ const member = getMember();
60
+ const account = member ? await member.getAccount(userId) : { state: 'not_enrolled' as const, observedAt: nowIso() };
61
+ const profile = (member ? await member.getProfile(userId) : { labels: [] }) as {
62
+ displayName?: string;
63
+ avatarUrl?: string;
64
+ contactDisplay?: string;
65
+ labels: { labelRef: string; name: string }[];
66
+ };
67
+ return {
68
+ userId,
69
+ state: account.state,
70
+ ...(profile.displayName ? { displayName: profile.displayName } : {}),
71
+ ...(profile.avatarUrl ? { avatarUrl: profile.avatarUrl } : {}),
72
+ ...(profile.contactDisplay ? { contactDisplay: profile.contactDisplay } : {}),
73
+ ...(account.memberRef ? { memberRef: account.memberRef } : {}),
74
+ ...(account.registeredAt ? { registeredAt: account.registeredAt } : {}),
75
+ labels: profile.labels ?? [],
76
+ };
77
+ },
78
+ });
79
+ }
80
+
81
+ /** 会员卡查询 → member-card-summary 卡 */
82
+ export function createQueryMemberCardTool(
83
+ getMember: () => (MemberCardPort & MemberBenefitsPort) | undefined,
84
+ ) {
85
+ return createTool({
86
+ id: 'queryMemberCard',
87
+ description: '查询会员卡信息(卡面、等级、积分摘要)',
88
+ inputSchema: coerce(z.object({ userId: z.string().describe('会员 userId') })),
89
+ execute: async ({ userId }: { userId: string }) => {
90
+ const member = getMember();
91
+ const card = member ? await member.getCard(userId) : null;
92
+ const points = member ? await member.queryPoints(userId) : { balance: 0, level: 'silver' as const };
93
+ return {
94
+ userId,
95
+ cardRef: card?.cardRef ?? `card:${userId}`,
96
+ cardDisplay: card?.cardDisplay ?? '大悦城会员卡',
97
+ status: card?.status ?? 'unknown',
98
+ ...(card?.levelName ? { levelName: card.levelName } : { levelName: points.level }),
99
+ availablePoints: points.balance,
100
+ ...(card?.cardImageUrl ? { cardImageUrl: card.cardImageUrl } : {}),
101
+ };
102
+ },
103
+ });
104
+ }
105
+
106
+ /** 入会表单查询 → member-enrollment-form 卡(011 B3 触发链:出表单卡 → 按钮 → componentAction → dispatcher) */
107
+ export function createQueryEnrollmentFormTool(
108
+ getMember: () => (MemberAccountPort & MemberBenefitsPort) | undefined,
109
+ ) {
110
+ return createTool({
111
+ id: 'queryEnrollmentForm',
112
+ description: '查询会员入会状态并返回入会表单(未入会时用户可提交入会)',
113
+ inputSchema: z.object({ userId: z.string().describe('会员 userId') }),
114
+ execute: async ({ userId }: { userId: string }) => {
115
+ const member = getMember();
116
+ const account = member ? await member.getAccount(userId) : { state: 'not_enrolled' as const };
117
+ return {
118
+ userId,
119
+ state: account.state,
120
+ fields:
121
+ account.state === 'not_enrolled'
122
+ ? [
123
+ { fieldId: 'name', label: '姓名', required: true },
124
+ { fieldId: 'phone', label: '手机号', required: true },
125
+ ]
126
+ : [],
127
+ actionId: 'member.enroll',
128
+ };
129
+ },
130
+ });
131
+ }
132
+
133
+ /** 优惠券列表查询 → member-coupon-list 卡 */
134
+ export function createQueryMemberCouponsTool(getMember: () => MemberCouponPort | undefined) {
135
+ return createTool({
136
+ id: 'queryMemberCoupons',
137
+ description: '查询会员优惠券列表(状态/面额/门槛)',
138
+ inputSchema: coerce(z.object({ userId: z.string().describe('会员 userId') })),
139
+ execute: async ({ userId }: { userId: string }) => {
26
140
  const member = getMember();
27
- if (!member) return { userId, balance: 0, level: 'silver' };
28
- return member.queryPoints(userId);
141
+ const result = member ? await member.listCoupons(userId, 1, 20) : { items: [], total: 0 };
142
+ return {
143
+ userId,
144
+ items: result.items.map((c) => ({
145
+ couponRef: c.couponRef,
146
+ title: c.title,
147
+ type: c.type,
148
+ status: c.status,
149
+ ...(c.benefitText ? { benefitText: c.benefitText } : {}),
150
+ ...(c.thresholdText ? { thresholdText: c.thresholdText } : {}),
151
+ })),
152
+ total: result.total,
153
+ };
29
154
  },
30
155
  });
31
156
  }
@@ -0,0 +1,4 @@
1
+ /** 会员模块工具辅助(011 K2) */
2
+ export function nowIso(): string {
3
+ return new Date().toISOString();
4
+ }
@@ -1,14 +1,22 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "name": "module-member",
4
- "version": "0.1.0",
4
+ "version": "0.2.0",
5
5
  "kind": "business",
6
6
  "risk": { "level": "medium", "writeBoundary": "source" },
7
7
  "distribution": { "defaultMode": "codegen" },
8
8
  "contributes": {
9
9
  "handlers": ["member"],
10
- "tools": ["queryPoints"],
11
- "cards": [{ "cardType": "points", "schemaVersion": 1 }],
10
+ "tools": ["queryPoints", "queryMemberInfo", "queryMemberCard", "queryMemberCoupons", "queryEnrollmentForm"],
11
+ "cards": [
12
+ { "cardType": "member-info", "schemaVersion": 1 },
13
+ { "cardType": "member-card-summary", "schemaVersion": 1 },
14
+ { "cardType": "member-coupon-list", "schemaVersion": 1 },
15
+ { "cardType": "member-coupon-detail", "schemaVersion": 1 },
16
+ { "cardType": "member-point-usage-list", "schemaVersion": 1 },
17
+ { "cardType": "member-enrollment-form", "schemaVersion": 1 },
18
+ { "cardType": "member-enrollment-status", "schemaVersion": 1 }
19
+ ],
12
20
  "routes": [],
13
21
  "pages": [],
14
22
  "tabs": []
@@ -8,7 +8,10 @@ describe('module-member 自注册与积分服务', () => {
8
8
  serverModule.register(ctx);
9
9
 
10
10
  expect(ctx.tools.has('queryPoints')).toBe(true);
11
- expect(ctx.cards.has('points')).toBe(true);
11
+ expect(ctx.cards.has('member-point-usage-list')).toBe(true);
12
+ expect(ctx.cards.has('member-info')).toBe(true);
13
+ expect(ctx.cards.has('member-enrollment-form')).toBe(true);
14
+ expect(ctx.cardActions.get('member.enroll')).toBeDefined();
12
15
  expect(ctx.handlers.has('member')).toBe(true);
13
16
  expect(ctx.resolveService('member')).toBeDefined();
14
17
  });
@@ -1,160 +0,0 @@
1
- // src/server/service.ts
2
- var InMemoryMemberService = class {
3
- balances = /* @__PURE__ */ new Map();
4
- levels = /* @__PURE__ */ new Map();
5
- idempotentKeys = /* @__PURE__ */ new Set();
6
- audit = [];
7
- constructor(initial = {}) {
8
- for (const [userId, balance] of Object.entries(initial)) {
9
- this.balances.set(userId, balance);
10
- }
11
- }
12
- async queryPoints(userId) {
13
- return {
14
- userId,
15
- balance: this.balances.get(userId) ?? 0,
16
- level: this.levelOf(userId)
17
- };
18
- }
19
- async queryDiscount(userId) {
20
- const level = this.levelOf(userId);
21
- const map = {
22
- silver: { level: "silver", discount: 0.95 },
23
- gold: { level: "gold", discount: 0.9 },
24
- platinum: { level: "platinum", discount: 0.85 }
25
- };
26
- return map[level];
27
- }
28
- async deductPoints(userId, amount, idempotencyKey) {
29
- if (this.idempotentKeys.has(idempotencyKey)) return true;
30
- const current = this.balances.get(userId) ?? 0;
31
- if (amount <= 0 || current < amount) return false;
32
- this.balances.set(userId, current - amount);
33
- this.idempotentKeys.add(idempotencyKey);
34
- this.audit.push({ action: "deduct", userId, amount, key: idempotencyKey });
35
- this.maybeUpgrade(userId);
36
- return true;
37
- }
38
- async refundPoints(userId, amount, idempotencyKey) {
39
- if (this.idempotentKeys.has(`refund:${idempotencyKey}`)) return true;
40
- const current = this.balances.get(userId) ?? 0;
41
- this.balances.set(userId, current + amount);
42
- this.idempotentKeys.add(`refund:${idempotencyKey}`);
43
- this.audit.push({ action: "refund", userId, amount, key: idempotencyKey });
44
- return true;
45
- }
46
- async awardPoints(userId, amount) {
47
- const current = this.balances.get(userId) ?? 0;
48
- this.balances.set(userId, current + amount);
49
- this.audit.push({ action: "award", userId, amount });
50
- this.maybeUpgrade(userId);
51
- return true;
52
- }
53
- levelOf(userId) {
54
- const cached = this.levels.get(userId);
55
- if (cached) return cached;
56
- const balance = this.balances.get(userId) ?? 0;
57
- if (balance >= 1e3) return "platinum";
58
- if (balance >= 300) return "gold";
59
- return "silver";
60
- }
61
- maybeUpgrade(userId) {
62
- const newLevel = this.levelOf(userId);
63
- const prev = this.levels.get(userId);
64
- this.levels.set(userId, newLevel);
65
- if (prev && prev !== newLevel) {
66
- this.levelChangeListeners.forEach((fn) => fn({ userId, newLevel }));
67
- }
68
- }
69
- levelChangeListeners = /* @__PURE__ */ new Set();
70
- onLevelChange(fn) {
71
- this.levelChangeListeners.add(fn);
72
- }
73
- };
74
-
75
- // src/server/cards/points/meta.ts
76
- import { z } from "zod";
77
- var dataSchema = z.object({
78
- userId: z.string(),
79
- balance: z.number(),
80
- level: z.enum(["silver", "gold", "platinum"])
81
- });
82
- var meta = {
83
- cardType: "points",
84
- dataSchema,
85
- allowedTools: ["queryPoints"],
86
- displayName: "\u4F1A\u5458\u79EF\u5206\u5361",
87
- description: "\u5C55\u793A\u4F1A\u5458\u5F53\u524D\u79EF\u5206\u4F59\u989D\u4E0E\u7B49\u7EA7",
88
- sampleData: { userId: "u_1001", balance: 860, level: "gold" },
89
- schemaVersion: 1
90
- };
91
- var meta_default = meta;
92
-
93
- // src/server/tool.ts
94
- import { createTool } from "@tbox.cn/app-sdk/server";
95
- import { z as z2 } from "zod";
96
- function createQueryPointsTool(getMember) {
97
- return createTool({
98
- id: "queryPoints",
99
- description: "\u67E5\u8BE2\u4F1A\u5458\u5F53\u524D\u79EF\u5206\u4F59\u989D\u4E0E\u7B49\u7EA7",
100
- inputSchema: z2.object({
101
- userId: z2.string().describe("\u4F1A\u5458 userId")
102
- }),
103
- outputSchema: z2.object({
104
- userId: z2.string(),
105
- balance: z2.number(),
106
- level: z2.enum(["silver", "gold", "platinum"])
107
- }),
108
- execute: async ({ userId }) => {
109
- const member2 = getMember();
110
- if (!member2) return { userId, balance: 0, level: "silver" };
111
- return member2.queryPoints(userId);
112
- }
113
- });
114
- }
115
-
116
- // src/server/handler.ts
117
- function createMemberHandler(ctx) {
118
- return {
119
- id: "member",
120
- handle: async (_action, payload) => {
121
- const member2 = ctx.resolveService("member");
122
- if (!member2) return { cards: [], text: "\u4F1A\u5458\u670D\u52A1\u4E0D\u53EF\u7528" };
123
- const { userId } = payload ?? {};
124
- if (!userId) return { cards: [], text: "\u7F3A\u5C11 userId" };
125
- const points = await member2.queryPoints(userId);
126
- return {
127
- cards: [{ cardType: "points", data: points }],
128
- text: `\u5F53\u524D\u79EF\u5206 ${points.balance}\uFF0C\u7B49\u7EA7 ${points.level}`
129
- };
130
- }
131
- };
132
- }
133
-
134
- // src/server/index.ts
135
- var member = new InMemoryMemberService({ u_1001: 860, u_1002: 120 });
136
- var cards = {
137
- points: meta_default
138
- };
139
- var serverModule = {
140
- cards,
141
- register(ctx) {
142
- ctx.services.register("member", member);
143
- ctx.tools.register(createQueryPointsTool(() => ctx.resolveService("member")));
144
- ctx.cards.registerMap(cards);
145
- ctx.handlers.register(createMemberHandler(ctx));
146
- const mallBus = ctx.bus;
147
- mallBus.subscribe("parking.coupon.used", async ({ userId, amount }) => {
148
- await member.awardPoints(userId, Math.floor(amount / 10));
149
- });
150
- member.onLevelChange(({ userId, newLevel }) => {
151
- void mallBus.publish("member.level.changed", { userId, newLevel });
152
- });
153
- }
154
- };
155
-
156
- export {
157
- InMemoryMemberService,
158
- meta_default,
159
- serverModule
160
- };
@@ -1,33 +0,0 @@
1
- // src/client/cards/points/index.tsx
2
- import { jsx, jsxs } from "react/jsx-runtime";
3
- function PointsCard({ data }) {
4
- const levelLabel = {
5
- silver: "\u94F6\u5361",
6
- gold: "\u91D1\u5361",
7
- platinum: "\u94C2\u91D1\u5361"
8
- };
9
- return /* @__PURE__ */ jsxs("div", { className: "mall-card mall-card--points", children: [
10
- /* @__PURE__ */ jsx("div", { className: "mall-card__title", children: "\u4F1A\u5458\u79EF\u5206" }),
11
- /* @__PURE__ */ jsx("div", { className: "mall-card__balance", children: data.balance }),
12
- /* @__PURE__ */ jsxs("div", { className: "mall-card__meta", children: [
13
- levelLabel[data.level],
14
- " \xB7 \u7528\u6237 ",
15
- data.userId
16
- ] })
17
- ] });
18
- }
19
-
20
- // src/client/index.ts
21
- var cards = {
22
- points: PointsCard
23
- };
24
- var clientModule = {
25
- cards,
26
- register(ctx) {
27
- ctx.cards.registerMap(cards);
28
- }
29
- };
30
-
31
- export {
32
- clientModule
33
- };
@@ -1,19 +0,0 @@
1
- import type { PointsCardData } from '../../../server/cards/points/meta';
2
-
3
- /** 会员积分卡组件 */
4
- export function PointsCard({ data }: { data: PointsCardData; cardId: string; isHistory: boolean }) {
5
- const levelLabel: Record<PointsCardData['level'], string> = {
6
- silver: '银卡',
7
- gold: '金卡',
8
- platinum: '铂金卡',
9
- };
10
- return (
11
- <div className="mall-card mall-card--points">
12
- <div className="mall-card__title">会员积分</div>
13
- <div className="mall-card__balance">{data.balance}</div>
14
- <div className="mall-card__meta">
15
- {levelLabel[data.level]} · 用户 {data.userId}
16
- </div>
17
- </div>
18
- );
19
- }
@@ -1,22 +0,0 @@
1
- import { z } from 'zod';
2
- import type { CardMeta } from '@tbox.cn/app-contracts';
3
-
4
- export const dataSchema = z.object({
5
- userId: z.string(),
6
- balance: z.number(),
7
- level: z.enum(['silver', 'gold', 'platinum']),
8
- });
9
-
10
- export type PointsCardData = z.infer<typeof dataSchema>;
11
-
12
- const meta: CardMeta<typeof dataSchema> = {
13
- cardType: 'points',
14
- dataSchema,
15
- allowedTools: ['queryPoints'],
16
- displayName: '会员积分卡',
17
- description: '展示会员当前积分余额与等级',
18
- sampleData: { userId: 'u_1001', balance: 860, level: 'gold' },
19
- schemaVersion: 1,
20
- };
21
-
22
- export default meta;