@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
|
/** 拿商品(含 variant)当前生效的 flash sale;多重叠时取价最低。orders 下单也会用。 */
|
|
2
3
|
export function getActiveFlashSale(db, productId, variantId) {
|
|
3
4
|
const variantClause = variantId
|
|
@@ -18,12 +19,14 @@ export function getActiveFlashSale(db, productId, variantId) {
|
|
|
18
19
|
return db.prepare(sql).get(...args);
|
|
19
20
|
}
|
|
20
21
|
export function registerFlashSalesRoutes(app, deps) {
|
|
22
|
+
// db 仍在 destructure 中——传给同步 getActiveFlashSale(订单金钱路径消费,随 money batch 迁);
|
|
23
|
+
// 本文件的 handler 已走 RFC-016 异步 seam(dbOne/dbAll/dbRun)
|
|
21
24
|
const { db, generateId, auth, broadcastSystemEvent } = deps;
|
|
22
|
-
app.post('/api/products/:product_id/flash-sale', (req, res) => {
|
|
25
|
+
app.post('/api/products/:product_id/flash-sale', async (req, res) => {
|
|
23
26
|
const user = auth(req, res);
|
|
24
27
|
if (!user)
|
|
25
28
|
return;
|
|
26
|
-
const product =
|
|
29
|
+
const product = await dbOne('SELECT id, seller_id, price, has_variants FROM products WHERE id = ? AND status = \'active\'', [req.params.product_id]);
|
|
27
30
|
if (!product)
|
|
28
31
|
return void res.status(404).json({ error: '商品不存在或已下架' });
|
|
29
32
|
if (product.seller_id !== user.id)
|
|
@@ -50,7 +53,7 @@ export function registerFlashSalesRoutes(app, deps) {
|
|
|
50
53
|
if (variant_id) {
|
|
51
54
|
if (!product.has_variants)
|
|
52
55
|
return void res.status(400).json({ error: '该商品无规格,不可绑定 variant' });
|
|
53
|
-
const v =
|
|
56
|
+
const v = await dbOne('SELECT id FROM product_variants WHERE id = ? AND product_id = ? AND is_active = 1', [variant_id, product.id]);
|
|
54
57
|
if (!v)
|
|
55
58
|
return void res.status(400).json({ error: 'variant 不存在' });
|
|
56
59
|
variantId = String(variant_id);
|
|
@@ -60,15 +63,14 @@ export function registerFlashSalesRoutes(app, deps) {
|
|
|
60
63
|
}
|
|
61
64
|
const maxQty = Number.isFinite(Number(max_qty)) ? Math.max(0, Number(max_qty)) : 0;
|
|
62
65
|
// 防重叠:同 product+variant 不能有进行中的促销
|
|
63
|
-
const conflict =
|
|
66
|
+
const conflict = await dbOne(`
|
|
64
67
|
SELECT id FROM flash_sales WHERE product_id = ? AND ${variantId ? 'variant_id = ?' : 'variant_id IS NULL'}
|
|
65
68
|
AND is_active = 1 AND ends_at > datetime('now') LIMIT 1
|
|
66
|
-
|
|
69
|
+
`, variantId ? [product.id, variantId] : [product.id]);
|
|
67
70
|
if (conflict)
|
|
68
71
|
return void res.status(409).json({ error: '已有进行中的促销,请先结束', existing_id: conflict.id });
|
|
69
72
|
const id = generateId('fls');
|
|
70
|
-
|
|
71
|
-
.run(id, user.id, product.id, variantId, salePrice, Number(product.price), maxQty, new Date(startsT).toISOString(), new Date(endsT).toISOString());
|
|
73
|
+
await dbRun(`INSERT INTO flash_sales (id, seller_id, product_id, variant_id, sale_price, original_price, max_qty, starts_at, ends_at) VALUES (?,?,?,?,?,?,?,?,?)`, [id, user.id, product.id, variantId, salePrice, Number(product.price), maxQty, new Date(startsT).toISOString(), new Date(endsT).toISOString()]);
|
|
72
74
|
try {
|
|
73
75
|
broadcastSystemEvent('flash_sale', '⚡', `限时促销创建 ${product.id} · ${salePrice}/${product.price} WAZ`, product.id);
|
|
74
76
|
}
|
|
@@ -82,25 +84,25 @@ export function registerFlashSalesRoutes(app, deps) {
|
|
|
82
84
|
res.json({ sale });
|
|
83
85
|
});
|
|
84
86
|
// seller 自己的 flash sales(全部状态)
|
|
85
|
-
app.get('/api/sellers/me/flash-sales', (req, res) => {
|
|
87
|
+
app.get('/api/sellers/me/flash-sales', async (req, res) => {
|
|
86
88
|
const user = auth(req, res);
|
|
87
89
|
if (!user)
|
|
88
90
|
return;
|
|
89
|
-
const rows =
|
|
91
|
+
const rows = await dbAll(`
|
|
90
92
|
SELECT f.*, p.title as product_title
|
|
91
93
|
FROM flash_sales f
|
|
92
94
|
JOIN products p ON p.id = f.product_id
|
|
93
95
|
WHERE f.seller_id = ?
|
|
94
96
|
ORDER BY f.ends_at DESC LIMIT 100
|
|
95
|
-
|
|
97
|
+
`, [user.id]);
|
|
96
98
|
res.json({ items: rows });
|
|
97
99
|
});
|
|
98
100
|
// 取消(仅 seller 自己,且未开始)
|
|
99
|
-
app.delete('/api/flash-sales/:id', (req, res) => {
|
|
101
|
+
app.delete('/api/flash-sales/:id', async (req, res) => {
|
|
100
102
|
const user = auth(req, res);
|
|
101
103
|
if (!user)
|
|
102
104
|
return;
|
|
103
|
-
const f =
|
|
105
|
+
const f = await dbOne('SELECT seller_id, starts_at FROM flash_sales WHERE id = ?', [req.params.id]);
|
|
104
106
|
if (!f)
|
|
105
107
|
return void res.status(404).json({ error: 'flash sale 不存在' });
|
|
106
108
|
if (f.seller_id !== user.id)
|
|
@@ -108,12 +110,12 @@ export function registerFlashSalesRoutes(app, deps) {
|
|
|
108
110
|
if (new Date(f.starts_at).getTime() <= Date.now()) {
|
|
109
111
|
return void res.status(400).json({ error: '已开始的促销不可取消,请设置 is_active=0 提前结束' });
|
|
110
112
|
}
|
|
111
|
-
|
|
113
|
+
await dbRun('DELETE FROM flash_sales WHERE id = ?', [req.params.id]);
|
|
112
114
|
res.json({ success: true });
|
|
113
115
|
});
|
|
114
116
|
// buyer 视角:当前全平台正在进行的 flash sales(首屏 discovery)
|
|
115
|
-
app.get('/api/flash-sales/live', (req, res) => {
|
|
116
|
-
const rows =
|
|
117
|
+
app.get('/api/flash-sales/live', async (req, res) => {
|
|
118
|
+
const rows = await dbAll(`
|
|
117
119
|
SELECT f.id, f.product_id, f.variant_id, f.sale_price, f.original_price, f.ends_at, f.max_qty, f.sold_count,
|
|
118
120
|
p.title, p.images, p.category,
|
|
119
121
|
u.handle as seller_handle, u.name as seller_name
|
|
@@ -124,7 +126,7 @@ export function registerFlashSalesRoutes(app, deps) {
|
|
|
124
126
|
AND f.starts_at <= datetime('now') AND f.ends_at > datetime('now')
|
|
125
127
|
AND (f.max_qty = 0 OR f.sold_count < f.max_qty)
|
|
126
128
|
ORDER BY f.ends_at ASC LIMIT 100
|
|
127
|
-
`)
|
|
129
|
+
`);
|
|
128
130
|
res.json({ items: rows });
|
|
129
131
|
});
|
|
130
132
|
}
|
|
@@ -1,31 +1,32 @@
|
|
|
1
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerFollowsRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
-
|
|
3
|
+
// db 已走 RFC-016 异步 seam(dbOne/dbAll/dbRun),不再直接用 deps.db
|
|
4
|
+
const { auth, generateId } = deps;
|
|
5
|
+
app.get('/api/follows/:user_id/status', async (req, res) => {
|
|
4
6
|
const user = auth(req, res);
|
|
5
7
|
if (!user)
|
|
6
8
|
return;
|
|
7
|
-
const r =
|
|
8
|
-
const followers =
|
|
9
|
-
const following =
|
|
9
|
+
const r = await dbOne("SELECT 1 FROM follows WHERE follower_id=? AND followee_id=?", [user.id, req.params.user_id]);
|
|
10
|
+
const followers = (await dbOne("SELECT COUNT(*) as n FROM follows WHERE followee_id=?", [req.params.user_id])).n;
|
|
11
|
+
const following = (await dbOne("SELECT COUNT(*) as n FROM follows WHERE follower_id=?", [req.params.user_id])).n;
|
|
10
12
|
res.json({ following: !!r, followers, following_count: following });
|
|
11
13
|
});
|
|
12
|
-
app.post('/api/follows/:user_id', (req, res) => {
|
|
14
|
+
app.post('/api/follows/:user_id', async (req, res) => {
|
|
13
15
|
const user = auth(req, res);
|
|
14
16
|
if (!user)
|
|
15
17
|
return;
|
|
16
18
|
if (user.id === req.params.user_id)
|
|
17
19
|
return void res.json({ error: '不能关注自己' });
|
|
18
|
-
const target =
|
|
20
|
+
const target = await dbOne("SELECT id FROM users WHERE id=?", [req.params.user_id]);
|
|
19
21
|
if (!target)
|
|
20
22
|
return void res.json({ error: '用户不存在' });
|
|
21
|
-
const result =
|
|
23
|
+
const result = await dbRun("INSERT OR IGNORE INTO follows (follower_id, followee_id) VALUES (?, ?)", [user.id, req.params.user_id]);
|
|
22
24
|
// 2026-05-24 新关注 → 通知被关注者(仅首次关注时;重复点击不重发)
|
|
23
25
|
if (result.changes > 0) {
|
|
24
26
|
try {
|
|
25
|
-
const followerName =
|
|
27
|
+
const followerName = await dbOne("SELECT name, handle FROM users WHERE id = ?", [user.id]);
|
|
26
28
|
const display = followerName?.handle ? '@' + followerName.handle : followerName?.name || 'someone';
|
|
27
|
-
|
|
28
|
-
.run(generateId('ntf'), req.params.user_id, 'social', `🤝 新关注`, `${display} 关注了你`, null);
|
|
29
|
+
await dbRun(`INSERT INTO notifications (id, user_id, type, title, body, order_id) VALUES (?,?,?,?,?,?)`, [generateId('ntf'), req.params.user_id, 'social', `🤝 新关注`, `${display} 关注了你`, null]);
|
|
29
30
|
}
|
|
30
31
|
catch (e) {
|
|
31
32
|
console.error('[follow notif]', e);
|
|
@@ -33,51 +34,51 @@ export function registerFollowsRoutes(app, deps) {
|
|
|
33
34
|
}
|
|
34
35
|
res.json({ ok: true, following: true });
|
|
35
36
|
});
|
|
36
|
-
app.delete('/api/follows/:user_id', (req, res) => {
|
|
37
|
+
app.delete('/api/follows/:user_id', async (req, res) => {
|
|
37
38
|
const user = auth(req, res);
|
|
38
39
|
if (!user)
|
|
39
40
|
return;
|
|
40
|
-
|
|
41
|
+
await dbRun("DELETE FROM follows WHERE follower_id=? AND followee_id=?", [user.id, req.params.user_id]);
|
|
41
42
|
res.json({ ok: true, following: false });
|
|
42
43
|
});
|
|
43
|
-
app.get('/api/follows/me', (req, res) => {
|
|
44
|
+
app.get('/api/follows/me', async (req, res) => {
|
|
44
45
|
const user = auth(req, res);
|
|
45
46
|
if (!user)
|
|
46
47
|
return;
|
|
47
|
-
const followers =
|
|
48
|
+
const followers = await dbAll(`
|
|
48
49
|
SELECT u.id, u.name, u.role, f.created_at
|
|
49
50
|
FROM follows f JOIN users u ON u.id = f.follower_id
|
|
50
51
|
WHERE f.followee_id = ? ORDER BY f.created_at DESC LIMIT 100
|
|
51
|
-
|
|
52
|
-
const following =
|
|
52
|
+
`, [user.id]);
|
|
53
|
+
const following = await dbAll(`
|
|
53
54
|
SELECT u.id, u.name, u.role, f.created_at
|
|
54
55
|
FROM follows f JOIN users u ON u.id = f.followee_id
|
|
55
56
|
WHERE f.follower_id = ? ORDER BY f.created_at DESC LIMIT 100
|
|
56
|
-
|
|
57
|
+
`, [user.id]);
|
|
57
58
|
res.json({ followers, following });
|
|
58
59
|
});
|
|
59
60
|
// Wave D-1: 关注卖家动态 feed — new_product + restock 合并 + 去重
|
|
60
|
-
app.get('/api/follows/feed', (req, res) => {
|
|
61
|
+
app.get('/api/follows/feed', async (req, res) => {
|
|
61
62
|
const user = auth(req, res);
|
|
62
63
|
if (!user)
|
|
63
64
|
return;
|
|
64
65
|
const limit = Math.min(100, Math.max(10, Number(req.query.limit) || 50));
|
|
65
|
-
const followees =
|
|
66
|
+
const followees = await dbAll(`SELECT followee_id FROM follows WHERE follower_id = ?`, [user.id]);
|
|
66
67
|
if (followees.length === 0)
|
|
67
68
|
return void res.json({ items: [] });
|
|
68
69
|
const ids = followees.map(f => f.followee_id);
|
|
69
70
|
const placeholders = ids.map(() => '?').join(',');
|
|
70
71
|
// 新品(近 30 天 active)
|
|
71
|
-
const newProducts =
|
|
72
|
+
const newProducts = await dbAll(`
|
|
72
73
|
SELECT 'new_product' as type, p.created_at as ts, p.id as product_id, p.title, p.price, p.stock, p.category, p.images,
|
|
73
74
|
u.id as seller_id, u.name as seller_name, u.handle as seller_handle
|
|
74
75
|
FROM products p JOIN users u ON u.id = p.seller_id
|
|
75
76
|
WHERE p.seller_id IN (${placeholders}) AND p.status = 'active'
|
|
76
77
|
AND p.created_at > datetime('now', '-30 days')
|
|
77
78
|
ORDER BY p.created_at DESC LIMIT 100
|
|
78
|
-
|
|
79
|
+
`, ids);
|
|
79
80
|
// 重新上架 / 补货(近 7 天 updated_at > created_at + 1 天,stock > 0)
|
|
80
|
-
const restocks =
|
|
81
|
+
const restocks = await dbAll(`
|
|
81
82
|
SELECT 'restock' as type, p.updated_at as ts, p.id as product_id, p.title, p.price, p.stock, p.category, p.images,
|
|
82
83
|
u.id as seller_id, u.name as seller_name, u.handle as seller_handle
|
|
83
84
|
FROM products p JOIN users u ON u.id = p.seller_id
|
|
@@ -86,7 +87,7 @@ export function registerFollowsRoutes(app, deps) {
|
|
|
86
87
|
AND p.updated_at > datetime('now', '-7 days')
|
|
87
88
|
AND p.updated_at > datetime(p.created_at, '+1 days')
|
|
88
89
|
ORDER BY p.updated_at DESC LIMIT 30
|
|
89
|
-
|
|
90
|
+
`, ids);
|
|
90
91
|
// 合并 + 去重(同 product 同时 new + restock → 优先 new)
|
|
91
92
|
const seen = new Set();
|
|
92
93
|
const merged = [];
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
// RFC-016 Phase 1 — cron 候选扫描读 → async seam;逐用户卸任的 db.transaction 写仍同步(Phase 3 迁 pg)。
|
|
2
|
+
import { dbAll } from '../../layer0-foundation/L0-1-database/db.js';
|
|
1
3
|
/**
|
|
2
4
|
* Run one auto-deactivate sweep. Returns deactivation report for caller
|
|
3
5
|
* (cron caller logs to console; admin endpoint can call directly for query).
|
|
4
6
|
*/
|
|
5
|
-
export function runAutoDeactivateSweep(deps) {
|
|
7
|
+
export async function runAutoDeactivateSweep(deps) {
|
|
6
8
|
const { db, generateId, getProtocolParam } = deps;
|
|
7
9
|
const thresholdCount = Number(getProtocolParam('governance_auto_deactivate_threshold_count', 5));
|
|
8
10
|
const thresholdPct = Number(getProtocolParam('governance_auto_deactivate_threshold_pct', 0.3));
|
|
@@ -15,7 +17,7 @@ export function runAutoDeactivateSweep(deps) {
|
|
|
15
17
|
// (verifier_stats is the source-of-truth for confirmed_wrong; server.ts:5387 increments it
|
|
16
18
|
// on overturn, admin-verifier-flow.ts:130 decrements it on appeal success — exactly the
|
|
17
19
|
// "confirmed_wrong" signal playbook §6.2 requires.)
|
|
18
|
-
const candidates =
|
|
20
|
+
const candidates = await dbAll(`
|
|
19
21
|
SELECT u.id AS user_id, u.roles,
|
|
20
22
|
vs.tasks_done, vs.tasks_wrong
|
|
21
23
|
FROM users u
|
|
@@ -25,7 +27,7 @@ export function runAutoDeactivateSweep(deps) {
|
|
|
25
27
|
AND vs.tasks_done >= ?
|
|
26
28
|
AND vs.tasks_wrong >= ?
|
|
27
29
|
AND (CAST(vs.tasks_wrong AS REAL) / CAST(vs.tasks_done AS REAL)) >= ?
|
|
28
|
-
|
|
30
|
+
`, [minSample, thresholdCount, thresholdPct]);
|
|
29
31
|
const result = { scanned: candidates.length, deactivated: [] };
|
|
30
32
|
const cooldownUntil = Math.floor(Date.now() / 1000) + cooldownDays * 86400;
|
|
31
33
|
for (const c of candidates) {
|
|
@@ -43,8 +45,18 @@ export function runAutoDeactivateSweep(deps) {
|
|
|
43
45
|
}
|
|
44
46
|
if (!roles.includes('verifier'))
|
|
45
47
|
return; // already deactivated
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
// Codex #231 P1:扫描与本 tx 之间 verifier_stats 可能变化(申诉成功/纠正会减 tasks_wrong)。
|
|
49
|
+
// 必须在 tx 内重读 stats 并基于重读值重算阈值,否则会用陈旧的越线结果误卸任。
|
|
50
|
+
const vs = db.prepare("SELECT tasks_done, tasks_wrong FROM verifier_stats WHERE user_id = ?").get(c.user_id);
|
|
51
|
+
if (!vs)
|
|
52
|
+
return; // stats 行已不存在
|
|
53
|
+
const tasksDone = Number(vs.tasks_done);
|
|
54
|
+
const tasksWrong = Number(vs.tasks_wrong);
|
|
55
|
+
const overThreshold = tasksDone > 0 && tasksDone >= minSample && tasksWrong >= thresholdCount && (tasksWrong / tasksDone) >= thresholdPct;
|
|
56
|
+
if (!overThreshold)
|
|
57
|
+
return; // 重读后已不再越线 → 不写任何东西
|
|
58
|
+
const wrongPct = tasksWrong / tasksDone;
|
|
59
|
+
const reason = `confirmed_wrong_count=${tasksWrong}/${tasksDone} (${(wrongPct * 100).toFixed(1)}%) ≥ threshold (count=${thresholdCount}, pct=${(thresholdPct * 100).toFixed(0)}%, min_sample=${minSample})`;
|
|
48
60
|
// 1. UPDATE all active rows → inactive
|
|
49
61
|
db.prepare("UPDATE governance_applications SET status = 'inactive' WHERE user_id = ? AND role = 'verifier' AND status = 'active'").run(c.user_id);
|
|
50
62
|
// 2. INSERT auto_deactivate row(audit + appeal source)
|
|
@@ -69,8 +81,8 @@ export function runAutoDeactivateSweep(deps) {
|
|
|
69
81
|
result.deactivated.push({
|
|
70
82
|
user_id: c.user_id,
|
|
71
83
|
role: 'verifier',
|
|
72
|
-
tasks_done:
|
|
73
|
-
tasks_wrong:
|
|
84
|
+
tasks_done: tasksDone,
|
|
85
|
+
tasks_wrong: tasksWrong,
|
|
74
86
|
wrong_pct: wrongPct,
|
|
75
87
|
reason,
|
|
76
88
|
});
|
|
@@ -93,9 +105,9 @@ export function runAutoDeactivateSweep(deps) {
|
|
|
93
105
|
export function startAutoDeactivateCron(deps) {
|
|
94
106
|
const hours = Number(deps.getProtocolParam('governance_auto_deactivate_cron_hours', 24));
|
|
95
107
|
const ms = Math.max(1, hours) * 60 * 60 * 1000;
|
|
96
|
-
setInterval(() => {
|
|
108
|
+
setInterval(async () => {
|
|
97
109
|
try {
|
|
98
|
-
const r = runAutoDeactivateSweep(deps);
|
|
110
|
+
const r = await runAutoDeactivateSweep(deps);
|
|
99
111
|
if (r.deactivated.length > 0) {
|
|
100
112
|
console.log(`[gov-auto-deactivate] swept ${r.scanned} candidates, deactivated ${r.deactivated.length}:`, r.deactivated.map(d => `${d.user_id}(${d.tasks_wrong}/${d.tasks_done})`).join(', '));
|
|
101
113
|
}
|