@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,4 +1,5 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
2
3
|
const MANIFEST_DAILY_LIMIT = 20;
|
|
3
4
|
const THUMB_MAX_BYTES = 12000; // ~12KB base64 ≈ 9KB 原始图
|
|
4
5
|
function verifyManifestSig(hash, ownerId, contentType, byteSize, signedAt, apiKey, signature) {
|
|
@@ -7,8 +8,9 @@ function verifyManifestSig(hash, ownerId, contentType, byteSize, signedAt, apiKe
|
|
|
7
8
|
return expected === signature;
|
|
8
9
|
}
|
|
9
10
|
export function registerManifestsRoutes(app, deps) {
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
// db 已走 RFC-016 异步 seam(dbOne/dbAll/dbRun),不再直接用 deps.db
|
|
12
|
+
const { auth, safeRoles } = deps;
|
|
13
|
+
app.post('/api/manifests', async (req, res) => {
|
|
12
14
|
const me = auth(req, res);
|
|
13
15
|
if (!me)
|
|
14
16
|
return;
|
|
@@ -29,21 +31,20 @@ export function registerManifestsRoutes(app, deps) {
|
|
|
29
31
|
return void res.json({ error: '签名验证失败' });
|
|
30
32
|
}
|
|
31
33
|
// 日上限
|
|
32
|
-
const todayCount =
|
|
34
|
+
const todayCount = (await dbOne(`SELECT COUNT(*) as n FROM manifest_registry WHERE owner_id = ? AND created_at > datetime('now', '-1 day')`, [me.id])).n;
|
|
33
35
|
if (todayCount >= MANIFEST_DAILY_LIMIT)
|
|
34
36
|
return void res.json({ error: `每日上限 ${MANIFEST_DAILY_LIMIT} 条` });
|
|
35
37
|
if (related_product_id) {
|
|
36
|
-
const p =
|
|
38
|
+
const p = await dbOne("SELECT id FROM products WHERE id = ?", [related_product_id]);
|
|
37
39
|
if (!p)
|
|
38
40
|
return void res.json({ error: '关联商品不存在' });
|
|
39
41
|
}
|
|
40
42
|
try {
|
|
41
|
-
|
|
42
|
-
VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
|
43
|
-
.run(hash, me.id, content_type, byte_size, title || null, description || null, thumbnail_data_uri || null, signature, signed_at, related_product_id || null, related_anchor || null);
|
|
43
|
+
await dbRun(`INSERT INTO manifest_registry (hash, owner_id, content_type, byte_size, title, description, thumbnail_data_uri, signature, signed_at, related_product_id, related_anchor)
|
|
44
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?)`, [hash, me.id, content_type, byte_size, title || null, description || null, thumbnail_data_uri || null, signature, signed_at, related_product_id || null, related_anchor || null]);
|
|
44
45
|
// 创作者立即注册为 owner peer
|
|
45
|
-
|
|
46
|
-
VALUES (?,?,1,1,datetime('now'))
|
|
46
|
+
await dbRun(`INSERT OR REPLACE INTO peer_directory (peer_id, manifest_hash, is_owner, pin_intent, last_heartbeat)
|
|
47
|
+
VALUES (?,?,1,1,datetime('now'))`, [me.id, hash]);
|
|
47
48
|
res.json({ ok: true, hash });
|
|
48
49
|
}
|
|
49
50
|
catch (e) {
|
|
@@ -53,63 +54,63 @@ export function registerManifestsRoutes(app, deps) {
|
|
|
53
54
|
res.json({ error: '发布失败:' + msg });
|
|
54
55
|
}
|
|
55
56
|
});
|
|
56
|
-
app.get('/api/manifests/me', (req, res) => {
|
|
57
|
+
app.get('/api/manifests/me', async (req, res) => {
|
|
57
58
|
const me = auth(req, res);
|
|
58
59
|
if (!me)
|
|
59
60
|
return;
|
|
60
|
-
const rows =
|
|
61
|
+
const rows = await dbAll(`
|
|
61
62
|
SELECT m.*, p.title as product_title FROM manifest_registry m
|
|
62
63
|
LEFT JOIN products p ON p.id = m.related_product_id
|
|
63
64
|
WHERE m.owner_id = ? AND m.status != 'removed'
|
|
64
65
|
ORDER BY m.created_at DESC LIMIT 100
|
|
65
|
-
|
|
66
|
+
`, [me.id]);
|
|
66
67
|
res.json({ manifests: rows });
|
|
67
68
|
});
|
|
68
|
-
app.get('/api/manifests/:hash', (req, res) => {
|
|
69
|
+
app.get('/api/manifests/:hash', async (req, res) => {
|
|
69
70
|
const me = auth(req, res);
|
|
70
71
|
if (!me)
|
|
71
72
|
return;
|
|
72
|
-
const m =
|
|
73
|
+
const m = await dbOne(`SELECT * FROM manifest_registry WHERE hash = ?`, [req.params.hash]);
|
|
73
74
|
if (!m)
|
|
74
75
|
return void res.status(404).json({ error: 'manifest 不存在' });
|
|
75
76
|
if (m.status === 'removed' || m.status === 'takedown_admin')
|
|
76
77
|
return void res.json({ error: '内容已下架', removed: true, reason: m.takedown_reason || null });
|
|
77
|
-
const peers =
|
|
78
|
+
const peers = await dbAll(`
|
|
78
79
|
SELECT peer_id, is_owner, pin_intent, last_heartbeat FROM peer_directory
|
|
79
80
|
WHERE manifest_hash = ? AND last_heartbeat > datetime('now', '-5 minutes')
|
|
80
81
|
ORDER BY is_owner DESC, last_heartbeat DESC LIMIT 30
|
|
81
|
-
|
|
82
|
+
`, [req.params.hash]);
|
|
82
83
|
res.json({ manifest: m, peers });
|
|
83
84
|
});
|
|
84
|
-
app.get('/api/manifests/by-product/:pid', (req, res) => {
|
|
85
|
+
app.get('/api/manifests/by-product/:pid', async (req, res) => {
|
|
85
86
|
const user = auth(req, res);
|
|
86
87
|
if (!user)
|
|
87
88
|
return;
|
|
88
|
-
const rows =
|
|
89
|
+
const rows = await dbAll(`
|
|
89
90
|
SELECT m.*, u.name as owner_name FROM manifest_registry m
|
|
90
91
|
LEFT JOIN users u ON u.id = m.owner_id
|
|
91
92
|
WHERE m.related_product_id = ? AND m.status = 'active'
|
|
92
93
|
ORDER BY m.created_at DESC LIMIT 20
|
|
93
|
-
|
|
94
|
+
`, [req.params.pid]);
|
|
94
95
|
res.json({ manifests: rows });
|
|
95
96
|
});
|
|
96
|
-
app.get('/api/manifests/by-anchor/:anchor', (req, res) => {
|
|
97
|
+
app.get('/api/manifests/by-anchor/:anchor', async (req, res) => {
|
|
97
98
|
const user = auth(req, res);
|
|
98
99
|
if (!user)
|
|
99
100
|
return;
|
|
100
|
-
const rows =
|
|
101
|
+
const rows = await dbAll(`
|
|
101
102
|
SELECT m.*, u.name as owner_name FROM manifest_registry m
|
|
102
103
|
LEFT JOIN users u ON u.id = m.owner_id
|
|
103
104
|
WHERE m.related_anchor = ? AND m.status = 'active'
|
|
104
105
|
ORDER BY m.created_at DESC LIMIT 50
|
|
105
|
-
|
|
106
|
+
`, [req.params.anchor]);
|
|
106
107
|
res.json({ manifests: rows });
|
|
107
108
|
});
|
|
108
|
-
app.patch('/api/manifests/:hash/takedown', (req, res) => {
|
|
109
|
+
app.patch('/api/manifests/:hash/takedown', async (req, res) => {
|
|
109
110
|
const me = auth(req, res);
|
|
110
111
|
if (!me)
|
|
111
112
|
return;
|
|
112
|
-
const m =
|
|
113
|
+
const m = await dbOne("SELECT owner_id FROM manifest_registry WHERE hash = ?", [req.params.hash]);
|
|
113
114
|
if (!m)
|
|
114
115
|
return void res.status(404).json({ error: 'manifest 不存在' });
|
|
115
116
|
const isAdmin = me.role === 'admin' || safeRoles(me).includes('admin');
|
|
@@ -117,10 +118,9 @@ export function registerManifestsRoutes(app, deps) {
|
|
|
117
118
|
if (!isOwner && !isAdmin)
|
|
118
119
|
return void res.json({ error: '无权下架' });
|
|
119
120
|
const reason = (req.body?.reason || '').toString().slice(0, 200);
|
|
120
|
-
|
|
121
|
-
.run(isAdmin && !isOwner ? 'takedown_admin' : 'removed', reason, me.id, req.params.hash);
|
|
121
|
+
await dbRun(`UPDATE manifest_registry SET status = ?, takedown_reason = ?, takedown_at = datetime('now'), takedown_by = ? WHERE hash = ?`, [isAdmin && !isOwner ? 'takedown_admin' : 'removed', reason, me.id, req.params.hash]);
|
|
122
122
|
// 同步清空 peer directory(强制客户端 evict)
|
|
123
|
-
|
|
123
|
+
await dbRun("DELETE FROM peer_directory WHERE manifest_hash = ?", [req.params.hash]);
|
|
124
124
|
res.json({ ok: true });
|
|
125
125
|
});
|
|
126
126
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerMeDataRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
+
// db 已全量走 RFC-016 异步 seam(dbOne/dbAll),不再直接用 deps.db
|
|
4
|
+
const { auth } = deps;
|
|
3
5
|
// COP 飞轮: 完成订单 7d 引导发笔记
|
|
4
|
-
app.get('/api/me/note-prompts', (req, res) => {
|
|
6
|
+
app.get('/api/me/note-prompts', async (req, res) => {
|
|
5
7
|
const user = auth(req, res);
|
|
6
8
|
if (!user)
|
|
7
9
|
return;
|
|
8
|
-
const rows =
|
|
10
|
+
const rows = await dbAll(`
|
|
9
11
|
SELECT o.id as order_id, o.product_id, o.updated_at as completed_at, o.total_amount,
|
|
10
12
|
p.title as product_title, p.images as product_images
|
|
11
13
|
FROM orders o
|
|
@@ -19,7 +21,7 @@ export function registerMeDataRoutes(app, deps) {
|
|
|
19
21
|
)
|
|
20
22
|
ORDER BY o.updated_at DESC
|
|
21
23
|
LIMIT 10
|
|
22
|
-
|
|
24
|
+
`, [user.id, user.id]);
|
|
23
25
|
const prompts = rows.map(r => {
|
|
24
26
|
let firstImage = null;
|
|
25
27
|
try {
|
|
@@ -40,7 +42,7 @@ export function registerMeDataRoutes(app, deps) {
|
|
|
40
42
|
res.json({ prompts });
|
|
41
43
|
});
|
|
42
44
|
// COP P0-1: 数据导出(用户主权)
|
|
43
|
-
app.get('/api/me/export', (req, res) => {
|
|
45
|
+
app.get('/api/me/export', async (req, res) => {
|
|
44
46
|
const user = auth(req, res);
|
|
45
47
|
if (!user)
|
|
46
48
|
return;
|
|
@@ -51,33 +53,33 @@ export function registerMeDataRoutes(app, deps) {
|
|
|
51
53
|
notice: 'WebAZ COP 承诺:你的数据属于你。可随时导出,可随时迁出。',
|
|
52
54
|
};
|
|
53
55
|
try {
|
|
54
|
-
data.profile =
|
|
55
|
-
data.wallet =
|
|
56
|
-
data.orders =
|
|
57
|
-
data.shareables =
|
|
58
|
-
data.bookmarks =
|
|
59
|
-
data.likes =
|
|
60
|
-
data.follows_following =
|
|
61
|
-
data.follows_followers =
|
|
62
|
-
data.addresses =
|
|
63
|
-
data.kyc =
|
|
56
|
+
data.profile = await dbOne(`SELECT id, name, handle, role, region, bio, search_anchor, email, phone, permanent_code, created_at, reputation FROM users WHERE id = ?`, [uid]);
|
|
57
|
+
data.wallet = await dbOne(`SELECT balance, staked, escrowed, earned FROM wallets WHERE user_id = ?`, [uid]);
|
|
58
|
+
data.orders = await dbAll(`SELECT * FROM orders WHERE buyer_id = ? OR seller_id = ? ORDER BY created_at DESC LIMIT 1000`, [uid, uid]);
|
|
59
|
+
data.shareables = await dbAll(`SELECT * FROM shareables WHERE owner_id = ? AND status != 'removed'`, [uid]);
|
|
60
|
+
data.bookmarks = await dbAll(`SELECT b.*, s.title FROM shareable_bookmarks b LEFT JOIN shareables s ON s.id = b.shareable_id WHERE b.user_id = ?`, [uid]);
|
|
61
|
+
data.likes = await dbAll(`SELECT l.*, s.title FROM shareable_likes l LEFT JOIN shareables s ON s.id = l.shareable_id WHERE l.user_id = ?`, [uid]);
|
|
62
|
+
data.follows_following = await dbAll(`SELECT followee_id, created_at FROM follows WHERE follower_id = ?`, [uid]);
|
|
63
|
+
data.follows_followers = await dbAll(`SELECT follower_id, created_at FROM follows WHERE followee_id = ?`, [uid]);
|
|
64
|
+
data.addresses = await dbAll(`SELECT * FROM user_addresses WHERE user_id = ?`, [uid]);
|
|
65
|
+
data.kyc = await dbOne(`SELECT status, id_type, id_number_last4, submitted_at, reviewed_at FROM kyc_records WHERE user_id = ?`, [uid]);
|
|
64
66
|
// #1017: wallet_history 不存在 — 用 deposits + withdrawals + commissions 复合
|
|
65
67
|
try {
|
|
66
|
-
data.deposits =
|
|
68
|
+
data.deposits = await dbAll(`SELECT * FROM deposit_txns WHERE user_id = ? ORDER BY created_at DESC LIMIT 200`, [uid]);
|
|
67
69
|
}
|
|
68
70
|
catch {
|
|
69
71
|
data.deposits = [];
|
|
70
72
|
}
|
|
71
73
|
try {
|
|
72
|
-
data.withdrawals =
|
|
74
|
+
data.withdrawals = await dbAll(`SELECT * FROM withdrawal_requests WHERE user_id = ? ORDER BY created_at DESC LIMIT 200`, [uid]);
|
|
73
75
|
}
|
|
74
76
|
catch {
|
|
75
77
|
data.withdrawals = [];
|
|
76
78
|
}
|
|
77
|
-
data.commissions =
|
|
78
|
-
data.anchors =
|
|
79
|
-
data.notifications =
|
|
80
|
-
data.error_log =
|
|
79
|
+
data.commissions = await dbAll(`SELECT * FROM commission_records WHERE beneficiary_id = ? ORDER BY created_at DESC LIMIT 500`, [uid]) || [];
|
|
80
|
+
data.anchors = await dbAll(`SELECT anchor, target_kind, target_id, status, created_at FROM anchor_registry WHERE owner_id = ?`, [uid]);
|
|
81
|
+
data.notifications = await dbAll(`SELECT * FROM notifications WHERE user_id = ? ORDER BY created_at DESC LIMIT 500`, [uid]);
|
|
82
|
+
data.error_log = await dbAll(`SELECT id, source, message, created_at FROM error_log WHERE user_id = ? ORDER BY id DESC LIMIT 100`, [uid]) || [];
|
|
81
83
|
}
|
|
82
84
|
catch (e) {
|
|
83
85
|
console.warn('[export] partial:', e.message);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { dbOne } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
import { getNotifications, getUnreadCount, markRead } from '../../layer2-business/L2-6-notifications/notification-engine.js';
|
|
2
3
|
export function registerNotificationsRoutes(app, deps) {
|
|
3
4
|
const { db, auth, sseClients } = deps;
|
|
4
5
|
// SSE 实时推送流(EventSource 不支持自定义 header,URL ?key= 也兼容)
|
|
5
|
-
app.get('/api/notifications/stream', (req, res) => {
|
|
6
|
+
app.get('/api/notifications/stream', async (req, res) => {
|
|
6
7
|
const key = req.query.key ?? req.headers.authorization?.replace('Bearer ', '');
|
|
7
|
-
const user = key ?
|
|
8
|
+
const user = key ? await dbOne('SELECT * FROM users WHERE api_key = ?', [key]) : null;
|
|
8
9
|
if (!user)
|
|
9
10
|
return void res.status(401).end();
|
|
10
11
|
res.setHeader('Content-Type', 'text/event-stream');
|
|
@@ -13,7 +14,7 @@ export function registerNotificationsRoutes(app, deps) {
|
|
|
13
14
|
res.flushHeaders();
|
|
14
15
|
sseClients.set(user.id, res);
|
|
15
16
|
// 连接时推送未读数
|
|
16
|
-
const unread = getUnreadCount(db, user.id);
|
|
17
|
+
const unread = await getUnreadCount(db, user.id);
|
|
17
18
|
res.write(`data: ${JSON.stringify({ type: 'init', unread })}\n\n`);
|
|
18
19
|
// 心跳保活(每 30s)
|
|
19
20
|
const heartbeat = setInterval(() => {
|
|
@@ -29,13 +30,13 @@ export function registerNotificationsRoutes(app, deps) {
|
|
|
29
30
|
clearInterval(heartbeat);
|
|
30
31
|
});
|
|
31
32
|
});
|
|
32
|
-
app.get('/api/notifications', (req, res) => {
|
|
33
|
+
app.get('/api/notifications', async (req, res) => {
|
|
33
34
|
const user = auth(req, res);
|
|
34
35
|
if (!user)
|
|
35
36
|
return;
|
|
36
37
|
const onlyUnread = req.query.unread === '1';
|
|
37
|
-
const notifs = getNotifications(db, user.id, onlyUnread);
|
|
38
|
-
const unread = getUnreadCount(db, user.id);
|
|
38
|
+
const notifs = await getNotifications(db, user.id, onlyUnread);
|
|
39
|
+
const unread = await getUnreadCount(db, user.id);
|
|
39
40
|
res.json({ unread, notifications: notifs });
|
|
40
41
|
});
|
|
41
42
|
app.post('/api/notifications/read', (req, res) => {
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import { dbOne, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerOffersRoutes(app, deps) {
|
|
3
|
+
// db 仍保留:用于 DELETE /offers 的 db.transaction(撤回 offer + 释放质押守恒,better-sqlite3 事务须同步)。
|
|
4
|
+
// 其余只读/单写站点已走 RFC-016 异步 seam(dbOne/dbRun)。
|
|
2
5
|
const { db, auth, VALID_FULFILLMENT_TYPES } = deps;
|
|
3
|
-
app.patch('/api/offers/:id', (req, res) => {
|
|
6
|
+
app.patch('/api/offers/:id', async (req, res) => {
|
|
4
7
|
const user = auth(req, res);
|
|
5
8
|
if (!user)
|
|
6
9
|
return;
|
|
7
|
-
const offer =
|
|
10
|
+
const offer = await dbOne("SELECT * FROM products WHERE id = ? AND listing_id IS NOT NULL", [req.params.id]);
|
|
8
11
|
if (!offer)
|
|
9
12
|
return void res.status(404).json({ error: 'offer 不存在' });
|
|
10
13
|
if (offer.seller_id !== user.id)
|
|
@@ -48,27 +51,37 @@ export function registerOffersRoutes(app, deps) {
|
|
|
48
51
|
updates.push("updated_at = datetime('now')");
|
|
49
52
|
updates.push("freshness_ts = datetime('now')");
|
|
50
53
|
args.push(req.params.id);
|
|
51
|
-
|
|
54
|
+
await dbRun(`UPDATE products SET ${updates.join(', ')} WHERE id = ?`, args);
|
|
52
55
|
res.json({ success: true });
|
|
53
56
|
});
|
|
54
57
|
// 撤回 offer(status=warehouse + 释放 stake;不真删 product)
|
|
55
|
-
app.delete('/api/offers/:id', (req, res) => {
|
|
58
|
+
app.delete('/api/offers/:id', async (req, res) => {
|
|
56
59
|
const user = auth(req, res);
|
|
57
60
|
if (!user)
|
|
58
61
|
return;
|
|
59
|
-
const offer =
|
|
62
|
+
const offer = await dbOne("SELECT * FROM products WHERE id = ? AND listing_id IS NOT NULL", [req.params.id]);
|
|
60
63
|
if (!offer)
|
|
61
64
|
return void res.status(404).json({ error: 'offer 不存在' });
|
|
62
65
|
if (offer.seller_id !== user.id)
|
|
63
66
|
return void res.status(403).json({ error: '仅卖家本人可撤回' });
|
|
64
|
-
const pending =
|
|
67
|
+
const pending = (await dbOne(`SELECT COUNT(1) as n FROM orders WHERE product_id = ? AND status NOT IN ('completed','cancelled','refunded','expired')`, [req.params.id]));
|
|
65
68
|
if (pending.n > 0)
|
|
66
69
|
return void res.json({ error: `该 offer 有 ${pending.n} 个进行中订单,暂无法撤回` });
|
|
67
70
|
const stake = Number(offer.listing_stake_locked) || 0;
|
|
68
71
|
const tx = db.transaction(() => {
|
|
69
|
-
|
|
72
|
+
// await-gap P1(proactive sweep,与 #239 系列同类):offer 行先 await 读出 stake,再进同步 tx 释放。
|
|
73
|
+
// 并发双撤回会都读到同一 listing_stake_locked 并各退一次 → 双倍退质押(印钱)。CAS 抢占该 offer
|
|
74
|
+
// (仍未撤回 + stake 仍等于读到的值),changes!==1 即并发已撤回 → 抛回滚,先于任何钱写。
|
|
75
|
+
const flip = db.prepare(`UPDATE products SET status = 'warehouse', listing_stake_locked = 0, updated_at = datetime('now') WHERE id = ? AND status != 'warehouse' AND listing_stake_locked = ?`).run(req.params.id, stake);
|
|
76
|
+
if (flip.changes !== 1)
|
|
77
|
+
throw new Error('OFFER_ALREADY_WITHDRAWN');
|
|
70
78
|
if (stake > 0) {
|
|
71
|
-
|
|
79
|
+
// Codex #254 follow-up P2:钱包释放带 staked>=stake 守卫 + changes 校验。若 wallet 行缺失或
|
|
80
|
+
// staked < listing_stake_locked(历史漂移/并发异常/前序 bug),不能在已清零 products.stake 的同时
|
|
81
|
+
// 让 staked 变负或丢质押 —— changes!==1 即抛回滚整笔(products 不清零、listings 不递减、钱包不动)。
|
|
82
|
+
const rel = db.prepare(`UPDATE wallets SET balance = balance + ?, staked = staked - ? WHERE user_id = ? AND staked >= ?`).run(stake, stake, user.id, stake);
|
|
83
|
+
if (rel.changes !== 1)
|
|
84
|
+
throw new Error('OFFER_STAKE_INVARIANT_VIOLATION');
|
|
72
85
|
}
|
|
73
86
|
db.prepare(`UPDATE listings SET total_offers = MAX(0, total_offers - 1) WHERE id = ?`).run(String(offer.listing_id));
|
|
74
87
|
});
|
|
@@ -76,21 +89,26 @@ export function registerOffersRoutes(app, deps) {
|
|
|
76
89
|
tx();
|
|
77
90
|
}
|
|
78
91
|
catch (e) {
|
|
79
|
-
|
|
92
|
+
const m = e.message;
|
|
93
|
+
if (m === 'OFFER_ALREADY_WITHDRAWN')
|
|
94
|
+
return void res.status(409).json({ error: '该 offer 已撤回(请刷新)', error_code: 'OFFER_ALREADY_WITHDRAWN' });
|
|
95
|
+
if (m === 'OFFER_STAKE_INVARIANT_VIOLATION')
|
|
96
|
+
return void res.status(500).json({ error: '质押释放校验失败(资金状态异常,已回滚未做任何变更,请联系支持)', error_code: 'OFFER_STAKE_INVARIANT_VIOLATION' });
|
|
97
|
+
return void res.status(500).json({ error: String(m) });
|
|
80
98
|
}
|
|
81
99
|
res.json({ success: true, stake_released: stake });
|
|
82
100
|
});
|
|
83
101
|
// 刷新 freshness(卖家点 "现货确认")
|
|
84
|
-
app.post('/api/offers/:id/refresh', (req, res) => {
|
|
102
|
+
app.post('/api/offers/:id/refresh', async (req, res) => {
|
|
85
103
|
const user = auth(req, res);
|
|
86
104
|
if (!user)
|
|
87
105
|
return;
|
|
88
|
-
const offer =
|
|
106
|
+
const offer = await dbOne("SELECT seller_id FROM products WHERE id = ? AND listing_id IS NOT NULL", [req.params.id]);
|
|
89
107
|
if (!offer)
|
|
90
108
|
return void res.status(404).json({ error: 'offer 不存在' });
|
|
91
109
|
if (offer.seller_id !== user.id)
|
|
92
110
|
return void res.status(403).json({ error: '仅卖家本人可刷新' });
|
|
93
|
-
|
|
111
|
+
await dbRun(`UPDATE products SET freshness_ts = datetime('now') WHERE id = ?`, [req.params.id]);
|
|
94
112
|
res.json({ success: true });
|
|
95
113
|
});
|
|
96
114
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js';
|
|
1
2
|
export function registerOrdersActionRoutes(app, deps) {
|
|
2
3
|
const { db, auth, isTrustedRole, generateId, transition, notifyTransition, settleOrder, settleFault, detectFraud, createDispute, checkTimeouts, recordViolationReputation, broadcastSystemEvent } = deps;
|
|
3
4
|
// RFC-007 stage 2:卖家主动拒单 reason_code 白名单。
|
|
@@ -11,7 +12,7 @@ export function registerOrdersActionRoutes(app, deps) {
|
|
|
11
12
|
// 客观-声称理由:链下事实(外部已售/损毁),协议无确定性信号可自动核验 → 临时判责 + 举证窗口(stage 5 仲裁)。
|
|
12
13
|
const OBJECTIVE_DECLINE_REASONS = new Set(['stock_consumed_concurrent', 'stale_price_snapshot', 'force_majeure']);
|
|
13
14
|
// C-4: 卖家批量发货
|
|
14
|
-
app.post('/api/orders/batch-ship', (req, res) => {
|
|
15
|
+
app.post('/api/orders/batch-ship', async (req, res) => {
|
|
15
16
|
const user = auth(req, res);
|
|
16
17
|
if (!user)
|
|
17
18
|
return;
|
|
@@ -20,11 +21,14 @@ export function registerOrdersActionRoutes(app, deps) {
|
|
|
20
21
|
return void res.status(400).json({ error: 'order_ids 必填' });
|
|
21
22
|
if (order_ids.length > 100)
|
|
22
23
|
return void res.status(400).json({ error: '单次最多 100 单' });
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (
|
|
27
|
-
|
|
24
|
+
// 自发货(self-fulfill,Phase 1 默认):不传 logistics_company_id → logistics_id 留空,卖家自负后续流转。
|
|
25
|
+
// 只有传了物流公司时才校验其存在。
|
|
26
|
+
// RFC-016: 纯校验读 → 异步 seam(物流公司是否存在);循环内的逐单 read+write 仍同步(Phase 3 随订单事务迁)
|
|
27
|
+
if (logistics_company_id) {
|
|
28
|
+
const lc = await dbOne("SELECT id FROM users WHERE id = ? AND role = 'logistics'", [logistics_company_id]);
|
|
29
|
+
if (!lc)
|
|
30
|
+
return void res.status(400).json({ error: '物流公司不存在' });
|
|
31
|
+
}
|
|
28
32
|
const results = [];
|
|
29
33
|
const trackingMap = (tracking_numbers && typeof tracking_numbers === 'object') ? tracking_numbers : {};
|
|
30
34
|
for (const oid of order_ids) {
|
|
@@ -42,18 +46,22 @@ export function registerOrdersActionRoutes(app, deps) {
|
|
|
42
46
|
results.push({ order_id: oid, status: 'skipped', reason: `状态非 accepted (当前 ${o.status})` });
|
|
43
47
|
continue;
|
|
44
48
|
}
|
|
45
|
-
|
|
49
|
+
// 仅当指定了物流公司时才绑定;自发货保持 logistics_id 为空(seller self-fulfill)
|
|
50
|
+
if (logistics_company_id && !o.logistics_id) {
|
|
46
51
|
db.prepare("UPDATE orders SET logistics_id = ? WHERE id = ?").run(logistics_company_id, oid);
|
|
47
52
|
}
|
|
48
53
|
const tn = trackingMap[oid] ? String(trackingMap[oid]).slice(0, 50) : null;
|
|
54
|
+
// accepted→shipped 状态机要求 evidence(requiresEvidence)。【始终】写一条文字 evidence,
|
|
55
|
+
// 否则无单号(尤其自发货默认)会被状态机拒绝 → shipped:0 卡在 accepted。单号可之后补。
|
|
56
|
+
const evDesc = logistics_company_id
|
|
57
|
+
? (tn ? `批量发货 · 快递单号:${tn} · 物流方 ${logistics_company_id}` : `批量发货,已交付物流公司 ${logistics_company_id},快递单号待物流揽收后回传`)
|
|
58
|
+
: (tn ? `卖家自己发货(批量)· 快递单号:${tn}` : `卖家自己发货(批量·自提自送)—— 由卖家负责揽收/运输/送达,单号可之后补`);
|
|
49
59
|
const evIds = [];
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
const result = transition(db, oid, 'shipped', user.id, evIds, tn ? `批量发货 · ${tn}` : '批量发货');
|
|
60
|
+
const eid = generateId('evt');
|
|
61
|
+
db.prepare(`INSERT INTO evidence (id, order_id, uploader_id, type, description, file_hash)
|
|
62
|
+
VALUES (?,?,?,'description',?,?)`).run(eid, oid, user.id, evDesc, `hash_${Date.now()}`);
|
|
63
|
+
evIds.push(eid);
|
|
64
|
+
const result = transition(db, oid, 'shipped', user.id, evIds, evDesc);
|
|
57
65
|
if (!result.success) {
|
|
58
66
|
results.push({ order_id: oid, status: 'skipped', reason: result.error || '状态机拒绝' });
|
|
59
67
|
continue;
|
|
@@ -69,11 +77,12 @@ export function registerOrdersActionRoutes(app, deps) {
|
|
|
69
77
|
res.json({ success: true, shipped, skipped: results.filter(r => r.status === 'skipped').length, results });
|
|
70
78
|
});
|
|
71
79
|
// 买家确认面交完成 → 直接 completed + settleOrder
|
|
72
|
-
app.post('/api/orders/:id/confirm-in-person', (req, res) => {
|
|
80
|
+
app.post('/api/orders/:id/confirm-in-person', async (req, res) => {
|
|
73
81
|
const user = auth(req, res);
|
|
74
82
|
if (!user)
|
|
75
83
|
return;
|
|
76
|
-
|
|
84
|
+
// RFC-016: 校验读 → 异步 seam;下方 completed+history 写仍是同步 db.transaction(Phase 3 迁 pg 事务)
|
|
85
|
+
const order = await dbOne('SELECT * FROM orders WHERE id = ?', [req.params.id]);
|
|
77
86
|
if (!order)
|
|
78
87
|
return void res.status(404).json({ error: '订单不存在' });
|
|
79
88
|
if (order.fulfillment_mode !== 'in_person')
|
|
@@ -105,7 +114,7 @@ export function registerOrdersActionRoutes(app, deps) {
|
|
|
105
114
|
res.json({ success: true });
|
|
106
115
|
});
|
|
107
116
|
// 通用状态机 action — accept/ship/pickup/transit/deliver/confirm/dispute
|
|
108
|
-
app.post('/api/orders/:id/action', (req, res) => {
|
|
117
|
+
app.post('/api/orders/:id/action', async (req, res) => {
|
|
109
118
|
const user = auth(req, res);
|
|
110
119
|
if (!user)
|
|
111
120
|
return;
|
|
@@ -113,7 +122,8 @@ export function registerOrdersActionRoutes(app, deps) {
|
|
|
113
122
|
if (isTrustedRole(user))
|
|
114
123
|
return void res.status(403).json({ error: '受信角色不可参与订单流转', error_code: 'TRUSTED_ROLE_NO_TRADE' });
|
|
115
124
|
const { action, notes = '', evidence_description = '', logistics_company_id = '' } = req.body;
|
|
116
|
-
|
|
125
|
+
// RFC-016: 顶层校验读 → 异步 seam;state-machine / settle / decline 写序列仍同步(Phase 3 迁 pg 行锁+事务)
|
|
126
|
+
const order = await dbOne('SELECT * FROM orders WHERE id = ?', [req.params.id]);
|
|
117
127
|
if (!order)
|
|
118
128
|
return void res.status(404).json({ error: '订单不存在' });
|
|
119
129
|
// P0: 路由层 ownership 校验(engine 层只看 role,必须补 ownership)
|
|
@@ -190,16 +200,26 @@ export function registerOrdersActionRoutes(app, deps) {
|
|
|
190
200
|
});
|
|
191
201
|
}
|
|
192
202
|
// 主观(或窗口=0):立即违约结算(退款买家 + 按 stake_backing 罚没,守恒,绝不印钱)
|
|
203
|
+
// Codex #119 P1:这是资金结算路径,settleFault / completed transition 失败【绝不能】吞掉后仍报 success。
|
|
204
|
+
// 订单此刻已在 fault_seller(上面 transition 已提交)。只有结算 + 推进 completed 都成功才返回 success;
|
|
205
|
+
// 任一失败 → 返回 500 DECLINE_SETTLEMENT_FAILED(订单停在 fault_seller,可重试/人工/cron 终结),不谎称已退款。
|
|
193
206
|
const sysUser = db.prepare("SELECT id FROM users WHERE id = 'sys_protocol'").get();
|
|
194
207
|
try {
|
|
208
|
+
if (!sysUser)
|
|
209
|
+
throw new Error('sys_protocol user missing — cannot finalize decline settlement');
|
|
195
210
|
settleFault(db, req.params.id, 'fault_seller');
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
211
|
+
const rc = transition(db, req.params.id, 'completed', sysUser.id, [], '主动拒单:系统执行违约结算');
|
|
212
|
+
if (!rc?.success)
|
|
213
|
+
throw new Error(`fault_seller→completed transition failed: ${rc?.error || 'unknown'}`);
|
|
214
|
+
notifyTransition(db, req.params.id, 'fault_seller', 'completed');
|
|
200
215
|
}
|
|
201
216
|
catch (e) {
|
|
202
217
|
console.error('[decline settleFault]', e);
|
|
218
|
+
return void res.status(500).json({
|
|
219
|
+
error: '违约结算未完成,订单仍停在 fault_seller,请稍后重试或联系支持(买家尚未退款)',
|
|
220
|
+
error_code: 'DECLINE_SETTLEMENT_FAILED',
|
|
221
|
+
outcome: 'fault_seller',
|
|
222
|
+
});
|
|
203
223
|
}
|
|
204
224
|
return void res.json({
|
|
205
225
|
success: true, outcome: 'fault_seller', decline_reason_code: reasonCode,
|
|
@@ -278,7 +298,8 @@ export function registerOrdersActionRoutes(app, deps) {
|
|
|
278
298
|
// QA 轮 9.4-retry-v3 P1:post-hoc build breakdown 从 DB,让 agent 看清每分钱去哪
|
|
279
299
|
try {
|
|
280
300
|
const round2 = (n) => Math.round(n * 100) / 100;
|
|
281
|
-
|
|
301
|
+
// RFC-016: settleOrder 已完成,以下纯只读 breakdown 查询 → 异步 seam(无写,无原子性要求)
|
|
302
|
+
const ord = await dbOne("SELECT id, total_amount, source, fulfillment_mode, snapshot_commission_rate, l1_uid, l2_uid, l3_uid, logistics_id, seller_id FROM orders WHERE id = ?", [req.params.id]);
|
|
282
303
|
if (ord) {
|
|
283
304
|
const total = Number(ord.total_amount);
|
|
284
305
|
const isSecondhand = ord.source === 'secondhand';
|
|
@@ -289,7 +310,7 @@ export function registerOrdersActionRoutes(app, deps) {
|
|
|
289
310
|
const logisticsActual = ord.logistics_id ? logisticsFee : 0;
|
|
290
311
|
const commissionRate = Number(ord.snapshot_commission_rate ?? 0.10);
|
|
291
312
|
const commissionPool = round2(total * commissionRate);
|
|
292
|
-
const commRecs =
|
|
313
|
+
const commRecs = await dbAll("SELECT level, amount, beneficiary_id FROM commission_records WHERE order_id = ?", [req.params.id]);
|
|
293
314
|
const commByLevel = {
|
|
294
315
|
1: { amount: 0, to: null }, 2: { amount: 0, to: null }, 3: { amount: 0, to: null },
|
|
295
316
|
};
|
|
@@ -301,9 +322,9 @@ export function registerOrdersActionRoutes(app, deps) {
|
|
|
301
322
|
const commissionRedirected = round2(commissionPool - commissionDistributed);
|
|
302
323
|
// QA 轮 14.b P2:redirected_total 拆 chain_gap(→charity) vs region_cap(→global_fund)
|
|
303
324
|
// 之前单一数字让 agent 无法分辨钱去哪(global region L2/L3 进 global_fund,不是 charity)
|
|
304
|
-
const charityRow =
|
|
325
|
+
const charityRow = (await dbOne("SELECT COALESCE(SUM(amount),0) AS s FROM charity_fund_txns WHERE related_order_id = ?", [req.params.id]));
|
|
305
326
|
const redirectedToCharity = round2(Number(charityRow.s));
|
|
306
|
-
const fundDepRow =
|
|
327
|
+
const fundDepRow = (await dbOne("SELECT COALESCE(SUM(amount_l3),0) AS s FROM fund_deposits WHERE order_id = ?", [req.params.id]));
|
|
307
328
|
const redirectedToGlobalFund = round2(Number(fundDepRow.s));
|
|
308
329
|
// QA 轮 9.5 P2:payouts 表只 MCP legacy 写,PWA settleOrder 直更 wallet.balance 不写 payouts
|
|
309
330
|
// 改用公式推算 sellerAmount(跟 PWA settleOrder 内部计算一致),更可靠
|
|
@@ -347,11 +368,12 @@ export function registerOrdersActionRoutes(app, deps) {
|
|
|
347
368
|
});
|
|
348
369
|
});
|
|
349
370
|
// 手动触发超时判责(当事人)
|
|
350
|
-
app.post('/api/orders/:id/force-timeout-check', (req, res) => {
|
|
371
|
+
app.post('/api/orders/:id/force-timeout-check', async (req, res) => {
|
|
351
372
|
const user = auth(req, res);
|
|
352
373
|
if (!user)
|
|
353
374
|
return;
|
|
354
|
-
|
|
375
|
+
// RFC-016: 当事人校验读 → 异步 seam;checkTimeouts(db) 自身仍是同步判责引擎(Phase 3 内部迁)
|
|
376
|
+
const order = await dbOne('SELECT buyer_id, seller_id, logistics_id, status FROM orders WHERE id = ?', [req.params.id]);
|
|
355
377
|
if (!order)
|
|
356
378
|
return void res.status(404).json({ error: '订单不存在' });
|
|
357
379
|
const uid = user.id;
|
|
@@ -360,7 +382,7 @@ export function registerOrdersActionRoutes(app, deps) {
|
|
|
360
382
|
}
|
|
361
383
|
const beforeStatus = order.status;
|
|
362
384
|
const r = checkTimeouts(db);
|
|
363
|
-
const after =
|
|
385
|
+
const after = (await dbOne('SELECT status FROM orders WHERE id = ?', [req.params.id]));
|
|
364
386
|
const touched = r.details.find((d) => d.orderId === req.params.id) || null;
|
|
365
387
|
if (touched) {
|
|
366
388
|
const faultMatch = touched.action.match(/→ (fault_\w+)/);
|