@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,17 +1,18 @@
|
|
|
1
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerAdminAdminsRoutes(app, deps) {
|
|
2
3
|
const { db, generateId, requireAdmin, requireRootAdmin, isRootAdmin, getAdminPermissions, ADMIN_PERMISSIONS } = deps;
|
|
3
4
|
// GET 全部 admin 列表
|
|
4
|
-
app.get('/api/admin/admins', (req, res) => {
|
|
5
|
+
app.get('/api/admin/admins', async (req, res) => {
|
|
5
6
|
const me = requireAdmin(req, res);
|
|
6
7
|
if (!me)
|
|
7
8
|
return;
|
|
8
|
-
const items =
|
|
9
|
+
const items = await dbAll(`
|
|
9
10
|
SELECT id, name, handle, role, admin_type, admin_scope, admin_permissions, email, created_at,
|
|
10
11
|
(SELECT MAX(created_at) FROM admin_audit_log WHERE admin_id = users.id) AS last_action_at
|
|
11
12
|
FROM users
|
|
12
13
|
WHERE role = 'admin' OR (roles IS NOT NULL AND roles LIKE '%admin%')
|
|
13
14
|
ORDER BY admin_type DESC, created_at ASC
|
|
14
|
-
|
|
15
|
+
`, []);
|
|
15
16
|
// 普通 admin 视角下 email 脱敏
|
|
16
17
|
const masked = items.map((u) => {
|
|
17
18
|
const enriched = { ...u, admin_permissions: u.admin_type === 'root' ? ['all'] : (() => { try {
|
|
@@ -31,7 +32,7 @@ export function registerAdminAdminsRoutes(app, deps) {
|
|
|
31
32
|
});
|
|
32
33
|
});
|
|
33
34
|
// POST 创建 admin(仅 root)
|
|
34
|
-
app.post('/api/admin/admins', (req, res) => {
|
|
35
|
+
app.post('/api/admin/admins', async (req, res) => {
|
|
35
36
|
const root = requireRootAdmin(req, res);
|
|
36
37
|
if (!root)
|
|
37
38
|
return;
|
|
@@ -60,7 +61,7 @@ export function registerAdminAdminsRoutes(app, deps) {
|
|
|
60
61
|
return void res.json({ error: 'admin_type 无效' });
|
|
61
62
|
if (!['global', 'china', 'us', 'eu', 'india', 'singapore', 'global_north'].includes(adminScope))
|
|
62
63
|
return void res.json({ error: 'admin_scope 无效' });
|
|
63
|
-
const target =
|
|
64
|
+
const target = await dbOne(`SELECT id, role, roles, admin_type FROM users WHERE id = ?`, [targetUserId]);
|
|
64
65
|
if (!target)
|
|
65
66
|
return void res.json({ error: '用户不存在' });
|
|
66
67
|
if (target.admin_type)
|
|
@@ -87,7 +88,7 @@ export function registerAdminAdminsRoutes(app, deps) {
|
|
|
87
88
|
res.json({ ok: true, user_id: targetUserId, admin_type: adminType, admin_scope: adminScope, admin_permissions: adminPerms });
|
|
88
89
|
});
|
|
89
90
|
// PATCH 更新权限(root only)
|
|
90
|
-
app.patch('/api/admin/admins/:id/permissions', (req, res) => {
|
|
91
|
+
app.patch('/api/admin/admins/:id/permissions', async (req, res) => {
|
|
91
92
|
const root = requireRootAdmin(req, res);
|
|
92
93
|
if (!root)
|
|
93
94
|
return;
|
|
@@ -102,33 +103,31 @@ export function registerAdminAdminsRoutes(app, deps) {
|
|
|
102
103
|
for (const p of adminPerms)
|
|
103
104
|
if (!validPerms.has(p))
|
|
104
105
|
return void res.json({ error: `权限 "${p}" 无效` });
|
|
105
|
-
const target =
|
|
106
|
+
const target = await dbOne(`SELECT id, admin_type FROM users WHERE id = ?`, [targetId]);
|
|
106
107
|
if (!target?.admin_type)
|
|
107
108
|
return void res.json({ error: '该用户不是 admin' });
|
|
108
109
|
if (target.admin_type === 'root')
|
|
109
110
|
return void res.json({ error: 'root admin 权限不可手动调整(永远是 all)' });
|
|
110
111
|
if (adminPerms.length === 0)
|
|
111
112
|
return void res.json({ error: '至少保留一项权限' });
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
db.prepare(`INSERT INTO admin_audit_log (id, admin_id, action, target_type, target_id, detail) VALUES (?,?,?,?,?,?)`)
|
|
115
|
-
.run(generateId('audit'), root.id, 'admin_update_perms', 'user', targetId, JSON.stringify({ admin_permissions: adminPerms, admin_scope: adminScope }));
|
|
113
|
+
await dbRun(`UPDATE users SET admin_permissions = ?${adminScope ? ', admin_scope = ?' : ''} WHERE id = ?`, [JSON.stringify(adminPerms), ...(adminScope ? [adminScope, targetId] : [targetId])]);
|
|
114
|
+
await dbRun(`INSERT INTO admin_audit_log (id, admin_id, action, target_type, target_id, detail) VALUES (?,?,?,?,?,?)`, [generateId('audit'), root.id, 'admin_update_perms', 'user', targetId, JSON.stringify({ admin_permissions: adminPerms, admin_scope: adminScope })]);
|
|
116
115
|
res.json({ ok: true, admin_permissions: adminPerms, admin_scope: adminScope });
|
|
117
116
|
});
|
|
118
117
|
// DELETE 撤销 admin(root only;不能撤自己;至少保留 1 个 root)
|
|
119
|
-
app.delete('/api/admin/admins/:id', (req, res) => {
|
|
118
|
+
app.delete('/api/admin/admins/:id', async (req, res) => {
|
|
120
119
|
const root = requireRootAdmin(req, res);
|
|
121
120
|
if (!root)
|
|
122
121
|
return;
|
|
123
122
|
const targetId = req.params.id;
|
|
124
123
|
if (targetId === root.id)
|
|
125
124
|
return void res.json({ error: '不能撤销自己' });
|
|
126
|
-
const target =
|
|
125
|
+
const target = await dbOne(`SELECT id, name, admin_type FROM users WHERE id = ?`, [targetId]);
|
|
127
126
|
if (!target || !target.admin_type)
|
|
128
127
|
return void res.json({ error: '该用户不是 admin' });
|
|
129
128
|
// 保护:至少保留 1 个 root
|
|
130
129
|
if (target.admin_type === 'root') {
|
|
131
|
-
const rootCount =
|
|
130
|
+
const rootCount = (await dbOne(`SELECT COUNT(1) as n FROM users WHERE admin_type = 'root'`, [])).n;
|
|
132
131
|
if (rootCount <= 1)
|
|
133
132
|
return void res.json({ error: '至少保留 1 个 root admin,不可撤销最后一个' });
|
|
134
133
|
}
|
|
@@ -1,36 +1,38 @@
|
|
|
1
|
+
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerAdminAnalyticsRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
-
|
|
3
|
+
// db 已全量走 RFC-016 异步 seam(dbOne/dbAll),不再直接用 deps.db
|
|
4
|
+
const { adminAuth, requireAdmin, requireRootAdmin, getProtocolParam, INTERNAL_AUDITOR_ID } = deps;
|
|
5
|
+
app.get('/api/admin/usage', async (req, res) => {
|
|
4
6
|
if (!adminAuth(req, res))
|
|
5
7
|
return;
|
|
6
|
-
const total =
|
|
7
|
-
const total24h =
|
|
8
|
-
const total7d =
|
|
9
|
-
const totalUsers =
|
|
10
|
-
const wau7d =
|
|
11
|
-
const dau24h =
|
|
12
|
-
const byTool =
|
|
8
|
+
const total = (await dbOne(`SELECT COUNT(*) as n FROM mcp_tool_calls`));
|
|
9
|
+
const total24h = (await dbOne(`SELECT COUNT(*) as n FROM mcp_tool_calls WHERE ts > datetime('now','-1 day')`));
|
|
10
|
+
const total7d = (await dbOne(`SELECT COUNT(*) as n FROM mcp_tool_calls WHERE ts > datetime('now','-7 day')`));
|
|
11
|
+
const totalUsers = (await dbOne(`SELECT COUNT(DISTINCT user_id_hash) as n FROM mcp_tool_calls WHERE user_id_hash IS NOT NULL`));
|
|
12
|
+
const wau7d = (await dbOne(`SELECT COUNT(DISTINCT user_id_hash) as n FROM mcp_tool_calls WHERE user_id_hash IS NOT NULL AND ts > datetime('now','-7 day')`));
|
|
13
|
+
const dau24h = (await dbOne(`SELECT COUNT(DISTINCT user_id_hash) as n FROM mcp_tool_calls WHERE user_id_hash IS NOT NULL AND ts > datetime('now','-1 day')`));
|
|
14
|
+
const byTool = await dbAll(`
|
|
13
15
|
SELECT tool_name,
|
|
14
16
|
COUNT(*) AS calls,
|
|
15
17
|
SUM(CASE WHEN outcome='error' THEN 1 ELSE 0 END) AS errors,
|
|
16
18
|
ROUND(AVG(latency_ms), 0) AS avg_latency_ms
|
|
17
19
|
FROM mcp_tool_calls WHERE ts > datetime('now','-7 day')
|
|
18
20
|
GROUP BY tool_name ORDER BY calls DESC
|
|
19
|
-
`)
|
|
20
|
-
const byDay =
|
|
21
|
+
`);
|
|
22
|
+
const byDay = await dbAll(`
|
|
21
23
|
SELECT substr(ts, 1, 10) AS day,
|
|
22
24
|
COUNT(*) AS calls,
|
|
23
25
|
COUNT(DISTINCT user_id_hash) AS distinct_users
|
|
24
26
|
FROM mcp_tool_calls WHERE ts > datetime('now','-14 day')
|
|
25
27
|
GROUP BY day ORDER BY day
|
|
26
|
-
`)
|
|
27
|
-
const byVersion =
|
|
28
|
+
`);
|
|
29
|
+
const byVersion = await dbAll(`
|
|
28
30
|
SELECT server_version,
|
|
29
31
|
COUNT(*) AS calls,
|
|
30
32
|
COUNT(DISTINCT user_id_hash) AS distinct_users
|
|
31
33
|
FROM mcp_tool_calls WHERE ts > datetime('now','-7 day')
|
|
32
34
|
GROUP BY server_version ORDER BY calls DESC
|
|
33
|
-
`)
|
|
35
|
+
`);
|
|
34
36
|
res.json({
|
|
35
37
|
summary: {
|
|
36
38
|
total_calls: total.n,
|
|
@@ -45,24 +47,23 @@ export function registerAdminAnalyticsRoutes(app, deps) {
|
|
|
45
47
|
by_version_7d: byVersion,
|
|
46
48
|
});
|
|
47
49
|
});
|
|
48
|
-
app.get('/api/admin/auditor', (req, res) => {
|
|
50
|
+
app.get('/api/admin/auditor', async (req, res) => {
|
|
49
51
|
const user = requireRootAdmin(req, res);
|
|
50
52
|
if (!user)
|
|
51
53
|
return;
|
|
52
|
-
const auditor =
|
|
53
|
-
.get(INTERNAL_AUDITOR_ID);
|
|
54
|
+
const auditor = await dbOne('SELECT id, name, api_key, created_at FROM users WHERE id = ?', [INTERNAL_AUDITOR_ID]);
|
|
54
55
|
if (!auditor)
|
|
55
56
|
return void res.json({ error: '内部审核账号未初始化' });
|
|
56
57
|
res.json({ id: auditor.id, name: auditor.name, api_key: auditor.api_key });
|
|
57
58
|
});
|
|
58
|
-
app.get('/api/admin/finance/monthly', (req, res) => {
|
|
59
|
+
app.get('/api/admin/finance/monthly', async (req, res) => {
|
|
59
60
|
const admin = requireAdmin(req, res);
|
|
60
61
|
if (!admin)
|
|
61
62
|
return;
|
|
62
63
|
const months = Math.max(3, Math.min(24, Number(req.query.months) || 12));
|
|
63
64
|
const feeShop = getProtocolParam('protocol_fee_rate_shop', 0.02);
|
|
64
65
|
const feeSecondhand = getProtocolParam('protocol_fee_rate_secondhand', 0.01);
|
|
65
|
-
const orderRows =
|
|
66
|
+
const orderRows = await dbAll(`
|
|
66
67
|
SELECT strftime('%Y-%m', created_at) as ym,
|
|
67
68
|
COALESCE(SUM(CASE WHEN source = 'secondhand' THEN total_amount * ? ELSE total_amount * ? END), 0) as fee,
|
|
68
69
|
COALESCE(SUM(total_amount), 0) as gmv,
|
|
@@ -71,13 +72,13 @@ export function registerAdminAnalyticsRoutes(app, deps) {
|
|
|
71
72
|
WHERE status = 'completed'
|
|
72
73
|
AND created_at > datetime('now', '-' || ? || ' months')
|
|
73
74
|
GROUP BY ym ORDER BY ym DESC
|
|
74
|
-
|
|
75
|
-
const rewardRows =
|
|
75
|
+
`, [feeSecondhand, feeShop, months]);
|
|
76
|
+
const rewardRows = await dbAll(`
|
|
76
77
|
SELECT strftime('%Y-%m', created_at) as ym, COALESCE(SUM(amount), 0) as rewards, COUNT(*) as count
|
|
77
78
|
FROM platform_reward_log
|
|
78
79
|
WHERE created_at > datetime('now', '-' || ? || ' months')
|
|
79
80
|
GROUP BY ym ORDER BY ym DESC
|
|
80
|
-
|
|
81
|
+
`, [months]);
|
|
81
82
|
const byMonth = new Map();
|
|
82
83
|
for (const o of orderRows)
|
|
83
84
|
byMonth.set(o.ym, { ym: o.ym, fee: o.fee, gmv: o.gmv, orders: o.orders_count, rewards: 0, reward_count: 0 });
|
|
@@ -94,7 +95,7 @@ export function registerAdminAnalyticsRoutes(app, deps) {
|
|
|
94
95
|
const totalFee = rows.reduce((s, r) => s + r.fee, 0);
|
|
95
96
|
const totalRewards = rows.reduce((s, r) => s + r.rewards, 0);
|
|
96
97
|
const totalGmv = rows.reduce((s, r) => s + r.gmv, 0);
|
|
97
|
-
const sysWallet =
|
|
98
|
+
const sysWallet = await dbOne("SELECT balance FROM wallets WHERE user_id = 'sys_protocol'");
|
|
98
99
|
res.json({
|
|
99
100
|
months,
|
|
100
101
|
fee_rate_shop: feeShop,
|
|
@@ -109,23 +110,23 @@ export function registerAdminAnalyticsRoutes(app, deps) {
|
|
|
109
110
|
},
|
|
110
111
|
});
|
|
111
112
|
});
|
|
112
|
-
app.get('/api/admin/protocol-kpi', (req, res) => {
|
|
113
|
+
app.get('/api/admin/protocol-kpi', async (req, res) => {
|
|
113
114
|
const admin = requireAdmin(req, res);
|
|
114
115
|
if (!admin)
|
|
115
116
|
return;
|
|
116
|
-
const windowCounts = (label, days) => {
|
|
117
|
+
const windowCounts = async (label, days) => {
|
|
117
118
|
const t = `datetime('now','-${days} days')`;
|
|
118
|
-
const orders =
|
|
119
|
-
const completed =
|
|
120
|
-
const disputes =
|
|
121
|
-
const refunds =
|
|
122
|
-
const newUsers =
|
|
119
|
+
const orders = (await dbOne(`SELECT COUNT(*) as n, COALESCE(SUM(total_amount),0) as gmv FROM orders WHERE created_at > ${t}`));
|
|
120
|
+
const completed = (await dbOne(`SELECT COUNT(*) as n FROM orders WHERE status='completed' AND created_at > ${t}`)).n;
|
|
121
|
+
const disputes = (await dbOne(`SELECT COUNT(*) as n FROM disputes WHERE created_at > ${t}`)).n;
|
|
122
|
+
const refunds = (await dbOne(`SELECT COUNT(*) as n FROM return_requests WHERE status='refunded' AND created_at > ${t}`)).n;
|
|
123
|
+
const newUsers = (await dbOne(`SELECT COUNT(*) as n FROM users WHERE created_at > ${t} AND id NOT IN ('sys_protocol', ?)`, [INTERNAL_AUDITOR_ID])).n;
|
|
123
124
|
return { label, days, orders: orders.n, gmv: orders.gmv, completed, disputes, refunds, new_users: newUsers,
|
|
124
125
|
dispute_rate: orders.n > 0 ? disputes / orders.n : 0,
|
|
125
126
|
refund_rate: completed > 0 ? refunds / completed : 0,
|
|
126
127
|
};
|
|
127
128
|
};
|
|
128
|
-
const dauProxy =
|
|
129
|
+
const dauProxy = (await dbOne(`
|
|
129
130
|
SELECT COUNT(DISTINCT u_id) as n FROM (
|
|
130
131
|
SELECT buyer_id as u_id FROM orders WHERE created_at > datetime('now', '-1 day')
|
|
131
132
|
UNION SELECT seller_id FROM orders WHERE created_at > datetime('now', '-1 day')
|
|
@@ -133,8 +134,8 @@ export function registerAdminAnalyticsRoutes(app, deps) {
|
|
|
133
134
|
UNION SELECT user_id FROM daily_checkins WHERE checkin_date >= date('now', '-1 day')
|
|
134
135
|
UNION SELECT user_id FROM feedback_tickets WHERE created_at > datetime('now', '-1 day')
|
|
135
136
|
)
|
|
136
|
-
`)
|
|
137
|
-
const mauProxy =
|
|
137
|
+
`)).n;
|
|
138
|
+
const mauProxy = (await dbOne(`
|
|
138
139
|
SELECT COUNT(DISTINCT u_id) as n FROM (
|
|
139
140
|
SELECT buyer_id as u_id FROM orders WHERE created_at > datetime('now', '-30 days')
|
|
140
141
|
UNION SELECT seller_id FROM orders WHERE created_at > datetime('now', '-30 days')
|
|
@@ -142,8 +143,8 @@ export function registerAdminAnalyticsRoutes(app, deps) {
|
|
|
142
143
|
UNION SELECT user_id FROM daily_checkins WHERE checkin_date >= date('now', '-30 days')
|
|
143
144
|
UNION SELECT user_id FROM feedback_tickets WHERE created_at > datetime('now', '-30 days')
|
|
144
145
|
)
|
|
145
|
-
`)
|
|
146
|
-
const userTotals =
|
|
146
|
+
`)).n;
|
|
147
|
+
const userTotals = (await dbOne(`
|
|
147
148
|
SELECT
|
|
148
149
|
SUM(CASE WHEN role='buyer' THEN 1 ELSE 0 END) as buyers,
|
|
149
150
|
SUM(CASE WHEN role='seller' THEN 1 ELSE 0 END) as sellers,
|
|
@@ -153,26 +154,26 @@ export function registerAdminAnalyticsRoutes(app, deps) {
|
|
|
153
154
|
SUM(CASE WHEN role='admin' THEN 1 ELSE 0 END) as admins,
|
|
154
155
|
COUNT(*) as total
|
|
155
156
|
FROM users WHERE id NOT IN ('sys_protocol', ?)
|
|
156
|
-
|
|
157
|
-
const sysWallet =
|
|
158
|
-
const totalEscrowed =
|
|
159
|
-
const totalStaked =
|
|
160
|
-
const platformRewards =
|
|
161
|
-
const platformRewardsToday =
|
|
162
|
-
const products =
|
|
163
|
-
const ratings =
|
|
164
|
-
const subs =
|
|
165
|
-
const trustOpen =
|
|
157
|
+
`, [INTERNAL_AUDITOR_ID]));
|
|
158
|
+
const sysWallet = await dbOne("SELECT balance FROM wallets WHERE user_id = 'sys_protocol'");
|
|
159
|
+
const totalEscrowed = (await dbOne("SELECT COALESCE(SUM(escrowed),0) as t FROM wallets")).t;
|
|
160
|
+
const totalStaked = (await dbOne("SELECT COALESCE(SUM(staked),0) as t FROM wallets")).t;
|
|
161
|
+
const platformRewards = (await dbOne("SELECT COALESCE(SUM(amount),0) as t FROM platform_reward_log")).t;
|
|
162
|
+
const platformRewardsToday = (await dbOne("SELECT COALESCE(SUM(amount),0) as t FROM platform_reward_log WHERE created_at > datetime('now','-1 day')")).t;
|
|
163
|
+
const products = (await dbOne("SELECT COUNT(*) as n, SUM(CASE WHEN status='active' THEN 1 ELSE 0 END) as active FROM products"));
|
|
164
|
+
const ratings = (await dbOne("SELECT COUNT(*) as n FROM order_ratings")).n;
|
|
165
|
+
const subs = (await dbOne("SELECT COUNT(*) as n FROM push_subscriptions WHERE enabled=1")).n;
|
|
166
|
+
const trustOpen = (await dbOne(`
|
|
166
167
|
SELECT
|
|
167
168
|
(SELECT COUNT(*) FROM disputes WHERE status IN ('open','in_review')) as disputes_open,
|
|
168
169
|
(SELECT COUNT(*) FROM feedback_tickets WHERE status IN ('open','in_progress')) as feedback_open,
|
|
169
170
|
(SELECT COUNT(*) FROM return_requests WHERE status='pending') as returns_pending
|
|
170
|
-
`)
|
|
171
|
+
`));
|
|
171
172
|
res.json({
|
|
172
173
|
activity: {
|
|
173
174
|
dau_proxy: dauProxy,
|
|
174
175
|
mau_proxy: mauProxy,
|
|
175
|
-
windows: [windowCounts('24h', 1), windowCounts('7d', 7), windowCounts('30d', 30)],
|
|
176
|
+
windows: await Promise.all([windowCounts('24h', 1), windowCounts('7d', 7), windowCounts('30d', 30)]),
|
|
176
177
|
},
|
|
177
178
|
users: userTotals,
|
|
178
179
|
finance: {
|
|
@@ -191,35 +192,35 @@ export function registerAdminAnalyticsRoutes(app, deps) {
|
|
|
191
192
|
trust_open: trustOpen,
|
|
192
193
|
});
|
|
193
194
|
});
|
|
194
|
-
app.get('/api/admin/dashboard', (req, res) => {
|
|
195
|
+
app.get('/api/admin/dashboard', async (req, res) => {
|
|
195
196
|
const admin = requireAdmin(req, res);
|
|
196
197
|
if (!admin)
|
|
197
198
|
return;
|
|
198
|
-
const u =
|
|
199
|
-
const sellers =
|
|
200
|
-
const active =
|
|
201
|
-
const o24 =
|
|
202
|
-
const dOpen =
|
|
203
|
-
const vOpen =
|
|
204
|
-
const locked =
|
|
205
|
-
const sus =
|
|
206
|
-
const verifierApps =
|
|
207
|
-
const verifierAppeals =
|
|
208
|
-
const quotaApps =
|
|
209
|
-
const listingPaused =
|
|
210
|
-
const activeVerifiers =
|
|
199
|
+
const u = (await dbOne("SELECT COUNT(*) as n FROM users WHERE id NOT IN ('sys_protocol', ?)", [INTERNAL_AUDITOR_ID]));
|
|
200
|
+
const sellers = (await dbOne("SELECT COUNT(*) as n FROM users WHERE role = 'seller' AND id NOT IN ('sys_protocol', ?)", [INTERNAL_AUDITOR_ID]));
|
|
201
|
+
const active = (await dbOne("SELECT COUNT(*) as n FROM products WHERE status = 'active'"));
|
|
202
|
+
const o24 = (await dbOne("SELECT COUNT(*) as n, COALESCE(SUM(total_amount),0) as gmv FROM orders WHERE created_at > datetime('now','-1 day')"));
|
|
203
|
+
const dOpen = (await dbOne("SELECT COUNT(*) as n FROM disputes WHERE status IN ('open','in_review')"));
|
|
204
|
+
const vOpen = (await dbOne("SELECT COUNT(*) as n FROM verify_tasks WHERE status IN ('open','code_issued')"));
|
|
205
|
+
const locked = (await dbOne("SELECT COALESCE(SUM(staked + escrowed),0) as t FROM wallets"));
|
|
206
|
+
const sus = (await dbOne("SELECT COUNT(*) as n FROM user_moderation WHERE suspended = 1"));
|
|
207
|
+
const verifierApps = (await dbOne("SELECT COUNT(*) as n FROM verifier_applications WHERE status = 'pending'"));
|
|
208
|
+
const verifierAppeals = (await dbOne("SELECT COUNT(*) as n FROM verifier_appeals WHERE status = 'pending'"));
|
|
209
|
+
const quotaApps = (await dbOne("SELECT COUNT(*) as n FROM quota_increase_applications WHERE status = 'pending'"));
|
|
210
|
+
const listingPaused = (await dbOne("SELECT COUNT(*) as n FROM users WHERE listing_paused = 1"));
|
|
211
|
+
const activeVerifiers = (await dbOne(`
|
|
211
212
|
SELECT COUNT(*) as n FROM verifier_whitelist vw
|
|
212
213
|
LEFT JOIN verifier_stats vs ON vs.user_id = vw.user_id
|
|
213
214
|
WHERE (vw.cooldown_until IS NULL OR vw.cooldown_until < datetime('now'))
|
|
214
215
|
AND (vs.suspended_until IS NULL OR vs.suspended_until < datetime('now'))
|
|
215
|
-
`)
|
|
216
|
-
const tokenomics = (() => {
|
|
217
|
-
const gf =
|
|
218
|
-
const mb =
|
|
219
|
-
const pendingLedger =
|
|
220
|
-
const commCount =
|
|
221
|
-
const dirtyUsers =
|
|
222
|
-
const matchedTotal =
|
|
216
|
+
`));
|
|
217
|
+
const tokenomics = await (async () => {
|
|
218
|
+
const gf = await dbOne("SELECT pool_balance, total_scores_pending, current_n, last_settled_at FROM global_fund WHERE id=1");
|
|
219
|
+
const mb = await dbOne("SELECT balance FROM management_bonus_pool WHERE id=1");
|
|
220
|
+
const pendingLedger = (await dbOne("SELECT COUNT(*) as n FROM pv_ledger WHERE processed = 0")).n;
|
|
221
|
+
const commCount = (await dbOne("SELECT COUNT(*) as n, COALESCE(SUM(amount),0) as t FROM commission_records"));
|
|
222
|
+
const dirtyUsers = (await dbOne("SELECT COUNT(*) as n FROM users WHERE pv_dirty_at IS NOT NULL")).n;
|
|
223
|
+
const matchedTotal = (await dbOne("SELECT COUNT(*) as n, COALESCE(SUM(waz_amount),0) as w FROM binary_score_records WHERE settled_at IS NOT NULL"));
|
|
223
224
|
return {
|
|
224
225
|
pool_balance: Number(gf?.pool_balance ?? 0),
|
|
225
226
|
scores_pending: Number(gf?.total_scores_pending ?? 0),
|
|
@@ -250,4 +251,43 @@ export function registerAdminAnalyticsRoutes(app, deps) {
|
|
|
250
251
|
tokenomics,
|
|
251
252
|
});
|
|
252
253
|
});
|
|
254
|
+
// RFC-002 rewards opt-in 生命周期监控(#937 A8)— 申请流 / 佣金 escrow / consent 版本漂移。
|
|
255
|
+
// 之前这三张表(rewards_applications / pending_commission_escrow / rewards_consent_texts)只有
|
|
256
|
+
// 引擎 cron 读写,无 admin 监控视图;上线后需盯:opt-in 申请量、escrow 待兑付/将到期、
|
|
257
|
+
// 以及"在旧 major consent 上仍 opted-in"= 下次 auto_downgrade cron 的降级候选。
|
|
258
|
+
app.get('/api/admin/rewards-health', async (req, res) => {
|
|
259
|
+
const admin = requireAdmin(req, res);
|
|
260
|
+
if (!admin)
|
|
261
|
+
return;
|
|
262
|
+
// 1. 申请流:按 action 计数 + 当前 opted-in 用户数 + 最近 20 条
|
|
263
|
+
const appsByAction = await dbAll(`SELECT action, COUNT(*) AS n FROM rewards_applications GROUP BY action ORDER BY n DESC`);
|
|
264
|
+
const optedIn = (await dbOne(`SELECT COUNT(*) AS n FROM users WHERE rewards_opted_in = 1`)).n;
|
|
265
|
+
const recentApps = await dbAll(`SELECT id, user_id, action, consent_version, verification_method, created_at
|
|
266
|
+
FROM rewards_applications ORDER BY created_at DESC LIMIT 20`);
|
|
267
|
+
// 2. 佣金 escrow:按 status 计数+金额、待兑付按 attribution_path、24h 内将到期
|
|
268
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
269
|
+
const escrowByStatus = await dbAll(`SELECT status, COUNT(*) AS n, COALESCE(SUM(amount),0) AS total
|
|
270
|
+
FROM pending_commission_escrow GROUP BY status`);
|
|
271
|
+
const escrowPendingByPath = await dbAll(`SELECT attribution_path, COUNT(*) AS n, COALESCE(SUM(amount),0) AS total
|
|
272
|
+
FROM pending_commission_escrow WHERE status='pending' GROUP BY attribution_path`);
|
|
273
|
+
const expiringSoon = (await dbOne(`SELECT COUNT(*) AS n, COALESCE(SUM(amount),0) AS total
|
|
274
|
+
FROM pending_commission_escrow WHERE status='pending' AND expires_at <= ?`, [nowSec + 86400]));
|
|
275
|
+
// 3. consent 版本:当前 major + 仍停留在旧 major 上的 opted-in 用户数(= auto_downgrade 候选)
|
|
276
|
+
const currentMajor = await dbOne(`SELECT version, effective_at FROM rewards_consent_texts WHERE change_class='major' ORDER BY effective_at DESC LIMIT 1`);
|
|
277
|
+
let staleConsentOptedIn = 0;
|
|
278
|
+
if (currentMajor) {
|
|
279
|
+
staleConsentOptedIn = (await dbOne(`SELECT COUNT(*) AS n FROM users u
|
|
280
|
+
WHERE u.rewards_opted_in = 1 AND (
|
|
281
|
+
SELECT consent_version FROM rewards_applications
|
|
282
|
+
WHERE user_id = u.id AND action IN ('activate','reconfirm') ORDER BY created_at DESC LIMIT 1
|
|
283
|
+
) IS NOT ?`, [currentMajor.version])).n;
|
|
284
|
+
}
|
|
285
|
+
const consentVersions = await dbAll(`SELECT version, change_class, effective_at FROM rewards_consent_texts ORDER BY effective_at DESC LIMIT 10`);
|
|
286
|
+
res.json({
|
|
287
|
+
applications: { by_action: appsByAction, opted_in_users: optedIn, recent: recentApps },
|
|
288
|
+
commission_escrow: { by_status: escrowByStatus, pending_by_path: escrowPendingByPath, expiring_within_24h: expiringSoon },
|
|
289
|
+
consent: { current_major: currentMajor || null, stale_consent_opted_in: staleConsentOptedIn, versions: consentVersions },
|
|
290
|
+
generated_at: new Date().toISOString(),
|
|
291
|
+
});
|
|
292
|
+
});
|
|
253
293
|
}
|
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
export function registerAdminAtomicRoutes(app, deps) {
|
|
2
|
-
const { requireProtocolAdmin, processPvLedger, runBinarySettlement, executeSafeSettlementCron } = deps;
|
|
2
|
+
const { requireProtocolAdmin, processPvLedger, runBinarySettlement, executeSafeSettlementCron, logAdminAction } = deps;
|
|
3
3
|
app.post('/api/admin/atomic/process-ledger', (req, res) => {
|
|
4
4
|
const admin = requireProtocolAdmin(req, res);
|
|
5
5
|
if (!admin)
|
|
6
6
|
return;
|
|
7
|
-
|
|
7
|
+
const processed = processPvLedger();
|
|
8
|
+
logAdminAction(admin.id, 'atomic_process_ledger', 'protocol', null, { processed });
|
|
9
|
+
res.json({ processed });
|
|
8
10
|
});
|
|
9
11
|
app.post('/api/admin/atomic/run-settlement', (req, res) => {
|
|
10
12
|
const admin = requireProtocolAdmin(req, res);
|
|
11
13
|
if (!admin)
|
|
12
14
|
return;
|
|
13
|
-
|
|
15
|
+
const settled = runBinarySettlement();
|
|
16
|
+
logAdminAction(admin.id, 'atomic_run_settlement', 'protocol', null, { settled });
|
|
17
|
+
res.json({ settled });
|
|
14
18
|
});
|
|
15
19
|
app.post('/api/admin/atomic/distribute', (req, res) => {
|
|
16
20
|
const admin = requireProtocolAdmin(req, res);
|
|
17
21
|
if (!admin)
|
|
18
22
|
return;
|
|
19
|
-
|
|
23
|
+
const result = executeSafeSettlementCron();
|
|
24
|
+
logAdminAction(admin.id, 'atomic_distribute', 'protocol', null, { result });
|
|
25
|
+
res.json(result);
|
|
20
26
|
});
|
|
21
27
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerAdminCatalogRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
+
// db 已走 RFC-016 异步 seam(dbOne/dbAll/dbRun),不再直接用 deps.db
|
|
4
|
+
const { requireContentAdmin, logAdminAction } = deps;
|
|
3
5
|
// ─── 类目 季节性配置 ─────────────────────────────────────
|
|
4
|
-
app.post('/api/admin/categories/:id/seasonal', (req, res) => {
|
|
6
|
+
app.post('/api/admin/categories/:id/seasonal', async (req, res) => {
|
|
5
7
|
const admin = requireContentAdmin(req, res);
|
|
6
8
|
if (!admin)
|
|
7
9
|
return;
|
|
@@ -12,22 +14,22 @@ export function registerAdminCatalogRoutes(app, deps) {
|
|
|
12
14
|
const valid = months.filter(m => Number.isInteger(m) && m >= 1 && m <= 12);
|
|
13
15
|
if (valid.length === 0)
|
|
14
16
|
return void res.json({ error: '没有有效的月份(1-12)' });
|
|
15
|
-
const cat =
|
|
17
|
+
const cat = await dbOne('SELECT id, name FROM product_categories WHERE id = ?', [req.params.id]);
|
|
16
18
|
if (!cat)
|
|
17
19
|
return void res.status(404).json({ error: 'category 不存在' });
|
|
18
20
|
const csv = [...new Set(valid)].sort((a, b) => a - b).join(',');
|
|
19
|
-
|
|
21
|
+
await dbRun('UPDATE product_categories SET seasonal_months = ? WHERE id = ?', [csv, req.params.id]);
|
|
20
22
|
res.json({ success: true, category: cat.name, seasonal_months: csv });
|
|
21
23
|
});
|
|
22
|
-
app.delete('/api/admin/categories/:id/seasonal', (req, res) => {
|
|
24
|
+
app.delete('/api/admin/categories/:id/seasonal', async (req, res) => {
|
|
23
25
|
const admin = requireContentAdmin(req, res);
|
|
24
26
|
if (!admin)
|
|
25
27
|
return;
|
|
26
|
-
|
|
28
|
+
await dbRun('UPDATE product_categories SET seasonal_months = NULL WHERE id = ?', [req.params.id]);
|
|
27
29
|
res.json({ success: true });
|
|
28
30
|
});
|
|
29
31
|
// ─── 商品 列表 + 强制下架 ───────────────────────────────
|
|
30
|
-
app.get('/api/admin/products', (req, res) => {
|
|
32
|
+
app.get('/api/admin/products', async (req, res) => {
|
|
31
33
|
const admin = requireContentAdmin(req, res);
|
|
32
34
|
if (!admin)
|
|
33
35
|
return;
|
|
@@ -41,23 +43,23 @@ export function registerAdminCatalogRoutes(app, deps) {
|
|
|
41
43
|
params.push(status);
|
|
42
44
|
}
|
|
43
45
|
sql += ` ORDER BY p.created_at DESC LIMIT 100`;
|
|
44
|
-
res.json({ products:
|
|
46
|
+
res.json({ products: await dbAll(sql, params) });
|
|
45
47
|
});
|
|
46
|
-
app.post('/api/admin/products/:id/force-delist', (req, res) => {
|
|
48
|
+
app.post('/api/admin/products/:id/force-delist', async (req, res) => {
|
|
47
49
|
// P0.5: 需 content 权限(之前仅 requireAdmin)
|
|
48
50
|
const admin = requireContentAdmin(req, res);
|
|
49
51
|
if (!admin)
|
|
50
52
|
return;
|
|
51
53
|
const { reason } = req.body;
|
|
52
54
|
const productId = req.params.id;
|
|
53
|
-
const product =
|
|
55
|
+
const product = await dbOne("SELECT id, status, title FROM products WHERE id = ?", [productId]);
|
|
54
56
|
if (!product)
|
|
55
57
|
return void res.json({ error: '商品不存在' });
|
|
56
58
|
if (product.status === 'deleted')
|
|
57
59
|
return void res.json({ error: '商品已删除' });
|
|
58
60
|
if (product.status === 'paused')
|
|
59
61
|
return void res.json({ error: '商品已是下架状态' });
|
|
60
|
-
|
|
62
|
+
await dbRun("UPDATE products SET status = 'paused', updated_at = datetime('now') WHERE id = ?", [productId]);
|
|
61
63
|
logAdminAction(admin.id, 'force_delist', 'product', productId, { reason: reason || null, title: product.title });
|
|
62
64
|
res.json({ success: true });
|
|
63
65
|
});
|
|
@@ -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 registerAdminEditorPicksRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
-
|
|
3
|
+
// db 已走 RFC-016 异步 seam(dbOne/dbAll/dbRun),不再直接用 deps.db
|
|
4
|
+
const { requireContentAdmin, generateId } = deps;
|
|
5
|
+
app.post('/api/admin/editor-picks', async (req, res) => {
|
|
4
6
|
const admin = requireContentAdmin(req, res);
|
|
5
7
|
if (!admin)
|
|
6
8
|
return;
|
|
@@ -10,11 +12,11 @@ export function registerAdminEditorPicksRoutes(app, deps) {
|
|
|
10
12
|
if (!target_id)
|
|
11
13
|
return void res.status(400).json({ error: 'target_id 必填' });
|
|
12
14
|
if (kind === 'product') {
|
|
13
|
-
if (!
|
|
15
|
+
if (!await dbOne("SELECT 1 FROM products WHERE id = ? AND status != 'deleted'", [target_id]))
|
|
14
16
|
return void res.status(400).json({ error: '商品不存在' });
|
|
15
17
|
}
|
|
16
18
|
else {
|
|
17
|
-
if (!
|
|
19
|
+
if (!await dbOne("SELECT 1 FROM users WHERE id = ? AND role = 'seller'", [target_id]))
|
|
18
20
|
return void res.status(400).json({ error: '卖家不存在' });
|
|
19
21
|
}
|
|
20
22
|
// SQLite 兼容:"YYYY-MM-DD HH:MM:SS" — ISO 'T'/毫秒会让窗口比较失效
|
|
@@ -24,22 +26,25 @@ export function registerAdminEditorPicksRoutes(app, deps) {
|
|
|
24
26
|
if (endsDate <= startsDate)
|
|
25
27
|
return void res.status(400).json({ error: 'ends_at 必须晚于 starts_at' });
|
|
26
28
|
const id = generateId('ep');
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
await dbRun(`INSERT INTO editor_picks (id, kind, target_id, title, note, starts_at, ends_at, sort_order, created_by) VALUES (?,?,?,?,?,?,?,?,?)`, [id, kind, target_id,
|
|
30
|
+
title ? String(title).slice(0, 100) : null,
|
|
31
|
+
note ? String(note).slice(0, 500) : null,
|
|
32
|
+
toSqliteUtc(startsDate), toSqliteUtc(endsDate),
|
|
33
|
+
Number(sort_order) || 0, admin.id]);
|
|
29
34
|
res.json({ success: true, id });
|
|
30
35
|
});
|
|
31
|
-
app.delete('/api/admin/editor-picks/:id', (req, res) => {
|
|
36
|
+
app.delete('/api/admin/editor-picks/:id', async (req, res) => {
|
|
32
37
|
const admin = requireContentAdmin(req, res);
|
|
33
38
|
if (!admin)
|
|
34
39
|
return;
|
|
35
|
-
|
|
40
|
+
await dbRun('DELETE FROM editor_picks WHERE id = ?', [req.params.id]);
|
|
36
41
|
res.json({ success: true });
|
|
37
42
|
});
|
|
38
|
-
app.get('/api/admin/editor-picks', (req, res) => {
|
|
43
|
+
app.get('/api/admin/editor-picks', async (req, res) => {
|
|
39
44
|
const admin = requireContentAdmin(req, res);
|
|
40
45
|
if (!admin)
|
|
41
46
|
return;
|
|
42
|
-
const rows =
|
|
47
|
+
const rows = await dbAll(`SELECT * FROM editor_picks ORDER BY ends_at DESC LIMIT 200`);
|
|
43
48
|
res.json({ items: rows });
|
|
44
49
|
});
|
|
45
50
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { dbOne } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerAdminEventsRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
+
// db 已走 RFC-016 异步 seam(dbOne),不再直接用 deps.db
|
|
4
|
+
const { requireAdmin, generateId, systemEventBuffer, SYSTEM_EVENT_BUFFER_SIZE, adminEventClients } = deps;
|
|
3
5
|
app.get('/api/admin/events/recent', (req, res) => {
|
|
4
6
|
const admin = requireAdmin(req, res);
|
|
5
7
|
if (!admin)
|
|
@@ -24,14 +26,14 @@ export function registerAdminEventsRoutes(app, deps) {
|
|
|
24
26
|
sseTickets.set(ticket, { userId: String(admin.id), expiresAt: Date.now() + 60_000 });
|
|
25
27
|
res.json({ ticket, expires_in: 60 });
|
|
26
28
|
});
|
|
27
|
-
app.get('/api/admin/events/stream', (req, res) => {
|
|
29
|
+
app.get('/api/admin/events/stream', async (req, res) => {
|
|
28
30
|
const ticket = String(req.query.ticket || '');
|
|
29
31
|
cleanupSseTickets();
|
|
30
32
|
const info = sseTickets.get(ticket);
|
|
31
33
|
if (!info)
|
|
32
34
|
return void res.status(403).json({ error: '无效或已过期的 ticket' });
|
|
33
35
|
sseTickets.delete(ticket); // 单次使用
|
|
34
|
-
const u =
|
|
36
|
+
const u = await dbOne('SELECT role FROM users WHERE id = ?', [info.userId]);
|
|
35
37
|
if (!u || u.role !== 'admin')
|
|
36
38
|
return void res.status(403).json({ error: '需要 admin 身份' });
|
|
37
39
|
res.setHeader('Content-Type', 'text/event-stream');
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dbOne } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerAdminHealthRoutes(app, deps) {
|
|
2
3
|
const { db, requireProtocolAdmin, getPublicClient, getRpcUrl, getNetwork, adminEventClients, sseClients, systemEventBuffer, authFailures } = deps;
|
|
3
4
|
app.get('/api/admin/health', async (req, res) => {
|
|
@@ -21,7 +22,7 @@ export function registerAdminHealthRoutes(app, deps) {
|
|
|
21
22
|
if (t === 'system_events_buffer_in_mem')
|
|
22
23
|
continue;
|
|
23
24
|
try {
|
|
24
|
-
tableCounts[t] =
|
|
25
|
+
tableCounts[t] = (await dbOne(`SELECT COUNT(*) as n FROM ${t}`)).n;
|
|
25
26
|
}
|
|
26
27
|
catch {
|
|
27
28
|
tableCounts[t] = -1;
|