@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.
- package/dist/chunk-JGZGLFO6.js +111 -0
- package/dist/chunk-ZV6B36AO.js +613 -0
- package/dist/client/index.d.ts +11 -2
- package/dist/client/index.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -4
- package/dist/server/index.d.ts +539 -23
- package/dist/server/index.js +3 -3
- package/package.json +3 -3
- package/src/client/cards/index.tsx +135 -0
- package/src/client/index.ts +19 -4
- package/src/server/cards/index.ts +31 -0
- package/src/server/cards/member-card-summary/meta.ts +21 -0
- package/src/server/cards/member-coupon-detail/meta.ts +19 -0
- package/src/server/cards/member-coupon-list/meta.ts +18 -0
- package/src/server/cards/member-enrollment-form/meta.ts +22 -0
- package/src/server/cards/member-enrollment-status/meta.ts +19 -0
- package/src/server/cards/member-info/meta.ts +19 -0
- package/src/server/cards/member-point-usage-list/meta.ts +20 -0
- package/src/server/handler.ts +125 -11
- package/src/server/index.ts +62 -9
- package/src/server/service.ts +143 -5
- package/src/server/tool.ts +140 -15
- package/src/server/util.ts +4 -0
- package/tbox.component.json +11 -3
- package/tests/service.test.ts +4 -1
- package/dist/chunk-F6FHBH3N.js +0 -160
- package/dist/chunk-OXXT6F5B.js +0 -33
- package/src/client/cards/points/index.tsx +0 -19
- package/src/server/cards/points/meta.ts +0 -22
package/src/server/service.ts
CHANGED
|
@@ -1,20 +1,42 @@
|
|
|
1
|
-
import type {
|
|
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
|
|
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
|
}
|
package/src/server/tool.ts
CHANGED
|
@@ -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 {
|
|
3
|
+
import type { MemberBenefitsPort, MemberAccountPort, MemberCardPort, MemberCouponPort } from '@tbox.cn/app-contracts-mall';
|
|
4
|
+
import { nowIso } from './util';
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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 })
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
28
|
-
return
|
|
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
|
}
|
package/tbox.component.json
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"name": "module-member",
|
|
4
|
-
"version": "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": [
|
|
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": []
|
package/tests/service.test.ts
CHANGED
|
@@ -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('
|
|
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
|
});
|
package/dist/chunk-F6FHBH3N.js
DELETED
|
@@ -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
|
-
};
|
package/dist/chunk-OXXT6F5B.js
DELETED
|
@@ -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;
|