@seasonkoh/webaz 0.1.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.
@@ -0,0 +1,217 @@
1
+ /**
2
+ * L2-6 · 通知系统
3
+ *
4
+ * 每次订单状态变更后调用 notifyTransition(),
5
+ * 自动判断通知哪些参与方,写入 notifications 表。
6
+ * PWA 通过 SSE 实时接收;Agent 通过 dcp_notifications 工具轮询。
7
+ */
8
+ import { generateId } from '../../layer0-foundation/L0-1-database/schema.js';
9
+ // ─── Schema 初始化 ────────────────────────────────────────────
10
+ export function initNotificationSchema(db) {
11
+ db.exec(`
12
+ CREATE TABLE IF NOT EXISTS notifications (
13
+ id TEXT PRIMARY KEY,
14
+ user_id TEXT NOT NULL REFERENCES users(id),
15
+ order_id TEXT REFERENCES orders(id),
16
+ type TEXT NOT NULL,
17
+ title TEXT NOT NULL,
18
+ body TEXT NOT NULL,
19
+ read INTEGER DEFAULT 0,
20
+ created_at TEXT DEFAULT (datetime('now'))
21
+ );
22
+ CREATE INDEX IF NOT EXISTS idx_notif_user ON notifications(user_id, read, created_at DESC);
23
+ `);
24
+ }
25
+ // 实时推送回调(由 PWA server 注入,解耦依赖)
26
+ let pushCallback = null;
27
+ export function setPushCallback(cb) {
28
+ pushCallback = cb;
29
+ }
30
+ const RULES = {
31
+ 'created→paid': {
32
+ recipients: ['seller'],
33
+ title: '🛍️ 新订单',
34
+ body: ctx => `${ctx.buyerName} 下单了「${ctx.productTitle}」,金额 ${ctx.totalAmount} DCP。请在 24h 内接单,否则自动退款。`,
35
+ },
36
+ 'paid→accepted': {
37
+ recipients: ['buyer'],
38
+ title: '✅ 卖家已接单',
39
+ body: ctx => `${ctx.sellerName} 已接受你的订单,预计 5 天内发货。`,
40
+ },
41
+ 'paid→cancelled': {
42
+ recipients: ['buyer'],
43
+ title: '❌ 订单已取消',
44
+ body: ctx => `订单「${ctx.productTitle}」已取消,${ctx.totalAmount} DCP 将原路退回。`,
45
+ },
46
+ 'accepted→shipped': {
47
+ recipients: ['buyer'],
48
+ title: '📦 商品已发货',
49
+ body: ctx => `${ctx.sellerName} 已发货,物流 48h 内揽收后你可以追踪包裹。`,
50
+ },
51
+ 'shipped→picked_up': {
52
+ recipients: ['buyer', 'seller'],
53
+ title: '🚚 物流已揽收',
54
+ body: ctx => `包裹已由${ctx.logisticsName ?? '物流方'}揽收,正在运输中。`,
55
+ },
56
+ 'in_transit→delivered': {
57
+ recipients: ['buyer'],
58
+ title: '📬 包裹已投递',
59
+ body: ctx => `你的包裹已送达,请确认收货。72 小时内未确认将自动完成。`,
60
+ },
61
+ 'delivered→confirmed': {
62
+ recipients: ['seller'],
63
+ title: '💰 买家确认收货',
64
+ body: ctx => `${ctx.buyerName} 已确认收货,${ctx.totalAmount} DCP 结算中。`,
65
+ },
66
+ 'confirmed→completed': {
67
+ recipients: ['seller'],
68
+ title: '✅ 交易完成,资金到账',
69
+ body: ctx => `订单「${ctx.productTitle}」交易完成,收益已入账,查看钱包确认。`,
70
+ },
71
+ 'paid→disputed': {
72
+ recipients: ['seller'],
73
+ title: '⚠️ 买家发起争议',
74
+ body: ctx => `${ctx.buyerName} 对订单「${ctx.productTitle}」发起了争议。请在 48 小时内提交反驳证据,否则协议自动裁定退款。`,
75
+ },
76
+ 'accepted→disputed': {
77
+ recipients: ['seller'],
78
+ title: '⚠️ 买家发起争议',
79
+ body: ctx => `${ctx.buyerName} 对订单「${ctx.productTitle}」发起了争议,请在 48h 内回应。`,
80
+ },
81
+ 'shipped→disputed': {
82
+ recipients: ['seller', 'logistics'],
83
+ title: '⚠️ 发生争议',
84
+ body: ctx => `订单「${ctx.productTitle}」出现争议,请提交相关证据。`,
85
+ },
86
+ 'in_transit→disputed': {
87
+ recipients: ['seller', 'logistics'],
88
+ title: '⚠️ 运输中发生争议',
89
+ body: ctx => `订单「${ctx.productTitle}」运输过程中发生争议,请及时回应。`,
90
+ },
91
+ 'delivered→disputed': {
92
+ recipients: ['seller'],
93
+ title: '⚠️ 买家对收货发起争议',
94
+ body: ctx => `${ctx.buyerName} 声称货物有问题,已发起争议。请在 48h 内提交证据。`,
95
+ },
96
+ 'disputed→completed': {
97
+ recipients: ['buyer', 'seller'],
98
+ title: '⚖️ 争议裁定:卖家胜诉',
99
+ body: ctx => `订单「${ctx.productTitle}」争议已裁定,资金已释放给卖家。`,
100
+ },
101
+ 'disputed→cancelled': {
102
+ recipients: ['buyer', 'seller'],
103
+ title: '⚖️ 争议裁定:退款买家',
104
+ body: ctx => `订单「${ctx.productTitle}」争议已裁定,${ctx.totalAmount} DCP 已退回买家。`,
105
+ },
106
+ 'paid→fault_seller': {
107
+ recipients: ['buyer', 'seller'],
108
+ title: '⏰ 卖家超时违约',
109
+ body: ctx => `卖家超时未接单,订单已自动取消,${ctx.totalAmount} DCP 退款处理中。`,
110
+ },
111
+ 'accepted→fault_seller': {
112
+ recipients: ['buyer', 'seller'],
113
+ title: '⏰ 卖家超时未发货',
114
+ body: ctx => `卖家超时未发货,订单已判违约,资金退回。`,
115
+ },
116
+ 'in_transit→fault_logistics': {
117
+ recipients: ['buyer', 'seller'],
118
+ title: '⏰ 物流超时',
119
+ body: ctx => `物流方超时未完成投递,已自动记录违约。`,
120
+ },
121
+ };
122
+ // ─── 主入口:状态变更后调用 ───────────────────────────────────
123
+ export function notifyTransition(db, orderId, fromStatus, toStatus) {
124
+ const rule = RULES[`${fromStatus}→${toStatus}`];
125
+ if (!rule)
126
+ return; // 没有规则的转移不发通知
127
+ // 查询订单上下文
128
+ const ctx = getOrderCtx(db, orderId);
129
+ if (!ctx)
130
+ return;
131
+ const title = rule.title;
132
+ const body = rule.body(ctx);
133
+ const type = `${fromStatus}→${toStatus}`;
134
+ // 确定收件人 ID 列表
135
+ const recipientIds = resolveRecipients(db, rule.recipients, ctx, orderId);
136
+ for (const userId of recipientIds) {
137
+ createNotification(db, userId, orderId, type, title, body);
138
+ }
139
+ }
140
+ // ─── 工具函数 ─────────────────────────────────────────────────
141
+ function getOrderCtx(db, orderId) {
142
+ const row = db.prepare(`
143
+ SELECT o.buyer_id, o.seller_id, o.logistics_id, o.total_amount,
144
+ ub.name as buyer_name, us.name as seller_name,
145
+ ul.name as logistics_name, p.title as product_title
146
+ FROM orders o
147
+ JOIN users ub ON o.buyer_id = ub.id
148
+ JOIN users us ON o.seller_id = us.id
149
+ LEFT JOIN users ul ON o.logistics_id = ul.id
150
+ LEFT JOIN products p ON o.product_id = p.id
151
+ WHERE o.id = ?
152
+ `).get(orderId);
153
+ if (!row)
154
+ return null;
155
+ return {
156
+ orderId,
157
+ buyerName: row.buyer_name,
158
+ sellerName: row.seller_name,
159
+ logisticsName: row.logistics_name,
160
+ productTitle: row.product_title,
161
+ totalAmount: row.total_amount,
162
+ };
163
+ }
164
+ function resolveRecipients(db, roles, ctx, orderId) {
165
+ const ids = new Set();
166
+ const order = db.prepare('SELECT buyer_id, seller_id, logistics_id FROM orders WHERE id = ?').get(orderId);
167
+ for (const role of roles) {
168
+ if (role === 'buyer' && order.buyer_id)
169
+ ids.add(order.buyer_id);
170
+ if (role === 'seller' && order.seller_id)
171
+ ids.add(order.seller_id);
172
+ if (role === 'logistics' && order.logistics_id)
173
+ ids.add(order.logistics_id);
174
+ if (role === 'arbitrators') {
175
+ const arbs = db.prepare("SELECT id FROM users WHERE role = 'arbitrator'").all();
176
+ arbs.forEach(a => ids.add(a.id));
177
+ }
178
+ }
179
+ return [...ids];
180
+ }
181
+ export function createNotification(db, userId, orderId, type, title, body) {
182
+ const notif = {
183
+ id: generateId('ntf'),
184
+ user_id: userId,
185
+ order_id: orderId,
186
+ type,
187
+ title,
188
+ body,
189
+ read: 0,
190
+ created_at: new Date().toISOString(),
191
+ };
192
+ db.prepare(`
193
+ INSERT INTO notifications (id, user_id, order_id, type, title, body)
194
+ VALUES (?, ?, ?, ?, ?, ?)
195
+ `).run(notif.id, userId, orderId, type, title, body);
196
+ // 实时推送(如果 PWA SSE 连接在线)
197
+ pushCallback?.(userId, notif);
198
+ return notif;
199
+ }
200
+ // ─── 查询 ─────────────────────────────────────────────────────
201
+ export function getNotifications(db, userId, onlyUnread = false, limit = 30) {
202
+ const sql = `SELECT * FROM notifications WHERE user_id = ?${onlyUnread ? ' AND read = 0' : ''}
203
+ ORDER BY created_at DESC LIMIT ?`;
204
+ return db.prepare(sql).all(userId, limit);
205
+ }
206
+ export function getUnreadCount(db, userId) {
207
+ const row = db.prepare('SELECT COUNT(*) as n FROM notifications WHERE user_id = ? AND read = 0').get(userId);
208
+ return row.n;
209
+ }
210
+ export function markRead(db, userId, notifId) {
211
+ if (notifId) {
212
+ db.prepare('UPDATE notifications SET read = 1 WHERE id = ? AND user_id = ?').run(notifId, userId);
213
+ }
214
+ else {
215
+ db.prepare('UPDATE notifications SET read = 1 WHERE user_id = ?').run(userId);
216
+ }
217
+ }