@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
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* module-member 客户端卡片渲染(011 K2:7 卡)。
|
|
3
|
+
* 只消费 SDK 基元(CardFrame/ValueText/StatusPill)+ `--tbox-*` tokens。
|
|
4
|
+
*/
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import { CardFrame, ValueText, StatusPill } from '@tbox.cn/app-sdk/client';
|
|
7
|
+
import type {
|
|
8
|
+
MemberInfoCardData,
|
|
9
|
+
MemberCardSummaryCardData,
|
|
10
|
+
MemberCouponListCardData,
|
|
11
|
+
MemberCouponDetailCardData,
|
|
12
|
+
MemberPointUsageListCardData,
|
|
13
|
+
MemberEnrollmentFormCardData,
|
|
14
|
+
MemberEnrollmentStatusCardData,
|
|
15
|
+
} from '@tbox.cn/app-contracts-mall';
|
|
16
|
+
|
|
17
|
+
const itemStyle: React.CSSProperties = {
|
|
18
|
+
padding: '8px 0',
|
|
19
|
+
borderBottom: '1px solid var(--tbox-divider, #f0f0f0)',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function MemberInfoCard(props: { data: MemberInfoCardData }) {
|
|
23
|
+
const { data } = props;
|
|
24
|
+
return (
|
|
25
|
+
<CardFrame title="会员信息">
|
|
26
|
+
<StatusPill tone={data.state === 'active' ? 'success' : 'neutral'} text={data.state === 'active' ? '已入会' : '未入会'} />
|
|
27
|
+
{data.displayName && <ValueText label="昵称" value={data.displayName} />}
|
|
28
|
+
{data.contactDisplay && <ValueText label="联系方式" value={data.contactDisplay} />}
|
|
29
|
+
{data.memberRef && <ValueText label="会员编号" value={data.memberRef} />}
|
|
30
|
+
{data.labels.length > 0 && <ValueText label="标签" value={data.labels.map((l) => l.name).join(' / ')} />}
|
|
31
|
+
</CardFrame>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function MemberCardSummaryCard(props: { data: MemberCardSummaryCardData }) {
|
|
36
|
+
const { data } = props;
|
|
37
|
+
return (
|
|
38
|
+
<CardFrame title={data.cardDisplay}>
|
|
39
|
+
<StatusPill tone={data.status === 'active' ? 'success' : 'warning'} text={data.status === 'active' ? '正常' : data.status} />
|
|
40
|
+
{data.levelName && <ValueText label="等级" value={data.levelName} />}
|
|
41
|
+
<ValueText label="可用积分" value={String(data.availablePoints)} />
|
|
42
|
+
</CardFrame>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function MemberCouponListCard(props: { data: MemberCouponListCardData }) {
|
|
47
|
+
const { data } = props;
|
|
48
|
+
if (data.items.length === 0) return <CardFrame title="优惠券">暂无优惠券</CardFrame>;
|
|
49
|
+
return (
|
|
50
|
+
<CardFrame title="优惠券" subtitle={`共 ${data.total} 张`}>
|
|
51
|
+
{data.items.map((c) => (
|
|
52
|
+
<div key={c.couponRef} style={itemStyle}>
|
|
53
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 8 }}>
|
|
54
|
+
<span style={{ fontSize: 14, fontWeight: 600 }}>{c.title}</span>
|
|
55
|
+
<StatusPill
|
|
56
|
+
tone={c.status === 'effective' ? 'success' : 'neutral'}
|
|
57
|
+
text={c.status === 'effective' ? '可使用' : c.status}
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
{c.benefitText && <div style={{ fontSize: 12, color: 'var(--tbox-primary, #e93b3d)' }}>{c.benefitText}</div>}
|
|
61
|
+
{c.thresholdText && <div style={{ fontSize: 12, color: 'var(--tbox-text-secondary, #888)' }}>{c.thresholdText}</div>}
|
|
62
|
+
</div>
|
|
63
|
+
))}
|
|
64
|
+
</CardFrame>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function MemberCouponDetailCard(props: { data: MemberCouponDetailCardData }) {
|
|
69
|
+
const { data } = props;
|
|
70
|
+
return (
|
|
71
|
+
<CardFrame title={data.title}>
|
|
72
|
+
<StatusPill tone={data.status === 'effective' ? 'success' : 'neutral'} text={data.status} />
|
|
73
|
+
{data.benefitText && <ValueText label="优惠" value={data.benefitText} />}
|
|
74
|
+
{data.thresholdText && <ValueText label="门槛" value={data.thresholdText} />}
|
|
75
|
+
{data.description && <div style={{ fontSize: 13, marginTop: 4 }}>{data.description}</div>}
|
|
76
|
+
</CardFrame>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function MemberPointUsageListCard(props: { data: MemberPointUsageListCardData }) {
|
|
81
|
+
const { data } = props;
|
|
82
|
+
return (
|
|
83
|
+
<CardFrame title="积分余额与流水" subtitle={`等级 ${data.level}`}>
|
|
84
|
+
<ValueText label="当前积分" value={String(data.balance)} />
|
|
85
|
+
{data.items.length > 0 && (
|
|
86
|
+
<div style={{ marginTop: 8 }}>
|
|
87
|
+
{data.items.map((u) => (
|
|
88
|
+
<div key={u.usageRecordRef} style={{ display: 'flex', justifyContent: 'space-between', padding: '4px 0' }}>
|
|
89
|
+
<span style={{ fontSize: 13 }}>{u.title}</span>
|
|
90
|
+
<span
|
|
91
|
+
style={{
|
|
92
|
+
fontSize: 13,
|
|
93
|
+
color: u.amount.startsWith('-') ? 'var(--tbox-primary, #e93b3d)' : 'var(--tbox-success, #0f8a5f)',
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
{u.amount}
|
|
97
|
+
</span>
|
|
98
|
+
</div>
|
|
99
|
+
))}
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
</CardFrame>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function MemberEnrollmentFormCard(props: { data: MemberEnrollmentFormCardData }) {
|
|
107
|
+
const { data } = props;
|
|
108
|
+
if (data.state === 'active') {
|
|
109
|
+
return (
|
|
110
|
+
<CardFrame title="入会">
|
|
111
|
+
<StatusPill tone="success" text="已入会" />
|
|
112
|
+
</CardFrame>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
return (
|
|
116
|
+
<CardFrame title="会员入会" subtitle="填写信息成为会员">
|
|
117
|
+
{/* 011 评审 M1:表单输入为二期;当前最小闭环 = 按钮以会话身份入会(字段仅展示) */}
|
|
118
|
+
<div style={{ fontSize: 13, color: 'var(--tbox-text-secondary, #888)', marginBottom: 8 }}>
|
|
119
|
+
入会需要{data.fields.filter((f) => f.required).map((f) => f.label).join('、')}
|
|
120
|
+
,当前以会话身份一键入会(表单填写二期开放)。
|
|
121
|
+
</div>
|
|
122
|
+
</CardFrame>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function MemberEnrollmentStatusCard(props: { data: MemberEnrollmentStatusCardData }) {
|
|
127
|
+
const { data } = props;
|
|
128
|
+
return (
|
|
129
|
+
<CardFrame title="入会结果">
|
|
130
|
+
<StatusPill tone={data.success ? 'success' : 'warning'} text={data.success ? '入会成功' : '入会失败'} />
|
|
131
|
+
{data.memberRef && <ValueText label="会员编号" value={data.memberRef} />}
|
|
132
|
+
{data.message && <div style={{ fontSize: 13, marginTop: 4 }}>{data.message}</div>}
|
|
133
|
+
</CardFrame>
|
|
134
|
+
);
|
|
135
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* module-member 客户端入口(与服务端 serverModule 对称)。
|
|
3
|
+
*/
|
|
1
4
|
import type { CardComponent } from '@tbox.cn/app-contracts';
|
|
2
5
|
import type { ClientContext, ClientModule } from '@tbox.cn/app-sdk/client';
|
|
3
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
MemberInfoCard,
|
|
8
|
+
MemberCardSummaryCard,
|
|
9
|
+
MemberCouponListCard,
|
|
10
|
+
MemberCouponDetailCard,
|
|
11
|
+
MemberPointUsageListCard,
|
|
12
|
+
MemberEnrollmentFormCard,
|
|
13
|
+
MemberEnrollmentStatusCard,
|
|
14
|
+
} from './cards';
|
|
4
15
|
|
|
5
|
-
/** 客户端卡片组件 map(key 约定 = cardType;CLI sync 装配读取) */
|
|
6
16
|
const cards = {
|
|
7
|
-
|
|
17
|
+
'member-info': MemberInfoCard as CardComponent,
|
|
18
|
+
'member-card-summary': MemberCardSummaryCard as CardComponent,
|
|
19
|
+
'member-coupon-list': MemberCouponListCard as CardComponent,
|
|
20
|
+
'member-coupon-detail': MemberCouponDetailCard as CardComponent,
|
|
21
|
+
'member-point-usage-list': MemberPointUsageListCard as CardComponent,
|
|
22
|
+
'member-enrollment-form': MemberEnrollmentFormCard as CardComponent,
|
|
23
|
+
'member-enrollment-status': MemberEnrollmentStatusCard as CardComponent,
|
|
8
24
|
} as const;
|
|
9
25
|
|
|
10
|
-
/** 模块注册入口:自注册闭环(与服务端 serverModule 对称) */
|
|
11
26
|
export const clientModule = {
|
|
12
27
|
cards,
|
|
13
28
|
register(ctx: ClientContext): void {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会员 7 卡 meta 聚合导出(每卡一个 meta.ts,doctor AST 约定)。
|
|
3
|
+
*/
|
|
4
|
+
import memberInfoMeta from './member-info/meta';
|
|
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
|
+
import memberPointUsageListMeta from './member-point-usage-list/meta';
|
|
9
|
+
import memberEnrollmentFormMeta from './member-enrollment-form/meta';
|
|
10
|
+
import memberEnrollmentStatusMeta from './member-enrollment-status/meta';
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
memberInfoMeta,
|
|
14
|
+
memberCardSummaryMeta,
|
|
15
|
+
memberCouponListMeta,
|
|
16
|
+
memberCouponDetailMeta,
|
|
17
|
+
memberPointUsageListMeta,
|
|
18
|
+
memberEnrollmentFormMeta,
|
|
19
|
+
memberEnrollmentStatusMeta,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** 7 卡 meta map(key 约定 = cardType) */
|
|
23
|
+
export const memberCards = {
|
|
24
|
+
'member-info': memberInfoMeta,
|
|
25
|
+
'member-card-summary': memberCardSummaryMeta,
|
|
26
|
+
'member-coupon-list': memberCouponListMeta,
|
|
27
|
+
'member-coupon-detail': memberCouponDetailMeta,
|
|
28
|
+
'member-point-usage-list': memberPointUsageListMeta,
|
|
29
|
+
'member-enrollment-form': memberEnrollmentFormMeta,
|
|
30
|
+
'member-enrollment-status': memberEnrollmentStatusMeta,
|
|
31
|
+
} as const;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CardMeta } from '@tbox.cn/app-contracts';
|
|
2
|
+
import { memberCardSummaryCardDataSchema } from '@tbox.cn/app-contracts-mall';
|
|
3
|
+
|
|
4
|
+
const meta: CardMeta<typeof memberCardSummaryCardDataSchema> = {
|
|
5
|
+
cardType: 'member-card-summary',
|
|
6
|
+
dataSchema: memberCardSummaryCardDataSchema,
|
|
7
|
+
allowedTools: ['queryMemberCard'],
|
|
8
|
+
displayName: '会员卡面',
|
|
9
|
+
description: '会员卡信息(等级、积分摘要)',
|
|
10
|
+
sampleData: {
|
|
11
|
+
userId: 'u_1001',
|
|
12
|
+
cardRef: 'card:u_1001',
|
|
13
|
+
cardDisplay: '大悦城会员卡',
|
|
14
|
+
status: 'active',
|
|
15
|
+
levelName: 'gold',
|
|
16
|
+
availablePoints: 860,
|
|
17
|
+
},
|
|
18
|
+
schemaVersion: 1,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default meta;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CardMeta } from '@tbox.cn/app-contracts';
|
|
2
|
+
import { memberCouponDetailCardDataSchema } from '@tbox.cn/app-contracts-mall';
|
|
3
|
+
|
|
4
|
+
const meta: CardMeta<typeof memberCouponDetailCardDataSchema> = {
|
|
5
|
+
cardType: 'member-coupon-detail',
|
|
6
|
+
dataSchema: memberCouponDetailCardDataSchema,
|
|
7
|
+
allowedTools: [],
|
|
8
|
+
displayName: '优惠券详情',
|
|
9
|
+
description: '单张优惠券详情(点击列表项查看)',
|
|
10
|
+
sampleData: {
|
|
11
|
+
couponRef: 'c1',
|
|
12
|
+
title: '满 100 减 10',
|
|
13
|
+
type: 'amount',
|
|
14
|
+
status: 'effective',
|
|
15
|
+
},
|
|
16
|
+
schemaVersion: 1,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default meta;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { CardMeta } from '@tbox.cn/app-contracts';
|
|
2
|
+
import { memberCouponListCardDataSchema } from '@tbox.cn/app-contracts-mall';
|
|
3
|
+
|
|
4
|
+
const meta: CardMeta<typeof memberCouponListCardDataSchema> = {
|
|
5
|
+
cardType: 'member-coupon-list',
|
|
6
|
+
dataSchema: memberCouponListCardDataSchema,
|
|
7
|
+
allowedTools: ['queryMemberCoupons'],
|
|
8
|
+
displayName: '优惠券列表',
|
|
9
|
+
description: '会员优惠券(状态/面额/门槛)',
|
|
10
|
+
sampleData: {
|
|
11
|
+
userId: 'u_1001',
|
|
12
|
+
items: [{ couponRef: 'c1', title: '满 100 减 10', type: 'amount', status: 'effective' }],
|
|
13
|
+
total: 1,
|
|
14
|
+
},
|
|
15
|
+
schemaVersion: 1,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default meta;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { CardMeta } from '@tbox.cn/app-contracts';
|
|
2
|
+
import { memberEnrollmentFormCardDataSchema } from '@tbox.cn/app-contracts-mall';
|
|
3
|
+
|
|
4
|
+
const meta: CardMeta<typeof memberEnrollmentFormCardDataSchema> = {
|
|
5
|
+
cardType: 'member-enrollment-form',
|
|
6
|
+
dataSchema: memberEnrollmentFormCardDataSchema,
|
|
7
|
+
allowedTools: ['queryEnrollmentForm'],
|
|
8
|
+
displayName: '入会表单',
|
|
9
|
+
description: '会员入会表单(写动作 member.enroll,grant 闭环)',
|
|
10
|
+
sampleData: {
|
|
11
|
+
userId: 'u_1001',
|
|
12
|
+
state: 'not_enrolled',
|
|
13
|
+
fields: [
|
|
14
|
+
{ fieldId: 'name', label: '姓名', required: true },
|
|
15
|
+
{ fieldId: 'phone', label: '手机号', required: true },
|
|
16
|
+
],
|
|
17
|
+
actionId: 'member.enroll',
|
|
18
|
+
},
|
|
19
|
+
schemaVersion: 1,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default meta;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CardMeta } from '@tbox.cn/app-contracts';
|
|
2
|
+
import { memberEnrollmentStatusCardDataSchema } from '@tbox.cn/app-contracts-mall';
|
|
3
|
+
|
|
4
|
+
const meta: CardMeta<typeof memberEnrollmentStatusCardDataSchema> = {
|
|
5
|
+
cardType: 'member-enrollment-status',
|
|
6
|
+
dataSchema: memberEnrollmentStatusCardDataSchema,
|
|
7
|
+
allowedTools: [],
|
|
8
|
+
displayName: '入会状态',
|
|
9
|
+
description: '入会结果状态(成功/失败 + 会员编号)',
|
|
10
|
+
sampleData: {
|
|
11
|
+
userId: 'u_1001',
|
|
12
|
+
success: true,
|
|
13
|
+
memberRef: 'member:u_1001',
|
|
14
|
+
state: 'active',
|
|
15
|
+
},
|
|
16
|
+
schemaVersion: 1,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default meta;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CardMeta } from '@tbox.cn/app-contracts';
|
|
2
|
+
import { memberInfoCardDataSchema } from '@tbox.cn/app-contracts-mall';
|
|
3
|
+
|
|
4
|
+
const meta: CardMeta<typeof memberInfoCardDataSchema> = {
|
|
5
|
+
cardType: 'member-info',
|
|
6
|
+
dataSchema: memberInfoCardDataSchema,
|
|
7
|
+
allowedTools: ['queryMemberInfo'],
|
|
8
|
+
displayName: '会员信息',
|
|
9
|
+
description: '会员账户状态与个人资料',
|
|
10
|
+
sampleData: {
|
|
11
|
+
userId: 'u_1001',
|
|
12
|
+
state: 'active',
|
|
13
|
+
displayName: '示例会员',
|
|
14
|
+
labels: [{ labelRef: 'member', name: '会员' }],
|
|
15
|
+
},
|
|
16
|
+
schemaVersion: 1,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default meta;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { CardMeta } from '@tbox.cn/app-contracts';
|
|
2
|
+
import { memberPointUsageListCardDataSchema } from '@tbox.cn/app-contracts-mall';
|
|
3
|
+
|
|
4
|
+
const meta: CardMeta<typeof memberPointUsageListCardDataSchema> = {
|
|
5
|
+
cardType: 'member-point-usage-list',
|
|
6
|
+
dataSchema: memberPointUsageListCardDataSchema,
|
|
7
|
+
allowedTools: ['queryPoints'],
|
|
8
|
+
displayName: '积分余额与流水',
|
|
9
|
+
description: '会员积分余额、等级与变动流水(吸收教学 points 卡)',
|
|
10
|
+
sampleData: {
|
|
11
|
+
userId: 'u_1001',
|
|
12
|
+
balance: 860,
|
|
13
|
+
level: 'gold',
|
|
14
|
+
items: [{ usageRecordRef: 'r1', title: '消费奖励', amount: '+100', usedAt: '2026-08-13T00:00:00Z' }],
|
|
15
|
+
total: 1,
|
|
16
|
+
},
|
|
17
|
+
schemaVersion: 1,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default meta;
|
package/src/server/handler.ts
CHANGED
|
@@ -1,23 +1,137 @@
|
|
|
1
|
-
import type { HandlerDefinition } from '@tbox.cn/app-sdk/server';
|
|
2
|
-
import type { MemberQueryPort } from '@tbox.cn/app-contracts-mall';
|
|
3
|
-
import type { ServerContext } from '@tbox.cn/app-sdk/server';
|
|
4
|
-
|
|
5
1
|
/**
|
|
6
|
-
* member handler
|
|
7
|
-
*
|
|
2
|
+
* member handler + 入会写动作(011 K2:enrollment grant 闭环)。
|
|
3
|
+
* - 查积分/信息/卡 → 出对应卡(LLM 路径走同名工具);
|
|
4
|
+
* - 写:member.enroll CardAction(singleUse grant → dispatcher 授权 → execute → 状态卡)。
|
|
5
|
+
* - 011 评审 M1 修复:execute 经 ctx.identity 取 userId(componentAction 无表单通道时
|
|
6
|
+
* 会话身份入会可用),name/phone 读 input(二期表单数据),inputSchema 补齐护栏。
|
|
8
7
|
*/
|
|
8
|
+
import type { HandlerDefinition, ServerContext } from '@tbox.cn/app-sdk/server';
|
|
9
|
+
import type { CardActionResult, RequestContext } from '@tbox.cn/app-contracts';
|
|
10
|
+
import type { MemberBenefitsPort, MemberEnrollmentPort } from '@tbox.cn/app-contracts-mall';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
|
|
9
13
|
export function createMemberHandler(ctx: ServerContext): HandlerDefinition {
|
|
10
14
|
return {
|
|
11
15
|
id: 'member',
|
|
12
|
-
handle: async (
|
|
13
|
-
const member = ctx.resolveService<
|
|
16
|
+
handle: async (action, payload) => {
|
|
17
|
+
const member = ctx.resolveService<MemberBenefitsPort & MemberEnrollmentPort>('member');
|
|
14
18
|
if (!member) return { cards: [], text: '会员服务不可用' };
|
|
15
19
|
const { userId } = (payload ?? {}) as { userId?: string };
|
|
16
|
-
|
|
20
|
+
|
|
21
|
+
switch (action) {
|
|
22
|
+
case 'query-points': {
|
|
23
|
+
if (!userId) return { cards: [], text: '缺少 userId' };
|
|
24
|
+
const points = await member.queryPoints(userId);
|
|
25
|
+
const usage = await member.listPointUsage(userId, 1, 10);
|
|
26
|
+
return {
|
|
27
|
+
cards: [
|
|
28
|
+
{
|
|
29
|
+
cardType: 'member-point-usage-list',
|
|
30
|
+
data: {
|
|
31
|
+
userId,
|
|
32
|
+
balance: points.balance,
|
|
33
|
+
level: points.level,
|
|
34
|
+
items: usage.items,
|
|
35
|
+
total: usage.total,
|
|
36
|
+
} as unknown as Record<string, unknown>,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
text: `当前积分 ${points.balance},等级 ${points.level}`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
default: {
|
|
43
|
+
if (!userId) return { cards: [], text: '缺少 userId' };
|
|
44
|
+
const points = await member.queryPoints(userId);
|
|
45
|
+
return {
|
|
46
|
+
cards: [
|
|
47
|
+
{
|
|
48
|
+
cardType: 'member-point-usage-list',
|
|
49
|
+
data: {
|
|
50
|
+
userId,
|
|
51
|
+
balance: points.balance,
|
|
52
|
+
level: points.level,
|
|
53
|
+
items: [],
|
|
54
|
+
total: 0,
|
|
55
|
+
} as unknown as Record<string, unknown>,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
text: `当前积分 ${points.balance},等级 ${points.level}`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 入会写动作(011 E1/E4):声明式 CardAction + singleUse grant。
|
|
67
|
+
* 011 评审 M1 修复:userId 优先取 ctx.identity(会话身份),input 仅承载二期表单数据;
|
|
68
|
+
* inputSchema 恒存在(空表单也通过——身份入会为最小闭环)。 */
|
|
69
|
+
export const enrollActionInputSchema = z.object({
|
|
70
|
+
userId: z.string().optional(),
|
|
71
|
+
name: z.string().optional(),
|
|
72
|
+
phone: z.string().optional(),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export function createEnrollAction(
|
|
76
|
+
getMember: () => (MemberEnrollmentPort & MemberBenefitsPort) | undefined,
|
|
77
|
+
): { actionId: string; componentId: string; cardType: string; label: string; singleUse: boolean; inputSchema: z.ZodTypeAny; execute(input: unknown, ctx: RequestContext): Promise<CardActionResult> } {
|
|
78
|
+
return {
|
|
79
|
+
actionId: 'member.enroll',
|
|
80
|
+
componentId: 'member',
|
|
81
|
+
cardType: 'member-enrollment-form',
|
|
82
|
+
label: '提交入会',
|
|
83
|
+
singleUse: true,
|
|
84
|
+
inputSchema: enrollActionInputSchema,
|
|
85
|
+
async execute(input, ctx): Promise<CardActionResult> {
|
|
86
|
+
// 011 M1:userId 优先会话身份(dispatcher 传入 ctx;表单无输入通道时身份入会仍闭环)
|
|
87
|
+
const sessionUserId = ctx?.identity?.userId;
|
|
88
|
+
const { userId: inputUserId, name, phone } = (input ?? {}) as {
|
|
89
|
+
userId?: string;
|
|
90
|
+
name?: string;
|
|
91
|
+
phone?: string;
|
|
92
|
+
};
|
|
93
|
+
const userId = sessionUserId ?? inputUserId;
|
|
94
|
+
const member = getMember();
|
|
95
|
+
if (!userId) {
|
|
96
|
+
return {
|
|
97
|
+
card: {
|
|
98
|
+
cardType: 'member-enrollment-status',
|
|
99
|
+
data: { userId: '', success: false, message: '缺少 userId', state: 'not_enrolled' },
|
|
100
|
+
},
|
|
101
|
+
text: '入会失败:缺少身份信息',
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (!member) {
|
|
105
|
+
return {
|
|
106
|
+
card: {
|
|
107
|
+
cardType: 'member-enrollment-status',
|
|
108
|
+
data: { userId, success: false, message: '会员服务不可用', state: 'not_enrolled' },
|
|
109
|
+
},
|
|
110
|
+
text: '入会失败:会员服务不可用',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const result = await member.enroll({ userId, ...(name ? { name } : {}), ...(phone ? { phone } : {}) });
|
|
114
|
+
if (!result.success) {
|
|
115
|
+
return {
|
|
116
|
+
card: {
|
|
117
|
+
cardType: 'member-enrollment-status',
|
|
118
|
+
data: { userId, success: false, message: result.error, state: 'not_enrolled' },
|
|
119
|
+
},
|
|
120
|
+
text: `入会失败:${result.error ?? '未知错误'}`,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
17
123
|
const points = await member.queryPoints(userId);
|
|
18
124
|
return {
|
|
19
|
-
|
|
20
|
-
|
|
125
|
+
card: {
|
|
126
|
+
cardType: 'member-enrollment-status',
|
|
127
|
+
data: {
|
|
128
|
+
userId,
|
|
129
|
+
success: true,
|
|
130
|
+
...(result.memberRef ? { memberRef: result.memberRef } : {}),
|
|
131
|
+
state: 'active',
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
text: `入会成功!当前积分 ${points.balance},等级 ${points.level}`,
|
|
21
135
|
};
|
|
22
136
|
},
|
|
23
137
|
};
|
package/src/server/index.ts
CHANGED
|
@@ -1,33 +1,86 @@
|
|
|
1
|
-
import type { ServerContext, ServerModule, TypedBus } from '@tbox.cn/app-sdk/server';
|
|
1
|
+
import type { ServerContext, ServerModule, TypedBus, PreToolDef } from '@tbox.cn/app-sdk/server';
|
|
2
|
+
import { definePreTool } from '@tbox.cn/app-sdk/server';
|
|
2
3
|
import type { MallEventMap } from '@tbox.cn/app-contracts-mall';
|
|
3
4
|
import { InMemoryMemberService } from './service';
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
5
|
+
import memberInfoMeta from './cards/member-info/meta';
|
|
6
|
+
import memberCardSummaryMeta from './cards/member-card-summary/meta';
|
|
7
|
+
import memberCouponListMeta from './cards/member-coupon-list/meta';
|
|
8
|
+
import memberCouponDetailMeta from './cards/member-coupon-detail/meta';
|
|
9
|
+
import memberPointUsageListMeta from './cards/member-point-usage-list/meta';
|
|
10
|
+
import memberEnrollmentFormMeta from './cards/member-enrollment-form/meta';
|
|
11
|
+
import memberEnrollmentStatusMeta from './cards/member-enrollment-status/meta';
|
|
12
|
+
import {
|
|
13
|
+
createQueryPointsTool,
|
|
14
|
+
createQueryMemberInfoTool,
|
|
15
|
+
createQueryMemberCardTool,
|
|
16
|
+
createQueryMemberCouponsTool,
|
|
17
|
+
createQueryEnrollmentFormTool,
|
|
18
|
+
} from './tool';
|
|
19
|
+
import { createMemberHandler, createEnrollAction } from './handler';
|
|
7
20
|
|
|
8
21
|
export { InMemoryMemberService } from './service';
|
|
9
22
|
export type { MemberService } from './service';
|
|
10
|
-
export {
|
|
23
|
+
export { memberCards } from './cards';
|
|
11
24
|
|
|
12
25
|
/**
|
|
13
26
|
* 模块级单例(B14):实例在模块包加载时创建,serverModule.register 幂等(重复调用共享状态)。
|
|
14
|
-
* 服务实例生命周期 = 模块包加载期,而非 register 调用次数。
|
|
15
27
|
*/
|
|
16
28
|
const member = new InMemoryMemberService({ u_1001: 860, u_1002: 120 });
|
|
17
29
|
|
|
30
|
+
/** 工具实例顶层创建(010:pre-tool 声明引用工具实例 = 单一真源) */
|
|
31
|
+
const queryPointsTool = createQueryPointsTool(() => member);
|
|
32
|
+
const queryMemberInfoTool = createQueryMemberInfoTool(() => member);
|
|
33
|
+
const queryMemberCardTool = createQueryMemberCardTool(() => member);
|
|
34
|
+
const queryMemberCouponsTool = createQueryMemberCouponsTool(() => member);
|
|
35
|
+
const queryEnrollmentFormTool = createQueryEnrollmentFormTool(() => member);
|
|
36
|
+
|
|
18
37
|
/** 卡片 meta 声明 map(key 约定 = meta.cardType;数据 + 类型源) */
|
|
19
38
|
const cards = {
|
|
20
|
-
|
|
39
|
+
'member-info': memberInfoMeta,
|
|
40
|
+
'member-card-summary': memberCardSummaryMeta,
|
|
41
|
+
'member-coupon-list': memberCouponListMeta,
|
|
42
|
+
'member-coupon-detail': memberCouponDetailMeta,
|
|
43
|
+
'member-point-usage-list': memberPointUsageListMeta,
|
|
44
|
+
'member-enrollment-form': memberEnrollmentFormMeta,
|
|
45
|
+
'member-enrollment-status': memberEnrollmentStatusMeta,
|
|
21
46
|
} as const;
|
|
22
47
|
|
|
48
|
+
/** 入会写动作(011 E4:singleUse grant 闭环) */
|
|
49
|
+
const enrollAction = createEnrollAction(() => member);
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 前置工具声明(010):queryPoints 的 userId 可工程化获取(会话身份),
|
|
53
|
+
* 无需模型抽参——always 必选候选 + 身份可用即预执行。
|
|
54
|
+
*/
|
|
55
|
+
const preTools = {
|
|
56
|
+
queryPoints: definePreTool(queryPointsTool, {
|
|
57
|
+
routing: { mode: 'always' },
|
|
58
|
+
decide: ({ identity }) =>
|
|
59
|
+
identity && identity.source !== 'anonymous'
|
|
60
|
+
? { decision: 'run', args: { userId: identity.userId } }
|
|
61
|
+
: { decision: 'skip', reason: '匿名身份不预执行' },
|
|
62
|
+
}),
|
|
63
|
+
} satisfies Record<string, PreToolDef>;
|
|
64
|
+
|
|
23
65
|
/** 模块注册入口:自注册闭环(与客户端 clientModule 对称) */
|
|
24
66
|
export const serverModule = {
|
|
67
|
+
declaration: {
|
|
68
|
+
componentId: 'member',
|
|
69
|
+
features: ['member-profile', 'member-card', 'member-benefits', 'member-points', 'member-coupons', 'member-enrollment'],
|
|
70
|
+
},
|
|
25
71
|
cards,
|
|
72
|
+
preTools,
|
|
26
73
|
register(ctx: ServerContext): void {
|
|
27
74
|
ctx.services.register('member', member);
|
|
28
|
-
ctx.tools.register(
|
|
75
|
+
ctx.tools.register(queryPointsTool);
|
|
76
|
+
ctx.tools.register(queryMemberInfoTool);
|
|
77
|
+
ctx.tools.register(queryMemberCardTool);
|
|
78
|
+
ctx.tools.register(queryMemberCouponsTool);
|
|
79
|
+
ctx.tools.register(queryEnrollmentFormTool);
|
|
29
80
|
ctx.cards.registerMap(cards);
|
|
30
81
|
ctx.handlers.register(createMemberHandler(ctx));
|
|
82
|
+
// 011 E7:入会写动作声明(装配端 dispatcher 收集 + 出卡时签发 token)
|
|
83
|
+
ctx.cardActions.register(enrollAction);
|
|
31
84
|
|
|
32
85
|
// 类型化事件总线:以 MallEventMap 收窄(keyof 约束载荷类型,编译期对齐)
|
|
33
86
|
const mallBus = ctx.bus as unknown as TypedBus<MallEventMap>;
|
|
@@ -37,7 +90,7 @@ export const serverModule = {
|
|
|
37
90
|
await member.awardPoints(userId, Math.floor(amount / 10));
|
|
38
91
|
});
|
|
39
92
|
|
|
40
|
-
// 等级变更对外发布(member
|
|
93
|
+
// 等级变更对外发布(member 发事件,其他模块订阅)
|
|
41
94
|
member.onLevelChange(({ userId, newLevel }) => {
|
|
42
95
|
void mallBus.publish('member.level.changed', { userId, newLevel });
|
|
43
96
|
});
|