@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
package/dist/pwa/routes/ai.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { dbOne } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerAiRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
+
// db 已走 RFC-016 异步 seam(dbOne),不再直接用 deps.db
|
|
4
|
+
const { auth, anthropic } = deps;
|
|
3
5
|
// G-2: AI 价格建议
|
|
4
6
|
app.post('/api/ai/price-suggestion', async (req, res) => {
|
|
5
7
|
const user = auth(req, res);
|
|
@@ -11,17 +13,17 @@ export function registerAiRoutes(app, deps) {
|
|
|
11
13
|
if (!title)
|
|
12
14
|
return void res.status(400).json({ error: '请提供 title' });
|
|
13
15
|
// 类目历史价位
|
|
14
|
-
const stats =
|
|
16
|
+
const stats = (await dbOne(`
|
|
15
17
|
SELECT COUNT(*) as cnt, COALESCE(AVG(price), 0) as avg, COALESCE(MIN(price), 0) as min, COALESCE(MAX(price), 0) as max,
|
|
16
18
|
COALESCE((SELECT price FROM products WHERE status='active' AND category = ? ORDER BY price LIMIT 1 OFFSET CAST((SELECT COUNT(*) FROM products WHERE status='active' AND category = ?) / 2 AS INTEGER)), 0) as median
|
|
17
19
|
FROM products WHERE status = 'active' AND category = ?
|
|
18
|
-
|
|
20
|
+
`, [category || '', category || '', category || '']));
|
|
19
21
|
// 近 30 天成交均价(更可信)
|
|
20
|
-
const recentAvg =
|
|
22
|
+
const recentAvg = (await dbOne(`
|
|
21
23
|
SELECT COALESCE(AVG(total_amount), 0) as avg FROM orders o
|
|
22
24
|
JOIN products p ON p.id = o.product_id
|
|
23
25
|
WHERE p.category = ? AND o.status = 'completed' AND o.created_at > datetime('now', '-30 days')
|
|
24
|
-
|
|
26
|
+
`, [category || ''])).avg;
|
|
25
27
|
try {
|
|
26
28
|
const message = await anthropic.messages.create({
|
|
27
29
|
model: 'claude-haiku-4-5-20251001',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
function median(arr) {
|
|
2
3
|
if (arr.length === 0)
|
|
3
4
|
return null;
|
|
@@ -6,9 +7,10 @@ function median(arr) {
|
|
|
6
7
|
return s.length % 2 ? s[mid] : (s[mid - 1] + s[mid]) / 2;
|
|
7
8
|
}
|
|
8
9
|
export function registerAnalyticsRoutes(app, deps) {
|
|
9
|
-
|
|
10
|
+
// db 已全量走 RFC-016 异步 seam(dbOne/dbAll),不再直接用 deps.db
|
|
11
|
+
const { auth } = deps;
|
|
10
12
|
// 物流绩效卡 (Wave B-4)
|
|
11
|
-
app.get('/api/logistics/me/performance', (req, res) => {
|
|
13
|
+
app.get('/api/logistics/me/performance', async (req, res) => {
|
|
12
14
|
const user = auth(req, res);
|
|
13
15
|
if (!user)
|
|
14
16
|
return;
|
|
@@ -16,22 +18,22 @@ export function registerAnalyticsRoutes(app, deps) {
|
|
|
16
18
|
return void res.status(403).json({ error: '仅物流角色可访问' });
|
|
17
19
|
}
|
|
18
20
|
const windowDays = Math.max(7, Math.min(365, Number(req.query.window) || 30));
|
|
19
|
-
const orders =
|
|
21
|
+
const orders = await dbAll(`
|
|
20
22
|
SELECT id, status, created_at, updated_at,
|
|
21
23
|
pickup_deadline, delivery_deadline
|
|
22
24
|
FROM orders
|
|
23
25
|
WHERE logistics_id = ? AND created_at > datetime('now', '-' || ? || ' days')
|
|
24
|
-
|
|
26
|
+
`, [user.id, windowDays]);
|
|
25
27
|
const orderIds = orders.map(o => o.id);
|
|
26
28
|
let history = [];
|
|
27
29
|
if (orderIds.length > 0) {
|
|
28
30
|
const placeholders = orderIds.map(() => '?').join(',');
|
|
29
|
-
history =
|
|
31
|
+
history = await dbAll(`
|
|
30
32
|
SELECT order_id, from_status, to_status, created_at
|
|
31
33
|
FROM order_state_history
|
|
32
34
|
WHERE order_id IN (${placeholders})
|
|
33
35
|
ORDER BY created_at ASC
|
|
34
|
-
|
|
36
|
+
`, orderIds);
|
|
35
37
|
}
|
|
36
38
|
const histByOrder = new Map();
|
|
37
39
|
for (const h of history) {
|
|
@@ -78,24 +80,24 @@ export function registerAnalyticsRoutes(app, deps) {
|
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
82
|
}
|
|
81
|
-
const disputes =
|
|
83
|
+
const disputes = (await dbOne(`
|
|
82
84
|
SELECT COUNT(*) as n FROM disputes d
|
|
83
85
|
JOIN orders o ON o.id = d.order_id
|
|
84
86
|
WHERE o.logistics_id = ? AND d.created_at > datetime('now', '-' || ? || ' days')
|
|
85
|
-
|
|
87
|
+
`, [user.id, windowDays])).n;
|
|
86
88
|
// 败诉两路:auto-fault 判物流 + 仲裁裁定物流为被告且退款
|
|
87
|
-
const autoFaultLost =
|
|
89
|
+
const autoFaultLost = (await dbOne(`
|
|
88
90
|
SELECT COUNT(*) as n FROM orders
|
|
89
91
|
WHERE logistics_id = ? AND status = 'fault_logistics'
|
|
90
92
|
AND updated_at > datetime('now', '-' || ? || ' days')
|
|
91
|
-
|
|
92
|
-
const arbitratedLost =
|
|
93
|
+
`, [user.id, windowDays])).n;
|
|
94
|
+
const arbitratedLost = (await dbOne(`
|
|
93
95
|
SELECT COUNT(*) as n FROM disputes d
|
|
94
96
|
JOIN orders o ON o.id = d.order_id
|
|
95
97
|
WHERE o.logistics_id = ? AND d.defendant_id = ?
|
|
96
98
|
AND d.ruling_type IN ('refund_buyer','partial_refund')
|
|
97
99
|
AND d.created_at > datetime('now', '-' || ? || ' days')
|
|
98
|
-
|
|
100
|
+
`, [user.id, user.id, windowDays])).n;
|
|
99
101
|
const disputeLoss = autoFaultLost + arbitratedLost;
|
|
100
102
|
const pickupTotalEvaluated = pickupOnTime + pickupOverdue;
|
|
101
103
|
const deliveryTotalEvaluated = deliveryOnTime + deliveryOverdue;
|
|
@@ -125,14 +127,14 @@ export function registerAnalyticsRoutes(app, deps) {
|
|
|
125
127
|
});
|
|
126
128
|
});
|
|
127
129
|
// 卖家销售分析 (Wave C-5)
|
|
128
|
-
app.get('/api/sellers/me/analytics', (req, res) => {
|
|
130
|
+
app.get('/api/sellers/me/analytics', async (req, res) => {
|
|
129
131
|
const user = auth(req, res);
|
|
130
132
|
if (!user)
|
|
131
133
|
return;
|
|
132
134
|
if (user.role !== 'seller')
|
|
133
135
|
return void res.status(403).json({ error: '仅卖家可访问' });
|
|
134
136
|
const windowDays = Math.max(7, Math.min(365, Number(req.query.window) || 30));
|
|
135
|
-
const ordersAgg =
|
|
137
|
+
const ordersAgg = (await dbOne(`
|
|
136
138
|
SELECT
|
|
137
139
|
COUNT(*) as total_orders,
|
|
138
140
|
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed_orders,
|
|
@@ -141,8 +143,8 @@ export function registerAnalyticsRoutes(app, deps) {
|
|
|
141
143
|
COALESCE(SUM(CASE WHEN status = 'completed' THEN total_amount ELSE 0 END), 0) as gmv,
|
|
142
144
|
COALESCE(AVG(CASE WHEN status = 'completed' THEN total_amount END), 0) as aov
|
|
143
145
|
FROM orders WHERE seller_id = ? AND created_at > datetime('now', '-' || ? || ' days')
|
|
144
|
-
|
|
145
|
-
const topProducts =
|
|
146
|
+
`, [user.id, windowDays]));
|
|
147
|
+
const topProducts = await dbAll(`
|
|
146
148
|
SELECT p.id, p.title, p.price, COUNT(o.id) as sales,
|
|
147
149
|
COALESCE(SUM(o.total_amount), 0) as revenue
|
|
148
150
|
FROM products p
|
|
@@ -153,27 +155,27 @@ export function registerAnalyticsRoutes(app, deps) {
|
|
|
153
155
|
GROUP BY p.id
|
|
154
156
|
HAVING sales > 0
|
|
155
157
|
ORDER BY sales DESC LIMIT 10
|
|
156
|
-
|
|
157
|
-
const buyerStats =
|
|
158
|
+
`, [windowDays, user.id]);
|
|
159
|
+
const buyerStats = (await dbOne(`
|
|
158
160
|
SELECT
|
|
159
161
|
COUNT(DISTINCT buyer_id) as unique_buyers,
|
|
160
162
|
COUNT(*) as orders_count
|
|
161
163
|
FROM orders WHERE seller_id = ? AND status = 'completed'
|
|
162
164
|
AND created_at > datetime('now', '-' || ? || ' days')
|
|
163
|
-
|
|
164
|
-
const repeatBuyers =
|
|
165
|
+
`, [user.id, windowDays]));
|
|
166
|
+
const repeatBuyers = (await dbOne(`
|
|
165
167
|
SELECT COUNT(*) as n FROM (
|
|
166
168
|
SELECT buyer_id FROM orders WHERE seller_id = ? AND status = 'completed'
|
|
167
169
|
AND created_at > datetime('now', '-' || ? || ' days')
|
|
168
170
|
GROUP BY buyer_id HAVING COUNT(*) > 1
|
|
169
171
|
)
|
|
170
|
-
|
|
171
|
-
const wishlistAdds =
|
|
172
|
+
`, [user.id, windowDays])).n;
|
|
173
|
+
const wishlistAdds = (await dbOne(`
|
|
172
174
|
SELECT COUNT(*) as n FROM user_wishlist w
|
|
173
175
|
JOIN products p ON p.id = w.product_id
|
|
174
176
|
WHERE p.seller_id = ? AND w.created_at > datetime('now', '-' || ? || ' days')
|
|
175
|
-
|
|
176
|
-
const dailyTrend =
|
|
177
|
+
`, [user.id, windowDays])).n;
|
|
178
|
+
const dailyTrend = await dbAll(`
|
|
177
179
|
SELECT DATE(created_at) as date,
|
|
178
180
|
COUNT(*) as orders,
|
|
179
181
|
COALESCE(SUM(CASE WHEN status = 'completed' THEN total_amount ELSE 0 END), 0) as gmv
|
|
@@ -181,30 +183,30 @@ export function registerAnalyticsRoutes(app, deps) {
|
|
|
181
183
|
WHERE seller_id = ? AND created_at > datetime('now', '-' || ? || ' days')
|
|
182
184
|
GROUP BY DATE(created_at)
|
|
183
185
|
ORDER BY date ASC
|
|
184
|
-
|
|
185
|
-
const ratingsAgg =
|
|
186
|
+
`, [user.id, Math.min(windowDays, 30)]);
|
|
187
|
+
const ratingsAgg = await dbOne(`
|
|
186
188
|
SELECT COUNT(*) as cnt, COALESCE(AVG(stars), 0) as avg_stars
|
|
187
189
|
FROM order_ratings WHERE seller_id = ?
|
|
188
190
|
AND created_at > datetime('now', '-' || ? || ' days')
|
|
189
|
-
|
|
190
|
-
const refundsCount =
|
|
191
|
+
`, [user.id, windowDays]);
|
|
192
|
+
const refundsCount = (await dbOne(`
|
|
191
193
|
SELECT COUNT(*) as n FROM return_requests
|
|
192
194
|
WHERE seller_id = ? AND status = 'refunded'
|
|
193
195
|
AND created_at > datetime('now', '-' || ? || ' days')
|
|
194
|
-
|
|
196
|
+
`, [user.id, windowDays])).n;
|
|
195
197
|
// S1: 平均备货时长(paid → shipped 中位 hours)
|
|
196
|
-
const handlingRow =
|
|
198
|
+
const handlingRow = (await dbOne(`
|
|
197
199
|
SELECT COALESCE(AVG((julianday(h_ship.created_at) - julianday(h_paid.created_at)) * 24), 0) as avg_handling_hours,
|
|
198
200
|
COUNT(*) as sample_n
|
|
199
201
|
FROM orders o
|
|
200
202
|
JOIN order_state_history h_paid ON h_paid.order_id = o.id AND h_paid.to_status = 'paid'
|
|
201
203
|
JOIN order_state_history h_ship ON h_ship.order_id = o.id AND h_ship.to_status = 'shipped'
|
|
202
204
|
WHERE o.seller_id = ? AND o.created_at > datetime('now', '-' || ? || ' days')
|
|
203
|
-
|
|
205
|
+
`, [user.id, windowDays]));
|
|
204
206
|
const completedN = Number(ordersAgg.completed_orders) || 0;
|
|
205
207
|
const returnRate = completedN > 0 ? refundsCount / completedN : 0;
|
|
206
208
|
// S1: 上一窗口对比
|
|
207
|
-
const prevAgg =
|
|
209
|
+
const prevAgg = (await dbOne(`
|
|
208
210
|
SELECT
|
|
209
211
|
COUNT(*) as total_orders,
|
|
210
212
|
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed_orders,
|
|
@@ -212,7 +214,7 @@ export function registerAnalyticsRoutes(app, deps) {
|
|
|
212
214
|
FROM orders WHERE seller_id = ?
|
|
213
215
|
AND created_at > datetime('now', '-' || ? || ' days')
|
|
214
216
|
AND created_at <= datetime('now', '-' || ? || ' days')
|
|
215
|
-
|
|
217
|
+
`, [user.id, windowDays * 2, windowDays]));
|
|
216
218
|
res.json({
|
|
217
219
|
window_days: windowDays,
|
|
218
220
|
orders: ordersAgg,
|
|
@@ -247,19 +249,19 @@ export function registerAnalyticsRoutes(app, deps) {
|
|
|
247
249
|
});
|
|
248
250
|
});
|
|
249
251
|
// 卖家退货仪表盘
|
|
250
|
-
app.get('/api/sellers/me/return-stats', (req, res) => {
|
|
252
|
+
app.get('/api/sellers/me/return-stats', async (req, res) => {
|
|
251
253
|
const user = auth(req, res);
|
|
252
254
|
if (!user)
|
|
253
255
|
return;
|
|
254
|
-
const totalReturns =
|
|
255
|
-
const refunded =
|
|
256
|
-
const rejected =
|
|
257
|
-
const pending =
|
|
258
|
-
const totalOrders =
|
|
259
|
-
const reasonBreakdown =
|
|
256
|
+
const totalReturns = (await dbOne(`SELECT COUNT(*) as n FROM return_requests WHERE seller_id = ?`, [user.id])).n;
|
|
257
|
+
const refunded = (await dbOne(`SELECT COUNT(*) as n FROM return_requests WHERE seller_id = ? AND status = 'refunded'`, [user.id])).n;
|
|
258
|
+
const rejected = (await dbOne(`SELECT COUNT(*) as n FROM return_requests WHERE seller_id = ? AND status = 'rejected'`, [user.id])).n;
|
|
259
|
+
const pending = (await dbOne(`SELECT COUNT(*) as n FROM return_requests WHERE seller_id = ? AND status = 'pending'`, [user.id])).n;
|
|
260
|
+
const totalOrders = (await dbOne(`SELECT COUNT(*) as n FROM orders WHERE seller_id = ? AND status IN ('delivered','completed','refunded')`, [user.id])).n;
|
|
261
|
+
const reasonBreakdown = await dbAll(`
|
|
260
262
|
SELECT reason, COUNT(*) as cnt FROM return_requests
|
|
261
263
|
WHERE seller_id = ? GROUP BY reason ORDER BY cnt DESC
|
|
262
|
-
|
|
264
|
+
`, [user.id]);
|
|
263
265
|
const returnRate = totalOrders > 0 ? (refunded / totalOrders) : 0;
|
|
264
266
|
res.json({
|
|
265
267
|
total_returns: totalReturns,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { generateAnchor, lookupAnchor, retireAnchor, userReferralVolume, computeTierLetter, userAnchorQuotaStats, TIER_THRESHOLDS, ANCHOR_HANDLE_MAX_FOR_USE, } from '../../layer2-business/L2-anchor-registry/anchor-registry.js';
|
|
2
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
2
3
|
export function registerAnchorsRoutes(app, deps) {
|
|
3
4
|
const { db, auth, rateLimitOk } = deps;
|
|
4
5
|
// POST /api/anchor/generate
|
|
@@ -25,7 +26,7 @@ export function registerAnchorsRoutes(app, deps) {
|
|
|
25
26
|
res.json({ ok: true, anchor: r.anchor, tier_letter: r.tier_letter });
|
|
26
27
|
});
|
|
27
28
|
// GET /api/anchor/:code/lookup — 公开(无需 auth)
|
|
28
|
-
app.get('/api/anchor/:code/lookup', (req, res) => {
|
|
29
|
+
app.get('/api/anchor/:code/lookup', async (req, res) => {
|
|
29
30
|
if (!rateLimitOk(req.ip || 'anon', 60, 60_000))
|
|
30
31
|
return void res.status(429).json({ error: 'too_many_lookups' });
|
|
31
32
|
const r = lookupAnchor(db, String(req.params.code || ''));
|
|
@@ -38,15 +39,15 @@ export function registerAnchorsRoutes(app, deps) {
|
|
|
38
39
|
return void res.status(404).json({ found: false, hint: 'reclaimable' });
|
|
39
40
|
}
|
|
40
41
|
// 2026-05-24 富化响应:附 owner 详情 + 商品推荐指数
|
|
41
|
-
const owner =
|
|
42
|
+
const owner = await dbOne(`
|
|
42
43
|
SELECT u.name, u.handle, u.region, u.created_at, u.bio,
|
|
43
44
|
(SELECT COUNT(*) FROM follows WHERE followee_id = u.id) as follower_count,
|
|
44
45
|
(SELECT COALESCE(SUM(s.like_count), 0) FROM shareables s WHERE s.owner_id = u.id AND s.status = 'active') as total_likes_received
|
|
45
46
|
FROM users u WHERE u.id = ? AND u.id != 'sys_protocol'
|
|
46
|
-
|
|
47
|
+
`, [r.owner_id]);
|
|
47
48
|
let product = null;
|
|
48
49
|
if (r.target_kind === 'product') {
|
|
49
|
-
product =
|
|
50
|
+
product = (await dbOne(`
|
|
50
51
|
SELECT p.id, p.title, p.price, p.category, p.images, p.completion_count, p.total_likes,
|
|
51
52
|
(SELECT COUNT(DISTINCT buyer_id) FROM order_ratings rt WHERE rt.product_id = p.id AND rt.stars >= 4) as recommend_count,
|
|
52
53
|
(SELECT ROUND(AVG(stars), 2) FROM order_ratings rt WHERE rt.product_id = p.id) as avg_rating,
|
|
@@ -54,12 +55,12 @@ export function registerAnchorsRoutes(app, deps) {
|
|
|
54
55
|
u.handle as seller_handle, u.name as seller_name
|
|
55
56
|
FROM products p LEFT JOIN users u ON u.id = p.seller_id
|
|
56
57
|
WHERE p.id = ? AND p.status = 'active'
|
|
57
|
-
|
|
58
|
+
`, [r.target_id])) ?? null;
|
|
58
59
|
}
|
|
59
60
|
else if (r.target_kind === 'shareable') {
|
|
60
|
-
const sh =
|
|
61
|
+
const sh = await dbOne(`SELECT related_product_id FROM shareables WHERE id = ?`, [r.target_id]);
|
|
61
62
|
if (sh?.related_product_id) {
|
|
62
|
-
product =
|
|
63
|
+
product = (await dbOne(`
|
|
63
64
|
SELECT p.id, p.title, p.price, p.category, p.images, p.completion_count, p.total_likes,
|
|
64
65
|
(SELECT COUNT(DISTINCT buyer_id) FROM order_ratings rt WHERE rt.product_id = p.id AND rt.stars >= 4) as recommend_count,
|
|
65
66
|
(SELECT ROUND(AVG(stars), 2) FROM order_ratings rt WHERE rt.product_id = p.id) as avg_rating,
|
|
@@ -67,7 +68,7 @@ export function registerAnchorsRoutes(app, deps) {
|
|
|
67
68
|
u.handle as seller_handle, u.name as seller_name
|
|
68
69
|
FROM products p LEFT JOIN users u ON u.id = p.seller_id
|
|
69
70
|
WHERE p.id = ? AND p.status = 'active'
|
|
70
|
-
|
|
71
|
+
`, [sh.related_product_id])) ?? null;
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
res.json({
|
|
@@ -81,7 +82,7 @@ export function registerAnchorsRoutes(app, deps) {
|
|
|
81
82
|
});
|
|
82
83
|
});
|
|
83
84
|
// POST /api/anchor/:code/touch — 写 attribution(first-touch + 30d)
|
|
84
|
-
app.post('/api/anchor/:code/touch', (req, res) => {
|
|
85
|
+
app.post('/api/anchor/:code/touch', async (req, res) => {
|
|
85
86
|
const user = auth(req, res);
|
|
86
87
|
if (!user)
|
|
87
88
|
return;
|
|
@@ -93,27 +94,25 @@ export function registerAnchorsRoutes(app, deps) {
|
|
|
93
94
|
let attributedProducts = 0;
|
|
94
95
|
const expiresAt = new Date(Date.now() + 30 * 86400_000).toISOString().slice(0, 19).replace('T', ' ');
|
|
95
96
|
if (r.target_kind === 'product') {
|
|
96
|
-
const existing =
|
|
97
|
+
const existing = await dbOne(`SELECT 1 FROM product_share_attribution WHERE product_id = ? AND recipient_id = ?`, [r.target_id, user.id]);
|
|
97
98
|
if (!existing) {
|
|
98
|
-
|
|
99
|
-
.run(r.target_id, user.id, r.owner_id, expiresAt);
|
|
99
|
+
await dbRun(`INSERT INTO product_share_attribution (product_id, recipient_id, sharer_id, shareable_id, expires_at) VALUES (?,?,?,NULL,?)`, [r.target_id, user.id, r.owner_id, expiresAt]);
|
|
100
100
|
attributedProducts = 1;
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
else if (r.target_kind === 'shareable') {
|
|
104
|
-
const s =
|
|
104
|
+
const s = await dbOne(`SELECT id, related_product_id FROM shareables WHERE id = ?`, [r.target_id]);
|
|
105
105
|
if (s?.related_product_id) {
|
|
106
|
-
const existing =
|
|
106
|
+
const existing = await dbOne(`SELECT 1 FROM product_share_attribution WHERE product_id = ? AND recipient_id = ?`, [s.related_product_id, user.id]);
|
|
107
107
|
if (!existing) {
|
|
108
|
-
|
|
109
|
-
.run(s.related_product_id, user.id, r.owner_id, s.id, expiresAt);
|
|
108
|
+
await dbRun(`INSERT INTO product_share_attribution (product_id, recipient_id, sharer_id, shareable_id, expires_at) VALUES (?,?,?,?,?)`, [s.related_product_id, user.id, r.owner_id, s.id, expiresAt]);
|
|
110
109
|
attributedProducts = 1;
|
|
111
110
|
}
|
|
112
111
|
}
|
|
113
112
|
}
|
|
114
113
|
else if (r.target_kind === 'user') {
|
|
115
114
|
// 限 LIMIT 50 防 DoS
|
|
116
|
-
const ownerProducts =
|
|
115
|
+
const ownerProducts = await dbAll(`SELECT id FROM products WHERE seller_id = ? AND status = 'active' ORDER BY last_sold_at DESC NULLS LAST LIMIT 50`, [r.owner_id]);
|
|
117
116
|
db.transaction(() => {
|
|
118
117
|
for (const p of ownerProducts) {
|
|
119
118
|
const existing = db.prepare(`SELECT 1 FROM product_share_attribution WHERE product_id = ? AND recipient_id = ?`).get(p.id, user.id);
|
|
@@ -146,14 +145,14 @@ export function registerAnchorsRoutes(app, deps) {
|
|
|
146
145
|
}
|
|
147
146
|
res.json({ ok: true });
|
|
148
147
|
});
|
|
149
|
-
app.get('/api/anchor/me', (req, res) => {
|
|
148
|
+
app.get('/api/anchor/me', async (req, res) => {
|
|
150
149
|
const user = auth(req, res);
|
|
151
150
|
if (!user)
|
|
152
151
|
return;
|
|
153
|
-
const rows =
|
|
152
|
+
const rows = await dbAll(`
|
|
154
153
|
SELECT anchor, prefix, middle, tier_letter, target_kind, target_id, status, retired_at, hits, last_hit_at, created_at
|
|
155
154
|
FROM anchor_registry WHERE owner_id = ? ORDER BY created_at DESC LIMIT 100
|
|
156
|
-
|
|
155
|
+
`, [user.id]);
|
|
157
156
|
const vol = userReferralVolume(db, user.id);
|
|
158
157
|
const tier = computeTierLetter(vol);
|
|
159
158
|
const quota = userAnchorQuotaStats(db, user.id);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerAnnouncementsRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
-
|
|
3
|
+
// db 已走 RFC-016 异步 seam(dbOne/dbAll/dbRun),不再直接用 deps.db
|
|
4
|
+
const { generateId, auth, safeRoles, requireProtocolAdmin, isRootAdmin, getAdminScope, logAdminAction } = deps;
|
|
5
|
+
app.post('/api/admin/announcements', async (req, res) => {
|
|
4
6
|
const admin = requireProtocolAdmin(req, res);
|
|
5
7
|
if (!admin)
|
|
6
8
|
return;
|
|
@@ -26,16 +28,15 @@ export function registerAnnouncementsRoutes(app, deps) {
|
|
|
26
28
|
}
|
|
27
29
|
}
|
|
28
30
|
const id = generateId('ann');
|
|
29
|
-
|
|
30
|
-
.run(id, admin.id, title.trim(), body.trim(), rolesJson, regionsJson, severity || 'info', starts_at || null, expires_at || null);
|
|
31
|
+
await dbRun(`INSERT INTO announcements (id, author_id, title, body, target_roles, target_regions, severity, starts_at, expires_at) VALUES (?,?,?,?,?,?,?,?,?)`, [id, admin.id, title.trim(), body.trim(), rolesJson, regionsJson, severity || 'info', starts_at || null, expires_at || null]);
|
|
31
32
|
logAdminAction(admin.id, 'create_announcement', 'announcement', id, { title, severity: severity || 'info' });
|
|
32
33
|
res.json({ success: true, id });
|
|
33
34
|
});
|
|
34
|
-
app.patch('/api/admin/announcements/:id', (req, res) => {
|
|
35
|
+
app.patch('/api/admin/announcements/:id', async (req, res) => {
|
|
35
36
|
const admin = requireProtocolAdmin(req, res);
|
|
36
37
|
if (!admin)
|
|
37
38
|
return;
|
|
38
|
-
const ann =
|
|
39
|
+
const ann = await dbOne('SELECT id, author_id FROM announcements WHERE id = ?', [req.params.id]);
|
|
39
40
|
if (!ann)
|
|
40
41
|
return void res.status(404).json({ error: '公告不存在' });
|
|
41
42
|
if (!isRootAdmin(admin) && ann.author_id !== admin.id)
|
|
@@ -54,17 +55,17 @@ export function registerAnnouncementsRoutes(app, deps) {
|
|
|
54
55
|
if (sets.length === 0)
|
|
55
56
|
return void res.status(400).json({ error: '无可更新字段' });
|
|
56
57
|
args.push(req.params.id);
|
|
57
|
-
|
|
58
|
+
await dbRun(`UPDATE announcements SET ${sets.join(', ')} WHERE id = ?`, args);
|
|
58
59
|
res.json({ success: true });
|
|
59
60
|
});
|
|
60
61
|
// 列出对当前用户可见的活跃公告(按角色 + 区域过滤)
|
|
61
|
-
app.get('/api/announcements/active', (req, res) => {
|
|
62
|
+
app.get('/api/announcements/active', async (req, res) => {
|
|
62
63
|
const user = auth(req, res);
|
|
63
64
|
if (!user)
|
|
64
65
|
return;
|
|
65
66
|
const userRoles = safeRoles(user);
|
|
66
67
|
const userRegion = user.region || 'global';
|
|
67
|
-
const rows =
|
|
68
|
+
const rows = await dbAll(`
|
|
68
69
|
SELECT a.id, a.title, a.body, a.severity, a.created_at, a.target_roles, a.target_regions,
|
|
69
70
|
(SELECT 1 FROM announcement_reads WHERE user_id = ? AND announcement_id = a.id) as is_read
|
|
70
71
|
FROM announcements a
|
|
@@ -72,7 +73,7 @@ export function registerAnnouncementsRoutes(app, deps) {
|
|
|
72
73
|
AND (a.starts_at IS NULL OR a.starts_at <= datetime('now'))
|
|
73
74
|
AND (a.expires_at IS NULL OR a.expires_at >= datetime('now'))
|
|
74
75
|
ORDER BY a.created_at DESC LIMIT 50
|
|
75
|
-
|
|
76
|
+
`, [user.id]);
|
|
76
77
|
// JS 端 filter 角色 / 区域(避免 JSON LIKE 在 SQLite 中麻烦)
|
|
77
78
|
const filtered = rows.filter(a => {
|
|
78
79
|
if (a.target_roles) {
|
|
@@ -96,13 +97,12 @@ export function registerAnnouncementsRoutes(app, deps) {
|
|
|
96
97
|
}).map(a => ({ ...a, target_roles: undefined, target_regions: undefined, is_read: !!a.is_read }));
|
|
97
98
|
res.json({ items: filtered });
|
|
98
99
|
});
|
|
99
|
-
app.post('/api/announcements/:id/read', (req, res) => {
|
|
100
|
+
app.post('/api/announcements/:id/read', async (req, res) => {
|
|
100
101
|
const user = auth(req, res);
|
|
101
102
|
if (!user)
|
|
102
103
|
return;
|
|
103
104
|
try {
|
|
104
|
-
|
|
105
|
-
.run(user.id, req.params.id);
|
|
105
|
+
await dbRun(`INSERT OR IGNORE INTO announcement_reads (user_id, announcement_id) VALUES (?,?)`, [user.id, req.params.id]);
|
|
106
106
|
}
|
|
107
107
|
catch { }
|
|
108
108
|
res.json({ success: true });
|