@seasonkoh/webaz 0.1.7 → 0.1.9
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/LICENSE +48 -0
- package/README.md +156 -20
- package/dist/layer0-foundation/L0-1-database/schema.js +5 -4
- package/dist/layer0-foundation/L0-2-state-machine/engine.js +228 -7
- package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +156 -0
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +53 -12
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +14 -1
- package/dist/layer1-agent/L1-1-mcp-server/auth.js +1 -1
- package/dist/layer1-agent/L1-1-mcp-server/server.js +3691 -714
- package/dist/layer1-agent/L1-2-external-anchor/anchor-engine.js +324 -0
- package/dist/layer1-agent/L1-2-identity/agent-passport.js +100 -0
- package/dist/layer2-business/L2-6-notifications/notification-engine.js +72 -5
- package/dist/layer2-business/L2-7-snf/snf-engine.js +287 -0
- package/dist/layer2-business/L2-anchor-registry/anchor-registry.js +396 -0
- package/dist/layer2-business/L2-notes/note-photo-storage.js +133 -0
- package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +6 -6
- package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +246 -0
- package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +95 -1
- package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +31 -2
- package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +358 -0
- package/dist/pwa/public/app.js +31947 -0
- package/dist/pwa/public/i18n.js +5751 -0
- package/dist/pwa/public/icon.svg +11 -0
- package/dist/pwa/public/index.html +21 -0
- package/dist/pwa/public/manifest.json +48 -0
- package/dist/pwa/public/openapi.json +5946 -0
- package/dist/pwa/public/style.css +535 -0
- package/dist/pwa/public/sw.js +63 -0
- package/dist/pwa/public/vendor/jsQR.js +10102 -0
- package/dist/pwa/public/webaz-logo.png +0 -0
- package/dist/pwa/routes/account-deletion.js +53 -0
- package/dist/pwa/routes/addresses.js +105 -0
- package/dist/pwa/routes/admin-admins.js +151 -0
- package/dist/pwa/routes/admin-analytics.js +253 -0
- package/dist/pwa/routes/admin-atomic.js +21 -0
- package/dist/pwa/routes/admin-catalog.js +64 -0
- package/dist/pwa/routes/admin-editor-picks.js +45 -0
- package/dist/pwa/routes/admin-events.js +60 -0
- package/dist/pwa/routes/admin-health.js +66 -0
- package/dist/pwa/routes/admin-moderation.js +120 -0
- package/dist/pwa/routes/admin-ops.js +179 -0
- package/dist/pwa/routes/admin-protocol-params.js +79 -0
- package/dist/pwa/routes/admin-reports.js +154 -0
- package/dist/pwa/routes/admin-tokenomics.js +113 -0
- package/dist/pwa/routes/admin-users-lifecycle.js +237 -0
- package/dist/pwa/routes/admin-users-query.js +390 -0
- package/dist/pwa/routes/admin-verifier-flow.js +126 -0
- package/dist/pwa/routes/admin-verifier-whitelist.js +111 -0
- package/dist/pwa/routes/admin-wallet-ops.js +66 -0
- package/dist/pwa/routes/agent-buy.js +215 -0
- package/dist/pwa/routes/agent-governance.js +341 -0
- package/dist/pwa/routes/agent-reputation.js +34 -0
- package/dist/pwa/routes/ai.js +101 -0
- package/dist/pwa/routes/analytics.js +272 -0
- package/dist/pwa/routes/anchors.js +169 -0
- package/dist/pwa/routes/announcements.js +110 -0
- package/dist/pwa/routes/arbitrator.js +117 -0
- package/dist/pwa/routes/auction.js +436 -0
- package/dist/pwa/routes/auth-login.js +40 -0
- package/dist/pwa/routes/auth-read.js +66 -0
- package/dist/pwa/routes/auth-register.js +138 -0
- package/dist/pwa/routes/auth-sessions.js +62 -0
- package/dist/pwa/routes/blocklist.js +60 -0
- package/dist/pwa/routes/buyer-feeds.js +224 -0
- package/dist/pwa/routes/cart.js +155 -0
- package/dist/pwa/routes/charity.js +816 -0
- package/dist/pwa/routes/chat.js +318 -0
- package/dist/pwa/routes/checkin-tasks.js +122 -0
- package/dist/pwa/routes/checkout-helpers.js +85 -0
- package/dist/pwa/routes/claim-initiators.js +88 -0
- package/dist/pwa/routes/claim-verify.js +615 -0
- package/dist/pwa/routes/claim-voting.js +114 -0
- package/dist/pwa/routes/claim-withdrawals.js +20 -0
- package/dist/pwa/routes/coupons.js +165 -0
- package/dist/pwa/routes/dashboards.js +99 -0
- package/dist/pwa/routes/dispute-cases.js +267 -0
- package/dist/pwa/routes/disputes-read.js +358 -0
- package/dist/pwa/routes/disputes-write.js +475 -0
- package/dist/pwa/routes/evidence.js +86 -0
- package/dist/pwa/routes/external-anchors.js +107 -0
- package/dist/pwa/routes/feedback.js +270 -0
- package/dist/pwa/routes/flash-sales.js +130 -0
- package/dist/pwa/routes/follows.js +103 -0
- package/dist/pwa/routes/group-buys.js +208 -0
- package/dist/pwa/routes/growth.js +199 -0
- package/dist/pwa/routes/import-product.js +153 -0
- package/dist/pwa/routes/kyc.js +40 -0
- package/dist/pwa/routes/leaderboard.js +149 -0
- package/dist/pwa/routes/listings.js +281 -0
- package/dist/pwa/routes/logistics.js +35 -0
- package/dist/pwa/routes/manifests.js +126 -0
- package/dist/pwa/routes/me-data.js +101 -0
- package/dist/pwa/routes/notifications.js +48 -0
- package/dist/pwa/routes/offers.js +96 -0
- package/dist/pwa/routes/orders-action.js +285 -0
- package/dist/pwa/routes/orders-create.js +339 -0
- package/dist/pwa/routes/orders-read.js +180 -0
- package/dist/pwa/routes/p2p-products.js +178 -0
- package/dist/pwa/routes/payments-governance.js +311 -0
- package/dist/pwa/routes/peers.js +34 -0
- package/dist/pwa/routes/pin-receipts.js +39 -0
- package/dist/pwa/routes/products-aliases.js +119 -0
- package/dist/pwa/routes/products-claims.js +60 -0
- package/dist/pwa/routes/products-create.js +206 -0
- package/dist/pwa/routes/products-crud.js +73 -0
- package/dist/pwa/routes/products-links.js +129 -0
- package/dist/pwa/routes/products-list.js +424 -0
- package/dist/pwa/routes/products-meta.js +155 -0
- package/dist/pwa/routes/products-update.js +125 -0
- package/dist/pwa/routes/profile-credentials.js +105 -0
- package/dist/pwa/routes/profile-identity.js +174 -0
- package/dist/pwa/routes/profile-location.js +35 -0
- package/dist/pwa/routes/profile-placement.js +70 -0
- package/dist/pwa/routes/profile-prefs.js +93 -0
- package/dist/pwa/routes/promoter.js +208 -0
- package/dist/pwa/routes/public-utils.js +170 -0
- package/dist/pwa/routes/push.js +54 -0
- package/dist/pwa/routes/ratings.js +220 -0
- package/dist/pwa/routes/recover-key.js +100 -0
- package/dist/pwa/routes/referral.js +58 -0
- package/dist/pwa/routes/reputation.js +34 -0
- package/dist/pwa/routes/returns.js +493 -0
- package/dist/pwa/routes/reviews.js +81 -0
- package/dist/pwa/routes/rfqs.js +443 -0
- package/dist/pwa/routes/search.js +172 -0
- package/dist/pwa/routes/secondhand.js +278 -0
- package/dist/pwa/routes/seller-quota.js +225 -0
- package/dist/pwa/routes/share-redirects.js +164 -0
- package/dist/pwa/routes/shareables-interactions.js +212 -0
- package/dist/pwa/routes/shareables.js +470 -0
- package/dist/pwa/routes/shops.js +98 -0
- package/dist/pwa/routes/signaling.js +43 -0
- package/dist/pwa/routes/skill-market.js +173 -0
- package/dist/pwa/routes/skills.js +174 -0
- package/dist/pwa/routes/snf.js +126 -0
- package/dist/pwa/routes/tags.js +47 -0
- package/dist/pwa/routes/trial.js +333 -0
- package/dist/pwa/routes/trusted-kpi.js +87 -0
- package/dist/pwa/routes/url-claim.js +113 -0
- package/dist/pwa/routes/users-public.js +317 -0
- package/dist/pwa/routes/variants.js +156 -0
- package/dist/pwa/routes/verifier-user.js +107 -0
- package/dist/pwa/routes/verify-tasks.js +120 -0
- package/dist/pwa/routes/waitlist.js +65 -0
- package/dist/pwa/routes/wallet-read.js +218 -0
- package/dist/pwa/routes/wallet-write.js +273 -0
- package/dist/pwa/routes/webauthn.js +188 -0
- package/dist/pwa/routes/webhooks.js +162 -0
- package/dist/pwa/routes/welcome.js +226 -0
- package/dist/pwa/routes/wishlist-qa.js +135 -0
- package/dist/pwa/security/ssrf.js +110 -0
- package/dist/pwa/server.js +9679 -698
- package/package.json +11 -4
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export function registerMeDataRoutes(app, deps) {
|
|
2
|
+
const { db, auth } = deps;
|
|
3
|
+
// COP 飞轮: 完成订单 7d 引导发笔记
|
|
4
|
+
app.get('/api/me/note-prompts', (req, res) => {
|
|
5
|
+
const user = auth(req, res);
|
|
6
|
+
if (!user)
|
|
7
|
+
return;
|
|
8
|
+
const rows = db.prepare(`
|
|
9
|
+
SELECT o.id as order_id, o.product_id, o.updated_at as completed_at, o.total_amount,
|
|
10
|
+
p.title as product_title, p.images as product_images
|
|
11
|
+
FROM orders o
|
|
12
|
+
JOIN products p ON p.id = o.product_id
|
|
13
|
+
WHERE o.buyer_id = ?
|
|
14
|
+
AND o.status IN ('confirmed', 'completed')
|
|
15
|
+
AND datetime(o.updated_at) > datetime('now', '-7 days')
|
|
16
|
+
AND NOT EXISTS (
|
|
17
|
+
SELECT 1 FROM shareables s
|
|
18
|
+
WHERE s.owner_id = ? AND s.related_order_id = o.id AND s.type = 'note' AND s.status = 'active'
|
|
19
|
+
)
|
|
20
|
+
ORDER BY o.updated_at DESC
|
|
21
|
+
LIMIT 10
|
|
22
|
+
`).all(user.id, user.id);
|
|
23
|
+
const prompts = rows.map(r => {
|
|
24
|
+
let firstImage = null;
|
|
25
|
+
try {
|
|
26
|
+
const arr = JSON.parse(r.product_images || '[]');
|
|
27
|
+
if (Array.isArray(arr) && arr.length > 0)
|
|
28
|
+
firstImage = String(arr[0]);
|
|
29
|
+
}
|
|
30
|
+
catch { }
|
|
31
|
+
return {
|
|
32
|
+
order_id: r.order_id,
|
|
33
|
+
product_id: r.product_id,
|
|
34
|
+
product_title: r.product_title,
|
|
35
|
+
product_image: firstImage,
|
|
36
|
+
completed_at: r.completed_at,
|
|
37
|
+
total_amount: r.total_amount,
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
res.json({ prompts });
|
|
41
|
+
});
|
|
42
|
+
// COP P0-1: 数据导出(用户主权)
|
|
43
|
+
app.get('/api/me/export', (req, res) => {
|
|
44
|
+
const user = auth(req, res);
|
|
45
|
+
if (!user)
|
|
46
|
+
return;
|
|
47
|
+
const uid = user.id;
|
|
48
|
+
const data = {
|
|
49
|
+
exported_at: new Date().toISOString(),
|
|
50
|
+
user_id: uid,
|
|
51
|
+
notice: 'WebAZ COP 承诺:你的数据属于你。可随时导出,可随时迁出。',
|
|
52
|
+
};
|
|
53
|
+
try {
|
|
54
|
+
data.profile = db.prepare(`SELECT id, name, handle, role, region, bio, search_anchor, email, phone, permanent_code, created_at, reputation FROM users WHERE id = ?`).get(uid);
|
|
55
|
+
data.wallet = db.prepare(`SELECT balance, staked, escrowed, earned FROM wallets WHERE user_id = ?`).get(uid);
|
|
56
|
+
data.orders = db.prepare(`SELECT * FROM orders WHERE buyer_id = ? OR seller_id = ? ORDER BY created_at DESC LIMIT 1000`).all(uid, uid);
|
|
57
|
+
data.shareables = db.prepare(`SELECT * FROM shareables WHERE owner_id = ? AND status != 'removed'`).all(uid);
|
|
58
|
+
data.bookmarks = db.prepare(`SELECT b.*, s.title FROM shareable_bookmarks b LEFT JOIN shareables s ON s.id = b.shareable_id WHERE b.user_id = ?`).all(uid);
|
|
59
|
+
data.likes = db.prepare(`SELECT l.*, s.title FROM shareable_likes l LEFT JOIN shareables s ON s.id = l.shareable_id WHERE l.user_id = ?`).all(uid);
|
|
60
|
+
data.follows_following = db.prepare(`SELECT followee_id, created_at FROM follows WHERE follower_id = ?`).all(uid);
|
|
61
|
+
data.follows_followers = db.prepare(`SELECT follower_id, created_at FROM follows WHERE followee_id = ?`).all(uid);
|
|
62
|
+
data.addresses = db.prepare(`SELECT * FROM user_addresses WHERE user_id = ?`).all(uid);
|
|
63
|
+
data.kyc = db.prepare(`SELECT status, id_type, id_number_last4, submitted_at, reviewed_at FROM kyc_records WHERE user_id = ?`).get(uid);
|
|
64
|
+
// #1017: wallet_history 不存在 — 用 deposits + withdrawals + commissions 复合
|
|
65
|
+
try {
|
|
66
|
+
data.deposits = db.prepare(`SELECT * FROM deposit_txns WHERE user_id = ? ORDER BY created_at DESC LIMIT 200`).all(uid);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
data.deposits = [];
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
data.withdrawals = db.prepare(`SELECT * FROM withdrawal_requests WHERE user_id = ? ORDER BY created_at DESC LIMIT 200`).all(uid);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
data.withdrawals = [];
|
|
76
|
+
}
|
|
77
|
+
data.commissions = db.prepare(`SELECT * FROM commission_records WHERE beneficiary_id = ? ORDER BY created_at DESC LIMIT 500`).all(uid) || [];
|
|
78
|
+
data.anchors = db.prepare(`SELECT anchor, target_kind, target_id, status, created_at FROM anchor_registry WHERE owner_id = ?`).all(uid);
|
|
79
|
+
data.notifications = db.prepare(`SELECT * FROM notifications WHERE user_id = ? ORDER BY created_at DESC LIMIT 500`).all(uid);
|
|
80
|
+
data.error_log = db.prepare(`SELECT id, source, message, created_at FROM error_log WHERE user_id = ? ORDER BY id DESC LIMIT 100`).all(uid) || [];
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
console.warn('[export] partial:', e.message);
|
|
84
|
+
}
|
|
85
|
+
const format = String(req.query.format || 'json').toLowerCase();
|
|
86
|
+
if (format === 'csv') {
|
|
87
|
+
// CSV:只导 orders 主表
|
|
88
|
+
const orders = data.orders;
|
|
89
|
+
if (!orders || orders.length === 0)
|
|
90
|
+
return void res.status(204).end();
|
|
91
|
+
const cols = Object.keys(orders[0]);
|
|
92
|
+
const csv = [cols.join(',')].concat(orders.map(o => cols.map(c => JSON.stringify(o[c] ?? '')).join(','))).join('\n');
|
|
93
|
+
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
|
94
|
+
res.setHeader('Content-Disposition', `attachment; filename="webaz-orders-${uid}-${Date.now()}.csv"`);
|
|
95
|
+
return void res.send(csv);
|
|
96
|
+
}
|
|
97
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
98
|
+
res.setHeader('Content-Disposition', `attachment; filename="webaz-export-${uid}-${Date.now()}.json"`);
|
|
99
|
+
res.json(data);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { getNotifications, getUnreadCount, markRead } from '../../layer2-business/L2-6-notifications/notification-engine.js';
|
|
2
|
+
export function registerNotificationsRoutes(app, deps) {
|
|
3
|
+
const { db, auth, sseClients } = deps;
|
|
4
|
+
// SSE 实时推送流(EventSource 不支持自定义 header,URL ?key= 也兼容)
|
|
5
|
+
app.get('/api/notifications/stream', (req, res) => {
|
|
6
|
+
const key = req.query.key ?? req.headers.authorization?.replace('Bearer ', '');
|
|
7
|
+
const user = key ? db.prepare('SELECT * FROM users WHERE api_key = ?').get(key) : null;
|
|
8
|
+
if (!user)
|
|
9
|
+
return void res.status(401).end();
|
|
10
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
11
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
12
|
+
res.setHeader('Connection', 'keep-alive');
|
|
13
|
+
res.flushHeaders();
|
|
14
|
+
sseClients.set(user.id, res);
|
|
15
|
+
// 连接时推送未读数
|
|
16
|
+
const unread = getUnreadCount(db, user.id);
|
|
17
|
+
res.write(`data: ${JSON.stringify({ type: 'init', unread })}\n\n`);
|
|
18
|
+
// 心跳保活(每 30s)
|
|
19
|
+
const heartbeat = setInterval(() => {
|
|
20
|
+
try {
|
|
21
|
+
res.write(': ping\n\n');
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
clearInterval(heartbeat);
|
|
25
|
+
}
|
|
26
|
+
}, 30_000);
|
|
27
|
+
req.on('close', () => {
|
|
28
|
+
sseClients.delete(user.id);
|
|
29
|
+
clearInterval(heartbeat);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
app.get('/api/notifications', (req, res) => {
|
|
33
|
+
const user = auth(req, res);
|
|
34
|
+
if (!user)
|
|
35
|
+
return;
|
|
36
|
+
const onlyUnread = req.query.unread === '1';
|
|
37
|
+
const notifs = getNotifications(db, user.id, onlyUnread);
|
|
38
|
+
const unread = getUnreadCount(db, user.id);
|
|
39
|
+
res.json({ unread, notifications: notifs });
|
|
40
|
+
});
|
|
41
|
+
app.post('/api/notifications/read', (req, res) => {
|
|
42
|
+
const user = auth(req, res);
|
|
43
|
+
if (!user)
|
|
44
|
+
return;
|
|
45
|
+
markRead(db, user.id, req.body?.id);
|
|
46
|
+
res.json({ success: true });
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export function registerOffersRoutes(app, deps) {
|
|
2
|
+
const { db, auth, VALID_FULFILLMENT_TYPES } = deps;
|
|
3
|
+
app.patch('/api/offers/:id', (req, res) => {
|
|
4
|
+
const user = auth(req, res);
|
|
5
|
+
if (!user)
|
|
6
|
+
return;
|
|
7
|
+
const offer = db.prepare("SELECT * FROM products WHERE id = ? AND listing_id IS NOT NULL").get(req.params.id);
|
|
8
|
+
if (!offer)
|
|
9
|
+
return void res.status(404).json({ error: 'offer 不存在' });
|
|
10
|
+
if (offer.seller_id !== user.id)
|
|
11
|
+
return void res.status(403).json({ error: '仅卖家本人可修改' });
|
|
12
|
+
const body = req.body;
|
|
13
|
+
const updates = [];
|
|
14
|
+
const args = [];
|
|
15
|
+
if (body.price != null) {
|
|
16
|
+
const p = Number(body.price);
|
|
17
|
+
if (!Number.isFinite(p) || p <= 0)
|
|
18
|
+
return void res.json({ error: 'price 必须 > 0' });
|
|
19
|
+
updates.push('price = ?');
|
|
20
|
+
args.push(p);
|
|
21
|
+
}
|
|
22
|
+
if (body.stock != null) {
|
|
23
|
+
const s = Math.max(0, Math.floor(Number(body.stock) || 0));
|
|
24
|
+
updates.push('stock = ?');
|
|
25
|
+
args.push(s);
|
|
26
|
+
}
|
|
27
|
+
if (body.fulfillment_type != null) {
|
|
28
|
+
const ft = String(body.fulfillment_type);
|
|
29
|
+
if (!VALID_FULFILLMENT_TYPES.has(ft))
|
|
30
|
+
return void res.json({ error: 'fulfillment_type 无效' });
|
|
31
|
+
updates.push('fulfillment_type = ?');
|
|
32
|
+
args.push(ft);
|
|
33
|
+
}
|
|
34
|
+
if (body.eta_hours != null) {
|
|
35
|
+
updates.push('eta_hours = ?');
|
|
36
|
+
args.push(Number(body.eta_hours));
|
|
37
|
+
}
|
|
38
|
+
if (body.is_clearance != null) {
|
|
39
|
+
updates.push('is_clearance = ?');
|
|
40
|
+
args.push(body.is_clearance ? 1 : 0);
|
|
41
|
+
}
|
|
42
|
+
if (body.clearance_until !== undefined) {
|
|
43
|
+
updates.push('clearance_until = ?');
|
|
44
|
+
args.push(body.clearance_until ? String(body.clearance_until) : null);
|
|
45
|
+
}
|
|
46
|
+
if (!updates.length)
|
|
47
|
+
return void res.json({ error: '无任何修改' });
|
|
48
|
+
updates.push("updated_at = datetime('now')");
|
|
49
|
+
updates.push("freshness_ts = datetime('now')");
|
|
50
|
+
args.push(req.params.id);
|
|
51
|
+
db.prepare(`UPDATE products SET ${updates.join(', ')} WHERE id = ?`).run(...args);
|
|
52
|
+
res.json({ success: true });
|
|
53
|
+
});
|
|
54
|
+
// 撤回 offer(status=warehouse + 释放 stake;不真删 product)
|
|
55
|
+
app.delete('/api/offers/:id', (req, res) => {
|
|
56
|
+
const user = auth(req, res);
|
|
57
|
+
if (!user)
|
|
58
|
+
return;
|
|
59
|
+
const offer = db.prepare("SELECT * FROM products WHERE id = ? AND listing_id IS NOT NULL").get(req.params.id);
|
|
60
|
+
if (!offer)
|
|
61
|
+
return void res.status(404).json({ error: 'offer 不存在' });
|
|
62
|
+
if (offer.seller_id !== user.id)
|
|
63
|
+
return void res.status(403).json({ error: '仅卖家本人可撤回' });
|
|
64
|
+
const pending = db.prepare(`SELECT COUNT(1) as n FROM orders WHERE product_id = ? AND status NOT IN ('completed','cancelled','refunded','expired')`).get(req.params.id);
|
|
65
|
+
if (pending.n > 0)
|
|
66
|
+
return void res.json({ error: `该 offer 有 ${pending.n} 个进行中订单,暂无法撤回` });
|
|
67
|
+
const stake = Number(offer.listing_stake_locked) || 0;
|
|
68
|
+
const tx = db.transaction(() => {
|
|
69
|
+
db.prepare(`UPDATE products SET status = 'warehouse', listing_stake_locked = 0, updated_at = datetime('now') WHERE id = ?`).run(req.params.id);
|
|
70
|
+
if (stake > 0) {
|
|
71
|
+
db.prepare(`UPDATE wallets SET balance = balance + ?, staked = staked - ? WHERE user_id = ?`).run(stake, stake, user.id);
|
|
72
|
+
}
|
|
73
|
+
db.prepare(`UPDATE listings SET total_offers = MAX(0, total_offers - 1) WHERE id = ?`).run(String(offer.listing_id));
|
|
74
|
+
});
|
|
75
|
+
try {
|
|
76
|
+
tx();
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
return void res.status(500).json({ error: String(e.message) });
|
|
80
|
+
}
|
|
81
|
+
res.json({ success: true, stake_released: stake });
|
|
82
|
+
});
|
|
83
|
+
// 刷新 freshness(卖家点 "现货确认")
|
|
84
|
+
app.post('/api/offers/:id/refresh', (req, res) => {
|
|
85
|
+
const user = auth(req, res);
|
|
86
|
+
if (!user)
|
|
87
|
+
return;
|
|
88
|
+
const offer = db.prepare("SELECT seller_id FROM products WHERE id = ? AND listing_id IS NOT NULL").get(req.params.id);
|
|
89
|
+
if (!offer)
|
|
90
|
+
return void res.status(404).json({ error: 'offer 不存在' });
|
|
91
|
+
if (offer.seller_id !== user.id)
|
|
92
|
+
return void res.status(403).json({ error: '仅卖家本人可刷新' });
|
|
93
|
+
db.prepare(`UPDATE products SET freshness_ts = datetime('now') WHERE id = ?`).run(req.params.id);
|
|
94
|
+
res.json({ success: true });
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
export function registerOrdersActionRoutes(app, deps) {
|
|
2
|
+
const { db, auth, isTrustedRole, generateId, transition, notifyTransition, settleOrder, detectFraud, createDispute, checkTimeouts, recordViolationReputation, broadcastSystemEvent } = deps;
|
|
3
|
+
// C-4: 卖家批量发货
|
|
4
|
+
app.post('/api/orders/batch-ship', (req, res) => {
|
|
5
|
+
const user = auth(req, res);
|
|
6
|
+
if (!user)
|
|
7
|
+
return;
|
|
8
|
+
const { order_ids, logistics_company_id, tracking_numbers } = req.body || {};
|
|
9
|
+
if (!Array.isArray(order_ids) || order_ids.length === 0)
|
|
10
|
+
return void res.status(400).json({ error: 'order_ids 必填' });
|
|
11
|
+
if (order_ids.length > 100)
|
|
12
|
+
return void res.status(400).json({ error: '单次最多 100 单' });
|
|
13
|
+
if (!logistics_company_id)
|
|
14
|
+
return void res.status(400).json({ error: 'logistics_company_id 必填' });
|
|
15
|
+
const lc = db.prepare("SELECT id FROM users WHERE id = ? AND role = 'logistics'").get(logistics_company_id);
|
|
16
|
+
if (!lc)
|
|
17
|
+
return void res.status(400).json({ error: '物流公司不存在' });
|
|
18
|
+
const results = [];
|
|
19
|
+
const trackingMap = (tracking_numbers && typeof tracking_numbers === 'object') ? tracking_numbers : {};
|
|
20
|
+
for (const oid of order_ids) {
|
|
21
|
+
try {
|
|
22
|
+
const o = db.prepare("SELECT id, seller_id, status, logistics_id FROM orders WHERE id = ?").get(oid);
|
|
23
|
+
if (!o) {
|
|
24
|
+
results.push({ order_id: oid, status: 'skipped', reason: '订单不存在' });
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (o.seller_id !== user.id) {
|
|
28
|
+
results.push({ order_id: oid, status: 'skipped', reason: '非自家订单' });
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (o.status !== 'accepted') {
|
|
32
|
+
results.push({ order_id: oid, status: 'skipped', reason: `状态非 accepted (当前 ${o.status})` });
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (!o.logistics_id) {
|
|
36
|
+
db.prepare("UPDATE orders SET logistics_id = ? WHERE id = ?").run(logistics_company_id, oid);
|
|
37
|
+
}
|
|
38
|
+
const tn = trackingMap[oid] ? String(trackingMap[oid]).slice(0, 50) : null;
|
|
39
|
+
const evIds = [];
|
|
40
|
+
if (tn) {
|
|
41
|
+
const eid = generateId('evt');
|
|
42
|
+
db.prepare(`INSERT INTO evidence (id, order_id, uploader_id, type, description, file_hash)
|
|
43
|
+
VALUES (?,?,?,'description',?,?)`).run(eid, oid, user.id, `快递单号:${tn}`, `hash_${Date.now()}`);
|
|
44
|
+
evIds.push(eid);
|
|
45
|
+
}
|
|
46
|
+
const result = transition(db, oid, 'shipped', user.id, evIds, tn ? `批量发货 · ${tn}` : '批量发货');
|
|
47
|
+
if (!result.success) {
|
|
48
|
+
results.push({ order_id: oid, status: 'skipped', reason: result.error || '状态机拒绝' });
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
notifyTransition(db, oid, 'accepted', 'shipped');
|
|
52
|
+
results.push({ order_id: oid, status: 'shipped' });
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
results.push({ order_id: oid, status: 'skipped', reason: e.message });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const shipped = results.filter(r => r.status === 'shipped').length;
|
|
59
|
+
res.json({ success: true, shipped, skipped: results.filter(r => r.status === 'skipped').length, results });
|
|
60
|
+
});
|
|
61
|
+
// 买家确认面交完成 → 直接 completed + settleOrder
|
|
62
|
+
app.post('/api/orders/:id/confirm-in-person', (req, res) => {
|
|
63
|
+
const user = auth(req, res);
|
|
64
|
+
if (!user)
|
|
65
|
+
return;
|
|
66
|
+
const order = db.prepare('SELECT * FROM orders WHERE id = ?').get(req.params.id);
|
|
67
|
+
if (!order)
|
|
68
|
+
return void res.status(404).json({ error: '订单不存在' });
|
|
69
|
+
if (order.fulfillment_mode !== 'in_person')
|
|
70
|
+
return void res.status(400).json({ error: '该订单非面交' });
|
|
71
|
+
if (order.buyer_id !== user.id)
|
|
72
|
+
return void res.status(403).json({ error: '仅买家可确认面交完成' });
|
|
73
|
+
if (!['paid', 'accepted'].includes(order.status))
|
|
74
|
+
return void res.status(400).json({ error: `订单状态 ${order.status} 不可确认面交` });
|
|
75
|
+
if (order.has_pending_claim)
|
|
76
|
+
return void res.status(400).json({ error: '存在进行中的验证任务,不可确认' });
|
|
77
|
+
const tx = db.transaction(() => {
|
|
78
|
+
db.prepare(`UPDATE orders SET status='completed', updated_at=datetime('now') WHERE id = ?`).run(req.params.id);
|
|
79
|
+
db.prepare(`INSERT INTO order_state_history (id, order_id, from_status, to_status, actor_id, actor_role, notes)
|
|
80
|
+
VALUES (?,?,?,?,?,?, '面交完成 — 买家确认')`)
|
|
81
|
+
.run(generateId('hst'), req.params.id, order.status, 'completed', user.id, user.role || 'buyer');
|
|
82
|
+
});
|
|
83
|
+
try {
|
|
84
|
+
tx();
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
return void res.status(500).json({ error: '状态写入失败:' + e.message });
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
settleOrder(req.params.id);
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
console.error('[settleOrder in-person]', e);
|
|
94
|
+
}
|
|
95
|
+
res.json({ success: true });
|
|
96
|
+
});
|
|
97
|
+
// 通用状态机 action — accept/ship/pickup/transit/deliver/confirm/dispute
|
|
98
|
+
app.post('/api/orders/:id/action', (req, res) => {
|
|
99
|
+
const user = auth(req, res);
|
|
100
|
+
if (!user)
|
|
101
|
+
return;
|
|
102
|
+
// P0 fix: 受信角色禁交易
|
|
103
|
+
if (isTrustedRole(user))
|
|
104
|
+
return void res.status(403).json({ error: '受信角色不可参与订单流转', error_code: 'TRUSTED_ROLE_NO_TRADE' });
|
|
105
|
+
const { action, notes = '', evidence_description = '', logistics_company_id = '' } = req.body;
|
|
106
|
+
const order = db.prepare('SELECT * FROM orders WHERE id = ?').get(req.params.id);
|
|
107
|
+
if (!order)
|
|
108
|
+
return void res.status(404).json({ error: '订单不存在' });
|
|
109
|
+
// P0: 路由层 ownership 校验(engine 层只看 role,必须补 ownership)
|
|
110
|
+
const buyerId = order.buyer_id;
|
|
111
|
+
const sellerId = order.seller_id;
|
|
112
|
+
const logisticsId = order.logistics_id || null;
|
|
113
|
+
const uid = user.id;
|
|
114
|
+
if ((action === 'accept' || action === 'ship') && uid !== sellerId) {
|
|
115
|
+
return void res.status(403).json({ error: '你不是本订单的卖家', error_code: 'NOT_ORDER_SELLER' });
|
|
116
|
+
}
|
|
117
|
+
if (action === 'confirm' && uid !== buyerId) {
|
|
118
|
+
return void res.status(403).json({ error: '你不是本订单的买家', error_code: 'NOT_ORDER_BUYER' });
|
|
119
|
+
}
|
|
120
|
+
if (action === 'pickup' || action === 'transit' || action === 'deliver') {
|
|
121
|
+
// pickup 时若订单尚无物流,允许领取(孤儿单兜底)
|
|
122
|
+
const isOrphanPickup = action === 'pickup' && !logisticsId;
|
|
123
|
+
if (!isOrphanPickup && uid !== logisticsId) {
|
|
124
|
+
return void res.status(403).json({ error: '你不是本订单的物流方', error_code: 'NOT_ORDER_LOGISTICS' });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (action === 'dispute' && uid !== buyerId && uid !== sellerId && uid !== logisticsId) {
|
|
128
|
+
return void res.status(403).json({ error: '只有交易参与方可发起争议', error_code: 'NOT_ORDER_PARTY' });
|
|
129
|
+
}
|
|
130
|
+
// 卖家发货时绑定物流公司
|
|
131
|
+
if (action === 'ship' && logistics_company_id) {
|
|
132
|
+
const logi = db.prepare(`SELECT id FROM users WHERE id = ? AND role = 'logistics'`).get(logistics_company_id);
|
|
133
|
+
if (!logi)
|
|
134
|
+
return void res.json({ error: '所选物流公司不存在' });
|
|
135
|
+
db.prepare('UPDATE orders SET logistics_id = ? WHERE id = ?').run(logistics_company_id, req.params.id);
|
|
136
|
+
}
|
|
137
|
+
// 物流自行揽收(卖家未指定物流时的兜底)
|
|
138
|
+
if (action === 'pickup' && !order.logistics_id && user.role === 'logistics') {
|
|
139
|
+
db.prepare('UPDATE orders SET logistics_id = ? WHERE id = ?').run(user.id, req.params.id);
|
|
140
|
+
}
|
|
141
|
+
const actionMap = {
|
|
142
|
+
accept: 'accepted', ship: 'shipped', pickup: 'picked_up',
|
|
143
|
+
transit: 'in_transit', deliver: 'delivered', confirm: 'confirmed', dispute: 'disputed'
|
|
144
|
+
};
|
|
145
|
+
const toStatus = actionMap[action];
|
|
146
|
+
if (!toStatus)
|
|
147
|
+
return void res.json({ error: `未知操作:${action}` });
|
|
148
|
+
// 创建证据记录 + 跨窗反诈:description 跑 detectFraud
|
|
149
|
+
const evidenceIds = [];
|
|
150
|
+
if (evidence_description) {
|
|
151
|
+
const eid = generateId('evt');
|
|
152
|
+
const evReasons = detectFraud(String(evidence_description));
|
|
153
|
+
db.prepare(`INSERT INTO evidence (id, order_id, uploader_id, type, description, file_hash, flag_reasons)
|
|
154
|
+
VALUES (?,?,?,'description',?,?,?)`).run(eid, req.params.id, user.id, evidence_description, `hash_${Date.now()}`, evReasons.length ? JSON.stringify(evReasons) : null);
|
|
155
|
+
evidenceIds.push(eid);
|
|
156
|
+
}
|
|
157
|
+
const fromStatus = order.status;
|
|
158
|
+
const result = transition(db, req.params.id, toStatus, user.id, evidenceIds, notes);
|
|
159
|
+
if (!result.success)
|
|
160
|
+
return void res.json({ error: result.error });
|
|
161
|
+
notifyTransition(db, req.params.id, fromStatus, toStatus);
|
|
162
|
+
if (toStatus === 'disputed') {
|
|
163
|
+
createDispute(db, req.params.id, user.id, notes || evidence_description || '买家发起争议', evidenceIds);
|
|
164
|
+
try {
|
|
165
|
+
broadcastSystemEvent('dispute_open', '⚖', `争议发起 (订单 ${req.params.id})`, req.params.id);
|
|
166
|
+
}
|
|
167
|
+
catch { }
|
|
168
|
+
}
|
|
169
|
+
if (toStatus === 'completed') {
|
|
170
|
+
try {
|
|
171
|
+
broadcastSystemEvent('order_completed', '✓', `订单完成 ${req.params.id}`, req.params.id);
|
|
172
|
+
}
|
|
173
|
+
catch { }
|
|
174
|
+
}
|
|
175
|
+
// 确认收货时自动结算
|
|
176
|
+
let settlementBreakdown = null;
|
|
177
|
+
if (toStatus === 'confirmed') {
|
|
178
|
+
const sysUser = db.prepare("SELECT id FROM users WHERE id = 'sys_protocol'").get();
|
|
179
|
+
transition(db, req.params.id, 'completed', sysUser.id, [], '系统自动结算');
|
|
180
|
+
notifyTransition(db, req.params.id, 'confirmed', 'completed');
|
|
181
|
+
settleOrder(req.params.id);
|
|
182
|
+
// QA 轮 9.4-retry-v3 P1:post-hoc build breakdown 从 DB,让 agent 看清每分钱去哪
|
|
183
|
+
try {
|
|
184
|
+
const round2 = (n) => Math.round(n * 100) / 100;
|
|
185
|
+
const ord = db.prepare("SELECT id, total_amount, source, fulfillment_mode, snapshot_commission_rate, l1_uid, l2_uid, l3_uid, logistics_id, seller_id FROM orders WHERE id = ?").get(req.params.id);
|
|
186
|
+
if (ord) {
|
|
187
|
+
const total = Number(ord.total_amount);
|
|
188
|
+
const isSecondhand = ord.source === 'secondhand';
|
|
189
|
+
const isInPerson = ord.fulfillment_mode === 'in_person';
|
|
190
|
+
const feeRate = isSecondhand ? 0.01 : 0.02;
|
|
191
|
+
const protocolFee = round2(total * feeRate);
|
|
192
|
+
const logisticsFee = isInPerson ? 0 : round2(total * 0.05);
|
|
193
|
+
const logisticsActual = ord.logistics_id ? logisticsFee : 0;
|
|
194
|
+
const commissionRate = Number(ord.snapshot_commission_rate ?? 0.10);
|
|
195
|
+
const commissionPool = round2(total * commissionRate);
|
|
196
|
+
const commRecs = db.prepare("SELECT level, amount, beneficiary_id FROM commission_records WHERE order_id = ?").all(req.params.id);
|
|
197
|
+
const commByLevel = {
|
|
198
|
+
1: { amount: 0, to: null }, 2: { amount: 0, to: null }, 3: { amount: 0, to: null },
|
|
199
|
+
};
|
|
200
|
+
let commissionDistributed = 0;
|
|
201
|
+
for (const r of commRecs) {
|
|
202
|
+
commByLevel[r.level] = { amount: Number(r.amount), to: r.beneficiary_id };
|
|
203
|
+
commissionDistributed += Number(r.amount);
|
|
204
|
+
}
|
|
205
|
+
const commissionRedirected = round2(commissionPool - commissionDistributed);
|
|
206
|
+
// QA 轮 14.b P2:redirected_total 拆 chain_gap(→charity) vs region_cap(→global_fund)
|
|
207
|
+
// 之前单一数字让 agent 无法分辨钱去哪(global region L2/L3 进 global_fund,不是 charity)
|
|
208
|
+
const charityRow = db.prepare("SELECT COALESCE(SUM(amount),0) AS s FROM charity_fund_txns WHERE related_order_id = ?").get(req.params.id);
|
|
209
|
+
const redirectedToCharity = round2(Number(charityRow.s));
|
|
210
|
+
const fundDepRow = db.prepare("SELECT COALESCE(SUM(amount_l3),0) AS s FROM fund_deposits WHERE order_id = ?").get(req.params.id);
|
|
211
|
+
const redirectedToGlobalFund = round2(Number(fundDepRow.s));
|
|
212
|
+
// QA 轮 9.5 P2:payouts 表只 MCP legacy 写,PWA settleOrder 直更 wallet.balance 不写 payouts
|
|
213
|
+
// 改用公式推算 sellerAmount(跟 PWA settleOrder 内部计算一致),更可靠
|
|
214
|
+
const fundBase1pct = round2(total * 0.01);
|
|
215
|
+
const sellerAmountComputed = round2(total - protocolFee - logisticsActual - commissionPool - fundBase1pct);
|
|
216
|
+
// sum_check 守恒:order_amount 应 = seller_net + protocol_fund + logistics + commission_pool(L1+L2+L3+redirect) + fund_base
|
|
217
|
+
const sumComponents = round2(sellerAmountComputed + protocolFee + logisticsActual + commissionPool + fundBase1pct);
|
|
218
|
+
settlementBreakdown = {
|
|
219
|
+
order_amount: total,
|
|
220
|
+
distribution: {
|
|
221
|
+
seller_net: { amount: sellerAmountComputed, to: ord.seller_id, note: '不含可能的首销 stake 锁定(settleOrder 内 stake_locked_at 首次锁,从 sellerAmount 划出)' },
|
|
222
|
+
protocol_fund_2pct: { amount: protocolFee, split: { management_bonus_pool: round2(protocolFee / 2), sys_protocol_ops: round2(protocolFee / 2) } },
|
|
223
|
+
logistics_fee: { amount: logisticsActual, rate: isInPerson ? 'N/A in_person' : (ord.logistics_id ? '5%' : 'N/A self-fulfill') },
|
|
224
|
+
commission_pool: { total: commissionPool, rate: `${(commissionRate * 100).toFixed(1)}%` },
|
|
225
|
+
commission_distribution_7_2_1: {
|
|
226
|
+
l1: commByLevel[1],
|
|
227
|
+
l2: commByLevel[2],
|
|
228
|
+
l3: commByLevel[3],
|
|
229
|
+
distributed_total: round2(commissionDistributed),
|
|
230
|
+
redirected_total: commissionRedirected,
|
|
231
|
+
redirected_to_charity: redirectedToCharity, // chain_gap (无 L / sponsor 无效)
|
|
232
|
+
redirected_to_global_fund: redirectedToGlobalFund, // region cap (level > region max_levels)
|
|
233
|
+
redirect_note: 'chain_gap (无 L) → charity_fund; region cap (level > max_levels) → global_fund',
|
|
234
|
+
},
|
|
235
|
+
fund_base_1pct: fundBase1pct,
|
|
236
|
+
},
|
|
237
|
+
sum_check: sumComponents,
|
|
238
|
+
sum_check_ok: Math.abs(sumComponents - total) < 0.01,
|
|
239
|
+
transparency_note: 'sum_check 校验:order_amount = seller_net + protocol_fund + logistics + commission_pool + fund_base。stake / pinner / PV 分账见各自专用查询。',
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
catch (e) {
|
|
244
|
+
console.error('[settlement_breakdown build]', e);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
res.json({
|
|
248
|
+
success: true,
|
|
249
|
+
new_status: result.newStatus,
|
|
250
|
+
...(settlementBreakdown ? { settlement_breakdown: settlementBreakdown } : {}),
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
// 手动触发超时判责(当事人)
|
|
254
|
+
app.post('/api/orders/:id/force-timeout-check', (req, res) => {
|
|
255
|
+
const user = auth(req, res);
|
|
256
|
+
if (!user)
|
|
257
|
+
return;
|
|
258
|
+
const order = db.prepare('SELECT buyer_id, seller_id, logistics_id, status FROM orders WHERE id = ?').get(req.params.id);
|
|
259
|
+
if (!order)
|
|
260
|
+
return void res.status(404).json({ error: '订单不存在' });
|
|
261
|
+
const uid = user.id;
|
|
262
|
+
if (uid !== order.buyer_id && uid !== order.seller_id && uid !== order.logistics_id) {
|
|
263
|
+
return void res.status(403).json({ error: '非订单当事人' });
|
|
264
|
+
}
|
|
265
|
+
const beforeStatus = order.status;
|
|
266
|
+
const r = checkTimeouts(db);
|
|
267
|
+
const after = db.prepare('SELECT status FROM orders WHERE id = ?').get(req.params.id);
|
|
268
|
+
const touched = r.details.find((d) => d.orderId === req.params.id) || null;
|
|
269
|
+
if (touched) {
|
|
270
|
+
const faultMatch = touched.action.match(/→ (fault_\w+)/);
|
|
271
|
+
if (faultMatch) {
|
|
272
|
+
try {
|
|
273
|
+
recordViolationReputation(db, req.params.id, faultMatch[1]);
|
|
274
|
+
}
|
|
275
|
+
catch { }
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
res.json({
|
|
279
|
+
before_status: beforeStatus,
|
|
280
|
+
after_status: after.status,
|
|
281
|
+
changed: beforeStatus !== after.status,
|
|
282
|
+
action: touched?.action || null,
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
}
|