@seasonkoh/webaz 0.1.24 → 0.1.26
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/README.md +5 -1
- package/dist/layer0-foundation/L0-1-database/db-backends/pg-backend.js +51 -0
- package/dist/layer0-foundation/L0-1-database/db-backends/sql-dialect-datetime.js +437 -0
- package/dist/layer0-foundation/L0-1-database/db-backends/sql-placeholders.js +98 -0
- package/dist/layer0-foundation/L0-1-database/db.js +65 -0
- package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +13 -11
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +1 -1
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +13 -11
- package/dist/layer1-agent/L1-1-mcp-server/server.js +288 -208
- package/dist/layer1-agent/L1-2-external-anchor/anchor-engine.js +14 -12
- package/dist/layer2-business/L2-6-notifications/notification-engine.js +8 -5
- package/dist/layer2-business/L2-7-snf/snf-engine.js +16 -14
- package/dist/layer2-business/L2-8-feedback/build-feedback-engine.js +18 -10
- package/dist/layer2-business/L2-9-contribution/build-reputation-engine.js +37 -23
- package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +182 -0
- package/dist/layer2-business/L2-9-contribution/build-task-participation.js +47 -0
- package/dist/layer2-business/L2-9-contribution/build-task-read.js +222 -0
- package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +11 -3
- package/dist/layer2-business/L2-9-contribution/canonical-contribution-target.js +16 -0
- package/dist/layer2-business/L2-9-contribution/contribution-display-envelope.js +40 -0
- package/dist/layer2-business/L2-9-contribution/contribution-score-contract.js +36 -0
- package/dist/layer2-business/L2-9-contribution/contribution-score-evidence.js +61 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/canonical.js +60 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/github-credential.schema.js +140 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/github-fetch-adapter.js +437 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/self-consistency.js +38 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/verifier.js +231 -0
- package/dist/layer2-business/L2-9-contribution/github-credential-ingestion-engine.js +145 -0
- package/dist/layer2-business/L2-9-contribution/github-credential-store.js +115 -0
- package/dist/layer2-business/L2-9-contribution/identity-binding-engine.js +134 -0
- package/dist/layer2-business/L2-9-contribution/identity-binding-store.js +101 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-engine.js +126 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-store.js +30 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-discovery.js +55 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-engine.js +109 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-fact-precondition.js +22 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-proof-verifier.js +97 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-read.js +59 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-ai-store.js +99 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-draft.js +191 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +129 -0
- package/dist/layer2-business/L2-notes/note-photo-storage.js +4 -2
- package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +17 -15
- package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +11 -8
- package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +9 -8
- package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +11 -8
- package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +22 -16
- package/dist/pwa/acp-feed.js +13 -1
- package/dist/pwa/admin-bearer-auth.js +21 -0
- package/dist/pwa/contract-fingerprint.js +2 -0
- package/dist/pwa/email-delivery.js +127 -0
- package/dist/pwa/endpoint-actions.js +5 -1
- package/dist/pwa/goal-index.js +8 -8
- package/dist/pwa/human-presence.js +62 -0
- package/dist/pwa/public/app.js +1485 -283
- package/dist/pwa/public/i18n.js +297 -59
- package/dist/pwa/public/index.html +1 -0
- package/dist/pwa/public/openapi.json +5 -5
- package/dist/pwa/public/whitepaper/en/index.html +153 -0
- package/dist/pwa/public/whitepaper/zh-CN/index.html +153 -0
- package/dist/pwa/rate-limit.js +22 -0
- package/dist/pwa/routes/account-deletion.js +15 -13
- package/dist/pwa/routes/addresses.js +10 -9
- package/dist/pwa/routes/admin-admins.js +13 -14
- package/dist/pwa/routes/admin-analytics.js +109 -69
- package/dist/pwa/routes/admin-atomic.js +10 -4
- package/dist/pwa/routes/admin-catalog.js +13 -11
- package/dist/pwa/routes/admin-editor-picks.js +15 -10
- package/dist/pwa/routes/admin-events.js +5 -3
- package/dist/pwa/routes/admin-health.js +2 -1
- package/dist/pwa/routes/admin-moderation.js +50 -29
- package/dist/pwa/routes/admin-ops.js +35 -23
- package/dist/pwa/routes/admin-protocol-params.js +16 -19
- package/dist/pwa/routes/admin-reports.js +23 -21
- package/dist/pwa/routes/admin-tokenomics.js +26 -25
- package/dist/pwa/routes/admin-users-lifecycle.js +37 -40
- package/dist/pwa/routes/admin-users-query.js +65 -53
- package/dist/pwa/routes/admin-verifier-flow.js +82 -41
- package/dist/pwa/routes/admin-verifier-whitelist.js +55 -27
- package/dist/pwa/routes/admin-wallet-ops.js +32 -7
- package/dist/pwa/routes/agent-buy.js +46 -22
- package/dist/pwa/routes/agent-governance.js +52 -56
- package/dist/pwa/routes/ai.js +7 -5
- package/dist/pwa/routes/analytics.js +43 -41
- package/dist/pwa/routes/anchors.js +19 -20
- package/dist/pwa/routes/announcements.js +13 -13
- package/dist/pwa/routes/arbitrator.js +97 -31
- package/dist/pwa/routes/auction.js +157 -116
- package/dist/pwa/routes/auth-login.js +6 -4
- package/dist/pwa/routes/auth-read.js +21 -10
- package/dist/pwa/routes/auth-register.js +111 -26
- package/dist/pwa/routes/auth-sessions.js +12 -11
- package/dist/pwa/routes/blocklist.js +16 -15
- package/dist/pwa/routes/build-feedback.js +10 -9
- package/dist/pwa/routes/build-reputation.js +6 -2
- package/dist/pwa/routes/build-tasks.js +45 -13
- package/dist/pwa/routes/buyer-feeds.js +27 -25
- package/dist/pwa/routes/cart.js +16 -15
- package/dist/pwa/routes/charity.js +212 -150
- package/dist/pwa/routes/chat.js +42 -43
- package/dist/pwa/routes/checkin-tasks.js +10 -9
- package/dist/pwa/routes/checkout-helpers.js +12 -10
- package/dist/pwa/routes/claim-initiators.js +34 -14
- package/dist/pwa/routes/claim-verify.js +86 -53
- package/dist/pwa/routes/claim-voting.js +43 -18
- package/dist/pwa/routes/contribution-identity.js +164 -0
- package/dist/pwa/routes/contribution-score.js +19 -0
- package/dist/pwa/routes/coupons.js +19 -16
- package/dist/pwa/routes/dashboards.js +18 -16
- package/dist/pwa/routes/dispute-cases.js +25 -24
- package/dist/pwa/routes/disputes-read.js +45 -51
- package/dist/pwa/routes/disputes-write.js +124 -61
- package/dist/pwa/routes/evidence.js +9 -9
- package/dist/pwa/routes/external-anchors.js +13 -12
- package/dist/pwa/routes/feedback.js +29 -33
- package/dist/pwa/routes/flash-sales.js +18 -16
- package/dist/pwa/routes/follows.js +25 -24
- package/dist/pwa/routes/governance-auto-deactivate.js +21 -9
- package/dist/pwa/routes/governance-onboarding.js +70 -59
- package/dist/pwa/routes/group-buys.js +22 -22
- package/dist/pwa/routes/growth.js +34 -31
- package/dist/pwa/routes/import-product.js +12 -10
- package/dist/pwa/routes/kyc.js +9 -8
- package/dist/pwa/routes/leaderboard.js +20 -18
- package/dist/pwa/routes/listings.js +23 -22
- package/dist/pwa/routes/logistics.js +10 -8
- package/dist/pwa/routes/manifests.js +27 -27
- package/dist/pwa/routes/me-data.js +23 -21
- package/dist/pwa/routes/notifications.js +7 -6
- package/dist/pwa/routes/offers.js +30 -12
- package/dist/pwa/routes/orders-action.js +51 -29
- package/dist/pwa/routes/orders-create.js +75 -20
- package/dist/pwa/routes/orders-read.js +21 -20
- package/dist/pwa/routes/p2p-products.js +30 -18
- package/dist/pwa/routes/payments-governance.js +61 -56
- package/dist/pwa/routes/peers.js +9 -8
- package/dist/pwa/routes/pin-receipts.js +13 -13
- package/dist/pwa/routes/products-aliases.js +12 -10
- package/dist/pwa/routes/products-claims.js +36 -17
- package/dist/pwa/routes/products-create.js +53 -38
- package/dist/pwa/routes/products-crud.js +17 -16
- package/dist/pwa/routes/products-links.js +49 -26
- package/dist/pwa/routes/products-list.js +6 -4
- package/dist/pwa/routes/products-meta.js +40 -39
- package/dist/pwa/routes/products-update.js +19 -5
- package/dist/pwa/routes/profile-credentials.js +20 -19
- package/dist/pwa/routes/profile-identity.js +14 -13
- package/dist/pwa/routes/profile-location.js +7 -6
- package/dist/pwa/routes/profile-placement.js +20 -19
- package/dist/pwa/routes/profile-prefs.js +11 -11
- package/dist/pwa/routes/promoter.js +58 -66
- package/dist/pwa/routes/public-build-tasks.js +19 -0
- package/dist/pwa/routes/public-utils.js +108 -46
- package/dist/pwa/routes/push.js +16 -15
- package/dist/pwa/routes/ratings.js +92 -32
- package/dist/pwa/routes/recover-key.js +66 -26
- package/dist/pwa/routes/referral.js +37 -52
- package/dist/pwa/routes/reputation.js +3 -2
- package/dist/pwa/routes/returns.js +76 -73
- package/dist/pwa/routes/reviews.js +41 -18
- package/dist/pwa/routes/rewards-apply.js +16 -15
- package/dist/pwa/routes/rewards-auto-downgrade.js +9 -7
- package/dist/pwa/routes/rewards-escrow-expire.js +7 -5
- package/dist/pwa/routes/rfqs.js +163 -85
- package/dist/pwa/routes/search.js +16 -14
- package/dist/pwa/routes/secondhand.js +25 -22
- package/dist/pwa/routes/seller-quota.js +24 -26
- package/dist/pwa/routes/share-redirects.js +60 -55
- package/dist/pwa/routes/shareables-interactions.js +34 -35
- package/dist/pwa/routes/shareables.js +55 -51
- package/dist/pwa/routes/shop-referral.js +58 -0
- package/dist/pwa/routes/shops.js +25 -20
- package/dist/pwa/routes/signaling.js +10 -9
- package/dist/pwa/routes/skill-market.js +16 -16
- package/dist/pwa/routes/skills.js +15 -14
- package/dist/pwa/routes/snf.js +14 -13
- package/dist/pwa/routes/tags.js +10 -9
- package/dist/pwa/routes/task-proposals.js +121 -0
- package/dist/pwa/routes/trial.js +72 -52
- package/dist/pwa/routes/trusted-kpi.js +20 -18
- package/dist/pwa/routes/url-claim.js +67 -28
- package/dist/pwa/routes/users-public.js +62 -70
- package/dist/pwa/routes/variants.js +12 -13
- package/dist/pwa/routes/verifier-user.js +61 -21
- package/dist/pwa/routes/verify-tasks.js +49 -25
- package/dist/pwa/routes/waitlist.js +16 -15
- package/dist/pwa/routes/wallet-read.js +75 -37
- package/dist/pwa/routes/wallet-write.js +12 -9
- package/dist/pwa/routes/webauthn.js +25 -26
- package/dist/pwa/routes/webhooks.js +26 -26
- package/dist/pwa/routes/welcome.js +45 -50
- package/dist/pwa/routes/wishlist-qa.js +29 -32
- package/dist/pwa/server.js +304 -90
- package/dist/version.js +1 -1
- package/package.json +76 -3
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
/** 计算 coupon 适用 + 折扣。跨域调用:orders 下单流程会用。 */
|
|
2
3
|
export function applyCouponToOrder(db, couponCode, sellerId, productId, totalAmount) {
|
|
3
4
|
const code = couponCode.trim().toUpperCase();
|
|
@@ -36,8 +37,9 @@ export function applyCouponToOrder(db, couponCode, sellerId, productId, totalAmo
|
|
|
36
37
|
return { ok: true, coupon, discount };
|
|
37
38
|
}
|
|
38
39
|
export function registerCouponsRoutes(app, deps) {
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
// db 已走 RFC-016 异步 seam(dbOne/dbAll/dbRun);applyCouponToOrder 仍同步(订单/结算金钱路径,随 money batch 一起迁)
|
|
41
|
+
const { generateId, auth, isTrustedRole, safeRoles, errorRes } = deps;
|
|
42
|
+
app.post('/api/coupons', async (req, res) => {
|
|
41
43
|
const user = auth(req, res);
|
|
42
44
|
if (!user)
|
|
43
45
|
return;
|
|
@@ -62,7 +64,7 @@ export function registerCouponsRoutes(app, deps) {
|
|
|
62
64
|
if (discount_type === 'percentage' && dv > 90)
|
|
63
65
|
return void res.status(400).json({ error: 'percentage 最高 90' });
|
|
64
66
|
if (scope === 'product') {
|
|
65
|
-
const p =
|
|
67
|
+
const p = await dbOne('SELECT seller_id FROM products WHERE id = ?', [scope_id]);
|
|
66
68
|
if (!p)
|
|
67
69
|
return void res.status(404).json({ error: '商品不存在' });
|
|
68
70
|
if (p.seller_id !== user.id)
|
|
@@ -74,8 +76,9 @@ export function registerCouponsRoutes(app, deps) {
|
|
|
74
76
|
}
|
|
75
77
|
const id = generateId('cpn');
|
|
76
78
|
try {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
await dbRun(`INSERT INTO coupons (id, seller_id, code, scope, scope_id, discount_type, discount_value, min_order_amount, max_uses, starts_at, expires_at) VALUES (?,?,?,?,?,?,?,?,?,?,?)`, [id, user.id, codeStr, scope, scope_id || null, discount_type, dv,
|
|
80
|
+
Number(min_order_amount) || 0, Number(max_uses) || 0,
|
|
81
|
+
starts_at || null, expires_at || null]);
|
|
79
82
|
}
|
|
80
83
|
catch {
|
|
81
84
|
return void res.status(409).json({ error: '此 code 已存在(每个卖家 code 唯一)' });
|
|
@@ -83,13 +86,13 @@ export function registerCouponsRoutes(app, deps) {
|
|
|
83
86
|
res.json({ success: true, id, code: codeStr });
|
|
84
87
|
});
|
|
85
88
|
// buyer 视角:全平台 + 已购卖家店铺/单品券 + 历史
|
|
86
|
-
app.get('/api/coupons/available', (req, res) => {
|
|
89
|
+
app.get('/api/coupons/available', async (req, res) => {
|
|
87
90
|
const user = auth(req, res);
|
|
88
91
|
if (!user)
|
|
89
92
|
return;
|
|
90
93
|
if (isTrustedRole(user))
|
|
91
94
|
return void errorRes(res, 403, 'TRUSTED_ROLE_NO_TRADE', '受信角色无购物功能');
|
|
92
|
-
const purchasedSellers =
|
|
95
|
+
const purchasedSellers = await dbAll(`SELECT DISTINCT seller_id FROM orders WHERE buyer_id = ?`, [user.id]);
|
|
93
96
|
const sellerIds = purchasedSellers.map(r => r.seller_id);
|
|
94
97
|
const placeholders = sellerIds.length > 0 ? sellerIds.map(() => '?').join(',') : '';
|
|
95
98
|
const sellerCondition = sellerIds.length > 0 ? `OR (c.seller_id IN (${placeholders}) AND c.scope IN ('shop','product'))` : '';
|
|
@@ -112,8 +115,8 @@ export function registerCouponsRoutes(app, deps) {
|
|
|
112
115
|
c.created_at DESC
|
|
113
116
|
LIMIT 100
|
|
114
117
|
`;
|
|
115
|
-
const rows =
|
|
116
|
-
const history =
|
|
118
|
+
const rows = await dbAll(sql, sellerIds);
|
|
119
|
+
const history = await dbAll(`
|
|
117
120
|
SELECT o.id as order_id, o.created_at, o.coupon_discount,
|
|
118
121
|
c.code, c.scope, c.discount_type, c.discount_value,
|
|
119
122
|
p.title as product_title
|
|
@@ -122,23 +125,23 @@ export function registerCouponsRoutes(app, deps) {
|
|
|
122
125
|
JOIN products p ON p.id = o.product_id
|
|
123
126
|
WHERE o.buyer_id = ? AND o.coupon_id IS NOT NULL
|
|
124
127
|
ORDER BY o.created_at DESC LIMIT 50
|
|
125
|
-
|
|
128
|
+
`, [user.id]);
|
|
126
129
|
res.json({ available: rows, history });
|
|
127
130
|
});
|
|
128
|
-
app.get('/api/coupons/mine', (req, res) => {
|
|
131
|
+
app.get('/api/coupons/mine', async (req, res) => {
|
|
129
132
|
const user = auth(req, res);
|
|
130
133
|
if (!user)
|
|
131
134
|
return;
|
|
132
|
-
const rows =
|
|
135
|
+
const rows = await dbAll(`
|
|
133
136
|
SELECT * FROM coupons WHERE seller_id = ? ORDER BY created_at DESC LIMIT 100
|
|
134
|
-
|
|
137
|
+
`, [user.id]);
|
|
135
138
|
res.json({ items: rows });
|
|
136
139
|
});
|
|
137
|
-
app.patch('/api/coupons/:id', (req, res) => {
|
|
140
|
+
app.patch('/api/coupons/:id', async (req, res) => {
|
|
138
141
|
const user = auth(req, res);
|
|
139
142
|
if (!user)
|
|
140
143
|
return;
|
|
141
|
-
const coupon =
|
|
144
|
+
const coupon = await dbOne('SELECT * FROM coupons WHERE id = ? AND seller_id = ?', [req.params.id, user.id]);
|
|
142
145
|
if (!coupon)
|
|
143
146
|
return void res.status(404).json({ error: '优惠码不存在或无权限' });
|
|
144
147
|
const { is_active, expires_at, max_uses } = req.body || {};
|
|
@@ -159,7 +162,7 @@ export function registerCouponsRoutes(app, deps) {
|
|
|
159
162
|
if (sets.length === 0)
|
|
160
163
|
return void res.status(400).json({ error: '无可更新字段' });
|
|
161
164
|
args.push(req.params.id);
|
|
162
|
-
|
|
165
|
+
await dbRun(`UPDATE coupons SET ${sets.join(', ')} WHERE id = ?`, args);
|
|
163
166
|
res.json({ success: true });
|
|
164
167
|
});
|
|
165
168
|
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerDashboardsRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
// db 已走 RFC-016 异步 seam(dbOne/dbAll),不再直接用 deps.db
|
|
4
|
+
const { auth } = deps;
|
|
5
|
+
app.get('/api/tokenomics/status', async (_req, res) => {
|
|
6
|
+
const gf = await dbOne("SELECT pool_balance FROM global_fund WHERE id = 1");
|
|
5
7
|
const poolBalance = Number(gf?.pool_balance ?? 0);
|
|
6
|
-
const histRow =
|
|
8
|
+
const histRow = (await dbOne(`
|
|
7
9
|
SELECT AVG(deposited_this_period) as avg_dep
|
|
8
10
|
FROM settlement_periods
|
|
9
11
|
WHERE status = 'completed' AND started_at > datetime('now', '-28 days')
|
|
10
|
-
`)
|
|
12
|
+
`));
|
|
11
13
|
const historyAverage = Math.round(Number(histRow?.avg_dep ?? 0) * 100) / 100;
|
|
12
14
|
let healthLevel = 'cold_start';
|
|
13
15
|
let distributionCap = 1.0;
|
|
@@ -23,15 +25,15 @@ export function registerDashboardsRoutes(app, deps) {
|
|
|
23
25
|
else
|
|
24
26
|
healthLevel = 'critical';
|
|
25
27
|
}
|
|
26
|
-
const lastSettled =
|
|
28
|
+
const lastSettled = await dbOne(`
|
|
27
29
|
SELECT effective_unit_cash, payout_rate, started_at FROM settlement_periods
|
|
28
30
|
WHERE status = 'completed' ORDER BY started_at DESC LIMIT 1
|
|
29
|
-
`)
|
|
30
|
-
const recentPaused =
|
|
31
|
+
`);
|
|
32
|
+
const recentPaused = await dbOne(`
|
|
31
33
|
SELECT period_id, started_at, note FROM settlement_periods
|
|
32
34
|
WHERE status = 'paused_low_water' AND started_at > datetime('now', '-7 days')
|
|
33
35
|
ORDER BY started_at DESC LIMIT 1
|
|
34
|
-
`)
|
|
36
|
+
`);
|
|
35
37
|
res.json({
|
|
36
38
|
pool_balance: poolBalance,
|
|
37
39
|
history_average: historyAverage,
|
|
@@ -42,12 +44,12 @@ export function registerDashboardsRoutes(app, deps) {
|
|
|
42
44
|
last_settlement: lastSettled || null,
|
|
43
45
|
});
|
|
44
46
|
});
|
|
45
|
-
app.get('/api/shares/dashboard', (req, res) => {
|
|
47
|
+
app.get('/api/shares/dashboard', async (req, res) => {
|
|
46
48
|
const user = auth(req, res);
|
|
47
49
|
if (!user)
|
|
48
50
|
return;
|
|
49
51
|
const userId = user.id;
|
|
50
|
-
const bought =
|
|
52
|
+
const bought = await dbAll(`
|
|
51
53
|
SELECT
|
|
52
54
|
o.id as order_id,
|
|
53
55
|
o.updated_at as completed_at,
|
|
@@ -65,8 +67,8 @@ export function registerDashboardsRoutes(app, deps) {
|
|
|
65
67
|
WHERE o.buyer_id = ? AND o.status = 'completed'
|
|
66
68
|
ORDER BY o.updated_at DESC
|
|
67
69
|
LIMIT 50
|
|
68
|
-
|
|
69
|
-
const highComm =
|
|
70
|
+
`, [userId, userId, userId, userId, userId, userId]);
|
|
71
|
+
const highComm = await dbAll(`
|
|
70
72
|
SELECT p.id, p.title, p.price, p.commission_rate, p.images, p.category,
|
|
71
73
|
(SELECT COUNT(*) FROM orders o WHERE o.product_id = p.id AND o.status = 'completed') as sales_count
|
|
72
74
|
FROM products p
|
|
@@ -76,8 +78,8 @@ export function registerDashboardsRoutes(app, deps) {
|
|
|
76
78
|
AND p.id NOT IN (SELECT product_id FROM orders WHERE buyer_id = ? AND status = 'completed')
|
|
77
79
|
ORDER BY p.commission_rate DESC, sales_count DESC
|
|
78
80
|
LIMIT 10
|
|
79
|
-
|
|
80
|
-
const myCreations =
|
|
81
|
+
`, [userId, userId]);
|
|
82
|
+
const myCreations = await dbAll(`
|
|
81
83
|
SELECT s.id, s.type, s.title, s.external_platform, s.external_url,
|
|
82
84
|
s.related_product_id, s.related_order_id, s.related_anchor, p.title as product_title,
|
|
83
85
|
s.click_count, s.like_count, s.created_at,
|
|
@@ -89,7 +91,7 @@ export function registerDashboardsRoutes(app, deps) {
|
|
|
89
91
|
WHERE s.owner_id = ? AND s.status = 'active'
|
|
90
92
|
ORDER BY s.created_at DESC
|
|
91
93
|
LIMIT 30
|
|
92
|
-
|
|
94
|
+
`, [userId]);
|
|
93
95
|
res.json({
|
|
94
96
|
bought_products: bought,
|
|
95
97
|
high_commission_products: highComm,
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
+
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerDisputeCasesRoutes(app, deps) {
|
|
2
3
|
const { db, auth, getUser, generateId, piiSanitize, detectFraud, commentBlocklistHit, llmModerateComment } = deps;
|
|
3
4
|
// 公共发言门槛 — 防新号/小号刷评论/投票
|
|
4
5
|
// 至少满足其一:账户 >= 3 天 / 完成过 >= 1 单 / lifetime_score >= 5
|
|
5
|
-
function meetsPublicSpeechThreshold(user) {
|
|
6
|
+
async function meetsPublicSpeechThreshold(user) {
|
|
6
7
|
const lifetime = Number(user.lifetime_score || 0);
|
|
7
8
|
if (lifetime >= 5)
|
|
8
9
|
return { ok: true };
|
|
9
|
-
const completed =
|
|
10
|
+
const completed = (await dbOne(`SELECT COUNT(*) as n FROM orders WHERE (buyer_id = ? OR seller_id = ?) AND status = 'completed'`, [user.id, user.id])).n;
|
|
10
11
|
if (completed >= 1)
|
|
11
12
|
return { ok: true };
|
|
12
13
|
const created = user.created_at ? new Date(String(user.created_at).replace(' ', 'T') + 'Z').getTime() : 0;
|
|
@@ -15,7 +16,7 @@ export function registerDisputeCasesRoutes(app, deps) {
|
|
|
15
16
|
return { ok: false, reason: '账号需 ≥ 3 天 或 完成 ≥ 1 单 或 lifetime_score ≥ 5 才能公开发言(防小号刷量)' };
|
|
16
17
|
}
|
|
17
18
|
// 公开列表(全网)— 判例库总览
|
|
18
|
-
app.get('/api/disputes/cases', (req, res) => {
|
|
19
|
+
app.get('/api/disputes/cases', async (req, res) => {
|
|
19
20
|
const limit = Math.min(50, Math.max(5, Number(req.query.limit) || 20));
|
|
20
21
|
const category = req.query.category ? String(req.query.category) : null;
|
|
21
22
|
const winner = req.query.winner ? String(req.query.winner) : null;
|
|
@@ -47,7 +48,7 @@ export function registerDisputeCasesRoutes(app, deps) {
|
|
|
47
48
|
orderSql = 'comment_count DESC, fairness_yes DESC, published_at DESC';
|
|
48
49
|
else if (sort === 'fair')
|
|
49
50
|
orderSql = 'fairness_yes DESC, comment_count DESC, published_at DESC';
|
|
50
|
-
const rows =
|
|
51
|
+
const rows = await dbAll(`
|
|
51
52
|
SELECT id, product_id, category_tag, winner, resolution, amount_bucket,
|
|
52
53
|
fairness_yes, fairness_no, comment_count, published_at,
|
|
53
54
|
(SELECT title FROM products WHERE id = dispute_cases.product_id) as product_title,
|
|
@@ -56,30 +57,30 @@ export function registerDisputeCasesRoutes(app, deps) {
|
|
|
56
57
|
${whereSql}
|
|
57
58
|
ORDER BY ${orderSql}
|
|
58
59
|
LIMIT ?
|
|
59
|
-
|
|
60
|
+
`, [...args, limit]);
|
|
60
61
|
// 类目统计(侧栏过滤用)— 受 q 影响(搜索时只显匹配 q 的类目计数)
|
|
61
62
|
const catCountWhere = q ? `WHERE (ruling_text LIKE ? OR buyer_argument LIKE ? OR seller_argument LIKE ? OR resolution LIKE ?
|
|
62
63
|
OR EXISTS (SELECT 1 FROM products p WHERE p.id = dispute_cases.product_id AND p.title LIKE ?))` : '';
|
|
63
64
|
const catCountArgs = q ? Array(5).fill('%' + q.replace(/[%_]/g, '\\$&') + '%') : [];
|
|
64
|
-
const categoryCounts =
|
|
65
|
+
const categoryCounts = await dbAll(`SELECT category_tag, COUNT(*) as n FROM dispute_cases ${catCountWhere} GROUP BY category_tag ORDER BY n DESC`, catCountArgs);
|
|
65
66
|
res.json({ items: rows, category_counts: categoryCounts, total: rows.length, query: q, sort });
|
|
66
67
|
});
|
|
67
68
|
// 公开列表(按商品)
|
|
68
|
-
app.get('/api/disputes/cases/by-product/:product_id', (req, res) => {
|
|
69
|
-
const rows =
|
|
69
|
+
app.get('/api/disputes/cases/by-product/:product_id', async (req, res) => {
|
|
70
|
+
const rows = await dbAll(`
|
|
70
71
|
SELECT id, category_tag, winner, resolution, amount_bucket, ruling_text,
|
|
71
72
|
fairness_yes, fairness_no, comment_count, published_at
|
|
72
73
|
FROM dispute_cases
|
|
73
74
|
WHERE product_id = ?
|
|
74
75
|
ORDER BY published_at DESC
|
|
75
76
|
LIMIT 50
|
|
76
|
-
|
|
77
|
+
`, [req.params.product_id]);
|
|
77
78
|
res.json({ items: rows });
|
|
78
79
|
});
|
|
79
80
|
// 案件详情(含评论 + 评论者身份标签)
|
|
80
|
-
app.get('/api/disputes/cases/:case_id', (req, res) => {
|
|
81
|
+
app.get('/api/disputes/cases/:case_id', async (req, res) => {
|
|
81
82
|
const me = getUser(req);
|
|
82
|
-
const c =
|
|
83
|
+
const c = await dbOne(`SELECT * FROM dispute_cases WHERE id = ?`, [req.params.case_id]);
|
|
83
84
|
if (!c)
|
|
84
85
|
return void res.status(404).json({ error: '判例不存在' });
|
|
85
86
|
// 不外露内部 ID(buyer_id/dispute_id/order_id 不返回给前端)
|
|
@@ -91,7 +92,7 @@ export function registerDisputeCasesRoutes(app, deps) {
|
|
|
91
92
|
product_id: c.product_id, seller_id: c.seller_id,
|
|
92
93
|
};
|
|
93
94
|
// 评论 + 自动身份标签
|
|
94
|
-
const rawComments =
|
|
95
|
+
const rawComments = await dbAll(`
|
|
95
96
|
SELECT dc.*, u.handle, u.name, u.role, u.lifetime_score,
|
|
96
97
|
(SELECT COUNT(*) FROM orders o
|
|
97
98
|
WHERE o.buyer_id = dc.commenter_id AND o.product_id = ? AND o.status = 'completed') as bought_count,
|
|
@@ -105,7 +106,7 @@ export function registerDisputeCasesRoutes(app, deps) {
|
|
|
105
106
|
(u.role IN ('verifier','arbitrator')) DESC,
|
|
106
107
|
dc.created_at DESC
|
|
107
108
|
LIMIT 50
|
|
108
|
-
|
|
109
|
+
`, [c.product_id, c.product_id, c.id]);
|
|
109
110
|
// 脱敏:anonymous=1 时清除 handle/name/commenter_id(保留贡献标签字段:bought/same_cat/role/lifetime)
|
|
110
111
|
// 验证员/仲裁员角色在脱敏时降级为 'staff'(避免小池子反推身份)
|
|
111
112
|
const anonymize = (row) => {
|
|
@@ -116,12 +117,12 @@ export function registerDisputeCasesRoutes(app, deps) {
|
|
|
116
117
|
};
|
|
117
118
|
// W5: 取所有子回复,按 parent_comment_id 分组挂在 comments 下
|
|
118
119
|
const commentIds = rawComments.map(r => r.id);
|
|
119
|
-
const rawReplies = commentIds.length > 0 ?
|
|
120
|
+
const rawReplies = commentIds.length > 0 ? await dbAll(`
|
|
120
121
|
SELECT r.*, u.handle, u.name, u.role, u.lifetime_score
|
|
121
122
|
FROM dispute_comment_replies r LEFT JOIN users u ON u.id = r.replier_id
|
|
122
123
|
WHERE r.parent_comment_id IN (${commentIds.map(() => '?').join(',')})
|
|
123
124
|
ORDER BY r.created_at ASC
|
|
124
|
-
|
|
125
|
+
`, commentIds) : [];
|
|
125
126
|
const repliesByParent = new Map();
|
|
126
127
|
for (const r of rawReplies) {
|
|
127
128
|
const pid = String(r.parent_comment_id);
|
|
@@ -136,7 +137,7 @@ export function registerDisputeCasesRoutes(app, deps) {
|
|
|
136
137
|
// 我的公正度投票(如已投)
|
|
137
138
|
let myVote = null;
|
|
138
139
|
if (me) {
|
|
139
|
-
const v =
|
|
140
|
+
const v = await dbOne('SELECT vote FROM dispute_fairness_votes WHERE case_id = ? AND voter_id = ?', [c.id, me.id]);
|
|
140
141
|
myVote = v?.vote || null;
|
|
141
142
|
}
|
|
142
143
|
// 我是否当事人(决定能否评论 / 投票)
|
|
@@ -148,10 +149,10 @@ export function registerDisputeCasesRoutes(app, deps) {
|
|
|
148
149
|
const user = auth(req, res);
|
|
149
150
|
if (!user)
|
|
150
151
|
return;
|
|
151
|
-
const gate = meetsPublicSpeechThreshold(user);
|
|
152
|
+
const gate = await meetsPublicSpeechThreshold(user);
|
|
152
153
|
if (!gate.ok)
|
|
153
154
|
return void res.status(403).json({ error: gate.reason, error_code: 'SPEECH_THRESHOLD' });
|
|
154
|
-
const c =
|
|
155
|
+
const c = await dbOne(`SELECT id, buyer_id, seller_id, arbitrator_id FROM dispute_cases WHERE id = ?`, [req.params.case_id]);
|
|
155
156
|
if (!c)
|
|
156
157
|
return void res.status(404).json({ error: '判例不存在' });
|
|
157
158
|
if (user.id === c.buyer_id || user.id === c.seller_id || user.id === c.arbitrator_id) {
|
|
@@ -194,16 +195,16 @@ export function registerDisputeCasesRoutes(app, deps) {
|
|
|
194
195
|
const user = auth(req, res);
|
|
195
196
|
if (!user)
|
|
196
197
|
return;
|
|
197
|
-
const gate = meetsPublicSpeechThreshold(user);
|
|
198
|
+
const gate = await meetsPublicSpeechThreshold(user);
|
|
198
199
|
if (!gate.ok)
|
|
199
200
|
return void res.status(403).json({ error: gate.reason, error_code: 'SPEECH_THRESHOLD' });
|
|
200
|
-
const c =
|
|
201
|
+
const c = await dbOne(`SELECT id, buyer_id, seller_id, arbitrator_id FROM dispute_cases WHERE id = ?`, [req.params.case_id]);
|
|
201
202
|
if (!c)
|
|
202
203
|
return void res.status(404).json({ error: '判例不存在' });
|
|
203
204
|
if (user.id === c.buyer_id || user.id === c.seller_id || user.id === c.arbitrator_id) {
|
|
204
205
|
return void res.status(403).json({ error: '当事人禁止评论', error_code: 'PARTY_NO_COMMENT' });
|
|
205
206
|
}
|
|
206
|
-
const parent =
|
|
207
|
+
const parent = await dbOne(`SELECT id FROM dispute_comments WHERE id = ? AND case_id = ?`, [req.params.comment_id, c.id]);
|
|
207
208
|
if (!parent)
|
|
208
209
|
return void res.status(404).json({ error: '父评论不存在' });
|
|
209
210
|
const rawBody = String(req.body?.body || '').trim();
|
|
@@ -230,17 +231,17 @@ export function registerDisputeCasesRoutes(app, deps) {
|
|
|
230
231
|
res.json({ success: true, id: rid, flag_reasons: repReasons });
|
|
231
232
|
});
|
|
232
233
|
// 公正度投票(👍 / 👎)— 一人一案一票
|
|
233
|
-
app.post('/api/disputes/cases/:case_id/fairness', (req, res) => {
|
|
234
|
+
app.post('/api/disputes/cases/:case_id/fairness', async (req, res) => {
|
|
234
235
|
const user = auth(req, res);
|
|
235
236
|
if (!user)
|
|
236
237
|
return;
|
|
237
|
-
const gate = meetsPublicSpeechThreshold(user);
|
|
238
|
+
const gate = await meetsPublicSpeechThreshold(user);
|
|
238
239
|
if (!gate.ok)
|
|
239
240
|
return void res.status(403).json({ error: gate.reason, error_code: 'SPEECH_THRESHOLD' });
|
|
240
241
|
const vote = req.body?.vote;
|
|
241
242
|
if (vote !== 'yes' && vote !== 'no')
|
|
242
243
|
return void res.status(400).json({ error: 'vote 必须是 yes 或 no' });
|
|
243
|
-
const c =
|
|
244
|
+
const c = await dbOne(`SELECT id, buyer_id, seller_id, arbitrator_id FROM dispute_cases WHERE id = ?`, [req.params.case_id]);
|
|
244
245
|
if (!c)
|
|
245
246
|
return void res.status(404).json({ error: '判例不存在' });
|
|
246
247
|
if (user.id === c.buyer_id || user.id === c.seller_id || user.id === c.arbitrator_id) {
|
|
@@ -1,29 +1,30 @@
|
|
|
1
|
+
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerDisputesReadRoutes(app, deps) {
|
|
2
3
|
const { db, auth, errorRes, getOpenDisputes, getDisputeDetails, getEvidenceRequests, listEvidenceFiles, isEligibleArbitrator } = deps;
|
|
3
4
|
// 仲裁员:查看所有开放争议
|
|
4
|
-
app.get('/api/disputes', (req, res) => {
|
|
5
|
+
app.get('/api/disputes', async (req, res) => {
|
|
5
6
|
const user = auth(req, res);
|
|
6
7
|
if (!user)
|
|
7
8
|
return;
|
|
8
9
|
const elig = isEligibleArbitrator(user.id);
|
|
9
10
|
if (!elig.ok)
|
|
10
11
|
return void errorRes(res, 403, 'NOT_ARBITRATOR', elig.reason || '仅限仲裁员访问');
|
|
11
|
-
res.json(getOpenDisputes(db));
|
|
12
|
+
res.json(await getOpenDisputes(db));
|
|
12
13
|
});
|
|
13
14
|
// A2 同类判例推荐
|
|
14
|
-
app.get('/api/disputes/:id/similar-cases', (req, res) => {
|
|
15
|
+
app.get('/api/disputes/:id/similar-cases', async (req, res) => {
|
|
15
16
|
const user = auth(req, res);
|
|
16
17
|
if (!user)
|
|
17
18
|
return;
|
|
18
|
-
const dispute =
|
|
19
|
+
const dispute = await dbOne('SELECT id, order_id, reason, initiator_id, defendant_id FROM disputes WHERE id = ?', [req.params.id]);
|
|
19
20
|
if (!dispute)
|
|
20
21
|
return void res.status(404).json({ error: '争议不存在' });
|
|
21
22
|
const role = user.role;
|
|
22
23
|
if (dispute.initiator_id !== user.id && dispute.defendant_id !== user.id && role !== 'arbitrator') {
|
|
23
24
|
return void res.status(403).json({ error: '无权查看' });
|
|
24
25
|
}
|
|
25
|
-
const order =
|
|
26
|
-
const productCategory = order ?
|
|
26
|
+
const order = await dbOne('SELECT product_id FROM orders WHERE id = ?', [dispute.order_id]);
|
|
27
|
+
const productCategory = order ? (await dbOne('SELECT category FROM products WHERE id = ?', [order.product_id]))?.category : null;
|
|
27
28
|
const reasonWords = (dispute.reason || '').split(/[\s,,。;\n]+/).filter(w => w.length >= 2).slice(0, 3);
|
|
28
29
|
const results = [];
|
|
29
30
|
const seen = new Set();
|
|
@@ -35,7 +36,7 @@ export function registerDisputesReadRoutes(app, deps) {
|
|
|
35
36
|
};
|
|
36
37
|
// ① 同 product 类目
|
|
37
38
|
if (productCategory) {
|
|
38
|
-
const r1 =
|
|
39
|
+
const r1 = await dbAll(`
|
|
39
40
|
SELECT dc.id, dc.product_id, dc.category_tag, dc.winner, dc.resolution,
|
|
40
41
|
dc.amount_bucket, dc.fairness_yes, dc.comment_count, dc.published_at,
|
|
41
42
|
(SELECT title FROM products WHERE id = dc.product_id) as product_title,
|
|
@@ -44,7 +45,7 @@ export function registerDisputesReadRoutes(app, deps) {
|
|
|
44
45
|
JOIN products p ON p.id = dc.product_id
|
|
45
46
|
WHERE p.category = ? AND (dc.dispute_id IS NULL OR dc.dispute_id != ?)
|
|
46
47
|
ORDER BY dc.published_at DESC LIMIT 3
|
|
47
|
-
|
|
48
|
+
`, [productCategory, dispute.id]);
|
|
48
49
|
r1.forEach(pushIfNew);
|
|
49
50
|
}
|
|
50
51
|
// ② reason 关键词命中 ruling_text / 双方陈述
|
|
@@ -53,7 +54,7 @@ export function registerDisputesReadRoutes(app, deps) {
|
|
|
53
54
|
if (results.length >= 3)
|
|
54
55
|
break;
|
|
55
56
|
const pat = '%' + w.replace(/[%_]/g, '\\$&') + '%';
|
|
56
|
-
const r2 =
|
|
57
|
+
const r2 = await dbAll(`
|
|
57
58
|
SELECT dc.id, dc.product_id, dc.category_tag, dc.winner, dc.resolution,
|
|
58
59
|
dc.amount_bucket, dc.fairness_yes, dc.comment_count, dc.published_at,
|
|
59
60
|
(SELECT title FROM products WHERE id = dc.product_id) as product_title,
|
|
@@ -62,13 +63,13 @@ export function registerDisputesReadRoutes(app, deps) {
|
|
|
62
63
|
WHERE (dc.dispute_id IS NULL OR dc.dispute_id != ?)
|
|
63
64
|
AND (dc.ruling_text LIKE ? OR dc.buyer_argument LIKE ? OR dc.seller_argument LIKE ?)
|
|
64
65
|
ORDER BY dc.published_at DESC LIMIT 3
|
|
65
|
-
|
|
66
|
+
`, [dispute.id, pat, pat, pat]);
|
|
66
67
|
r2.forEach(pushIfNew);
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
70
|
// ③ 兜底:最近 3 条已发布判例
|
|
70
71
|
if (results.length < 3) {
|
|
71
|
-
const r3 =
|
|
72
|
+
const r3 = await dbAll(`
|
|
72
73
|
SELECT dc.id, dc.product_id, dc.category_tag, dc.winner, dc.resolution,
|
|
73
74
|
dc.amount_bucket, dc.fairness_yes, dc.comment_count, dc.published_at,
|
|
74
75
|
(SELECT title FROM products WHERE id = dc.product_id) as product_title,
|
|
@@ -76,30 +77,29 @@ export function registerDisputesReadRoutes(app, deps) {
|
|
|
76
77
|
FROM dispute_cases dc
|
|
77
78
|
WHERE (dc.dispute_id IS NULL OR dc.dispute_id != ?)
|
|
78
79
|
ORDER BY dc.published_at DESC LIMIT 3
|
|
79
|
-
|
|
80
|
+
`, [dispute.id]);
|
|
80
81
|
r3.forEach(pushIfNew);
|
|
81
82
|
}
|
|
82
83
|
res.json({ items: results.slice(0, 3), product_category: productCategory, reason_keywords: reasonWords });
|
|
83
84
|
});
|
|
84
85
|
// 详情聚合(含 W4 timeline + chain ruling)
|
|
85
|
-
app.get('/api/disputes/:id', (req, res) => {
|
|
86
|
+
app.get('/api/disputes/:id', async (req, res) => {
|
|
86
87
|
const user = auth(req, res);
|
|
87
88
|
if (!user)
|
|
88
89
|
return;
|
|
89
|
-
const dispute = getDisputeDetails(db, req.params.id);
|
|
90
|
+
const dispute = await getDisputeDetails(db, req.params.id);
|
|
90
91
|
if (!dispute)
|
|
91
92
|
return void res.status(404).json({ error: '争议不存在' });
|
|
92
93
|
const role = user.role;
|
|
93
94
|
// 允许:发起方、被告方、物流方、仲裁员
|
|
94
|
-
const orderForAuth =
|
|
95
|
-
.get(dispute.order_id);
|
|
95
|
+
const orderForAuth = await dbOne('SELECT logistics_id FROM orders WHERE id = ?', [dispute.order_id]);
|
|
96
96
|
const isLogisticsParty = orderForAuth?.logistics_id === user.id;
|
|
97
97
|
if (dispute.initiator_id !== user.id && dispute.defendant_id !== user.id
|
|
98
98
|
&& !isLogisticsParty && role !== 'arbitrator') {
|
|
99
99
|
return void res.status(403).json({ error: '无权查看此争议' });
|
|
100
100
|
}
|
|
101
101
|
// 原告证据 — 从状态机历史中取 disputed 转移时附带的
|
|
102
|
-
const hist =
|
|
102
|
+
const hist = await dbOne(`SELECT evidence_ids FROM order_state_history WHERE order_id = ? AND to_status = 'disputed'`, [dispute.order_id]);
|
|
103
103
|
// P1 fix: 单条脏 JSON 不应封死整个 dispute 详情
|
|
104
104
|
const safeJsonArr = (s) => {
|
|
105
105
|
try {
|
|
@@ -112,26 +112,24 @@ export function registerDisputesReadRoutes(app, deps) {
|
|
|
112
112
|
};
|
|
113
113
|
const plaintiffEvidenceIds = hist ? safeJsonArr(hist.evidence_ids) : [];
|
|
114
114
|
const defEvidenceIds = safeJsonArr(dispute.defendant_evidence_ids);
|
|
115
|
-
const fetchEvidence = (ids) => ids.length
|
|
116
|
-
?
|
|
115
|
+
const fetchEvidence = async (ids) => ids.length
|
|
116
|
+
? await dbAll(`SELECT * FROM evidence WHERE id IN (${ids.map(() => '?').join(',')})`, ids)
|
|
117
117
|
: [];
|
|
118
|
-
const evidenceRequests = getEvidenceRequests(db, req.params.id);
|
|
118
|
+
const evidenceRequests = await getEvidenceRequests(db, req.params.id);
|
|
119
119
|
const myPendingRequests = evidenceRequests.filter((r) => r.requested_from_id === user.id && r.status === 'pending');
|
|
120
|
-
const order =
|
|
121
|
-
.get(dispute.order_id);
|
|
120
|
+
const order = await dbOne('SELECT buyer_id, seller_id, logistics_id FROM orders WHERE id = ?', [dispute.order_id]);
|
|
122
121
|
const partyIds = [dispute.initiator_id, dispute.defendant_id, order?.logistics_id].filter(Boolean);
|
|
123
|
-
const parties = [...new Set(partyIds)].map(id =>
|
|
122
|
+
const parties = (await Promise.all([...new Set(partyIds)].map(id => dbOne('SELECT id, name, role FROM users WHERE id = ?', [id])))).filter(Boolean);
|
|
124
123
|
const partyEvidenceIds = safeJsonArr(dispute.party_evidence_ids);
|
|
125
|
-
const orderParties =
|
|
126
|
-
.get(dispute.order_id);
|
|
124
|
+
const orderParties = await dbOne('SELECT buyer_id, seller_id, logistics_id FROM orders WHERE id = ?', [dispute.order_id]);
|
|
127
125
|
const allPartyIds = [
|
|
128
126
|
orderParties?.buyer_id, orderParties?.seller_id, orderParties?.logistics_id,
|
|
129
127
|
dispute.initiator_id, dispute.defendant_id
|
|
130
128
|
].filter(Boolean);
|
|
131
129
|
const isParty = allPartyIds.includes(user.id);
|
|
132
|
-
const plaintiffEvidence = fetchEvidence(plaintiffEvidenceIds);
|
|
133
|
-
const defendantEvidence = fetchEvidence(defEvidenceIds);
|
|
134
|
-
const partyEvidence = fetchEvidence(partyEvidenceIds);
|
|
130
|
+
const plaintiffEvidence = await fetchEvidence(plaintiffEvidenceIds);
|
|
131
|
+
const defendantEvidence = await fetchEvidence(defEvidenceIds);
|
|
132
|
+
const partyEvidence = await fetchEvidence(partyEvidenceIds);
|
|
135
133
|
// W4 timeline 归一化
|
|
136
134
|
const partyMap = new Map();
|
|
137
135
|
partyMap.set(dispute.initiator_id, 'plaintiff');
|
|
@@ -149,15 +147,15 @@ export function registerDisputesReadRoutes(app, deps) {
|
|
|
149
147
|
return 'unknown';
|
|
150
148
|
};
|
|
151
149
|
const userById = new Map();
|
|
152
|
-
const loadUser = (uid) => {
|
|
150
|
+
const loadUser = async (uid) => {
|
|
153
151
|
if (!uid || userById.has(uid))
|
|
154
152
|
return;
|
|
155
|
-
const u =
|
|
153
|
+
const u = await dbOne('SELECT id, name, handle, role FROM users WHERE id = ?', [uid]);
|
|
156
154
|
if (u)
|
|
157
155
|
userById.set(uid, u);
|
|
158
156
|
};
|
|
159
|
-
loadUser(dispute.initiator_id);
|
|
160
|
-
loadUser(dispute.defendant_id);
|
|
157
|
+
await loadUser(dispute.initiator_id);
|
|
158
|
+
await loadUser(dispute.defendant_id);
|
|
161
159
|
const events = [];
|
|
162
160
|
// 1) open
|
|
163
161
|
events.push({
|
|
@@ -174,8 +172,8 @@ export function registerDisputesReadRoutes(app, deps) {
|
|
|
174
172
|
},
|
|
175
173
|
});
|
|
176
174
|
// 2) plaintiff evidence
|
|
177
|
-
const evToEvent = (ev, role) => {
|
|
178
|
-
loadUser(ev.uploader_id);
|
|
175
|
+
const evToEvent = async (ev, role) => {
|
|
176
|
+
await loadUser(ev.uploader_id);
|
|
179
177
|
let fr = [];
|
|
180
178
|
try {
|
|
181
179
|
fr = ev.flag_reasons ? JSON.parse(String(ev.flag_reasons)) : [];
|
|
@@ -204,7 +202,7 @@ export function registerDisputesReadRoutes(app, deps) {
|
|
|
204
202
|
};
|
|
205
203
|
};
|
|
206
204
|
for (const ev of plaintiffEvidence)
|
|
207
|
-
events.push(evToEvent(ev, 'plaintiff'));
|
|
205
|
+
events.push(await evToEvent(ev, 'plaintiff'));
|
|
208
206
|
// 3) defendant response — 用 defendant 第一条证据时间逼近 respond_at
|
|
209
207
|
if (dispute.defendant_notes) {
|
|
210
208
|
const firstDefTs = defendantEvidence[0]?.created_at;
|
|
@@ -218,11 +216,11 @@ export function registerDisputesReadRoutes(app, deps) {
|
|
|
218
216
|
});
|
|
219
217
|
}
|
|
220
218
|
for (const ev of defendantEvidence)
|
|
221
|
-
events.push(evToEvent(ev, 'defendant'));
|
|
219
|
+
events.push(await evToEvent(ev, 'defendant'));
|
|
222
220
|
// 4) party evidence (物流等)
|
|
223
221
|
for (const ev of partyEvidence) {
|
|
224
222
|
const r = orderRole(ev.uploader_id);
|
|
225
|
-
events.push(evToEvent(ev, r === 'unknown' ? 'party' : r));
|
|
223
|
+
events.push(await evToEvent(ev, r === 'unknown' ? 'party' : r));
|
|
226
224
|
}
|
|
227
225
|
// 5) evidence requests + submitted_items
|
|
228
226
|
for (const r of evidenceRequests) {
|
|
@@ -231,7 +229,7 @@ export function registerDisputesReadRoutes(app, deps) {
|
|
|
231
229
|
evidenceTypes = JSON.parse(String(r.evidence_types || '[]'));
|
|
232
230
|
}
|
|
233
231
|
catch { }
|
|
234
|
-
loadUser(r.requested_from_id);
|
|
232
|
+
await loadUser(r.requested_from_id);
|
|
235
233
|
events.push({
|
|
236
234
|
id: `req-${r.id}`,
|
|
237
235
|
type: 'evidence_request',
|
|
@@ -252,14 +250,14 @@ export function registerDisputesReadRoutes(app, deps) {
|
|
|
252
250
|
const submitted = (r.submitted_items || []);
|
|
253
251
|
for (const si of submitted) {
|
|
254
252
|
const role = orderRole(si.uploader_id);
|
|
255
|
-
const ev = evToEvent(si, role === 'unknown' ? 'party' : role);
|
|
253
|
+
const ev = await evToEvent(si, role === 'unknown' ? 'party' : role);
|
|
256
254
|
ev.meta = { ...(ev.meta || {}), in_response_to: r.id };
|
|
257
255
|
events.push(ev);
|
|
258
256
|
}
|
|
259
257
|
}
|
|
260
258
|
// 6) ruling — 从 order_events 签名链取
|
|
261
259
|
try {
|
|
262
|
-
const chainRows =
|
|
260
|
+
const chainRows = await dbAll(`SELECT actor_id, signed_at, payload_json FROM order_events WHERE order_id = ? ORDER BY seq ASC`, [dispute.order_id]);
|
|
263
261
|
for (const row of chainRows) {
|
|
264
262
|
let payload = {};
|
|
265
263
|
try {
|
|
@@ -270,7 +268,7 @@ export function registerDisputesReadRoutes(app, deps) {
|
|
|
270
268
|
}
|
|
271
269
|
const extra = (payload.extra || {});
|
|
272
270
|
if (extra.action === 'arbitration_ruling' && extra.dispute_id === dispute.id) {
|
|
273
|
-
loadUser(row.actor_id);
|
|
271
|
+
await loadUser(row.actor_id);
|
|
274
272
|
events.push({
|
|
275
273
|
id: `rule-${row.signed_at}`,
|
|
276
274
|
type: 'ruling',
|
|
@@ -324,12 +322,12 @@ export function registerDisputesReadRoutes(app, deps) {
|
|
|
324
322
|
});
|
|
325
323
|
});
|
|
326
324
|
// 当事人 + 仲裁员可查(meta only,blob 单独拉)
|
|
327
|
-
app.get('/api/disputes/:id/evidence-list', (req, res) => {
|
|
325
|
+
app.get('/api/disputes/:id/evidence-list', async (req, res) => {
|
|
328
326
|
const user = auth(req, res);
|
|
329
327
|
if (!user)
|
|
330
328
|
return;
|
|
331
329
|
try {
|
|
332
|
-
const rows = listEvidenceFiles(db, req.params.id, user.id);
|
|
330
|
+
const rows = await listEvidenceFiles(db, req.params.id, user.id);
|
|
333
331
|
res.json(rows);
|
|
334
332
|
}
|
|
335
333
|
catch (e) {
|
|
@@ -338,21 +336,17 @@ export function registerDisputesReadRoutes(app, deps) {
|
|
|
338
336
|
}
|
|
339
337
|
});
|
|
340
338
|
// 涉案三方(仲裁员选择发证据请求的对象)
|
|
341
|
-
app.get('/api/disputes/:id/parties', (req, res) => {
|
|
339
|
+
app.get('/api/disputes/:id/parties', async (req, res) => {
|
|
342
340
|
const user = auth(req, res);
|
|
343
341
|
if (!user)
|
|
344
342
|
return;
|
|
345
|
-
const dispute = getDisputeDetails(db, req.params.id);
|
|
343
|
+
const dispute = await getDisputeDetails(db, req.params.id);
|
|
346
344
|
if (!dispute)
|
|
347
345
|
return void res.status(404).json({ error: '争议不存在' });
|
|
348
|
-
const order =
|
|
349
|
-
.get(dispute.order_id);
|
|
346
|
+
const order = await dbOne('SELECT buyer_id, seller_id, logistics_id FROM orders WHERE id = ?', [dispute.order_id]);
|
|
350
347
|
const partyIds = [dispute.initiator_id, dispute.defendant_id, order?.logistics_id].filter(Boolean);
|
|
351
348
|
const uniqueIds = [...new Set(partyIds)];
|
|
352
|
-
const parties = uniqueIds.map(id =>
|
|
353
|
-
const u = db.prepare('SELECT id, name, role FROM users WHERE id = ?').get(id);
|
|
354
|
-
return u;
|
|
355
|
-
}).filter(Boolean);
|
|
349
|
+
const parties = (await Promise.all(uniqueIds.map(id => dbOne('SELECT id, name, role FROM users WHERE id = ?', [id])))).filter(Boolean);
|
|
356
350
|
res.json(parties);
|
|
357
351
|
});
|
|
358
352
|
}
|