@seasonkoh/webaz 0.1.23 → 0.1.25
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 +2 -0
- 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 +198 -83
- 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 +173 -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 +10 -2
- 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-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-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/contract-fingerprint.js +2 -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 +575 -68
- package/dist/pwa/public/i18n.js +29 -20
- package/dist/pwa/public/index.html +1 -0
- package/dist/pwa/public/openapi.json +2 -2
- 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-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 +26 -29
- package/dist/pwa/routes/admin-ops.js +22 -21
- 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 +54 -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 +7 -5
- 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 +153 -114
- package/dist/pwa/routes/auth-login.js +6 -4
- package/dist/pwa/routes/auth-read.js +11 -9
- package/dist/pwa/routes/auth-register.js +35 -20
- 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 +147 -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 +33 -30
- 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 +33 -17
- 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 +14 -16
- 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 +19 -17
- package/dist/pwa/routes/profile-prefs.js +11 -11
- package/dist/pwa/routes/promoter.js +55 -49
- 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 +30 -30
- package/dist/pwa/routes/recover-key.js +13 -12
- package/dist/pwa/routes/referral.js +37 -32
- 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 +59 -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 +57 -0
- package/dist/pwa/routes/shops.js +20 -18
- 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 +45 -0
- package/dist/pwa/routes/trial.js +69 -51
- 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 -60
- 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 +74 -36
- 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 +237 -81
- package/dist/version.js +1 -1
- package/package.json +47 -2
|
@@ -1,15 +1,17 @@
|
|
|
1
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerAuthSessionsRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
-
|
|
3
|
+
// db 已全量走 RFC-016 异步 seam(dbOne/dbAll/dbRun),不再直接用 deps.db
|
|
4
|
+
const { auth, verifyPassword, recordSession, generateSecureKey } = deps;
|
|
5
|
+
app.get('/api/auth/sessions', async (req, res) => {
|
|
4
6
|
const user = auth(req, res);
|
|
5
7
|
if (!user)
|
|
6
8
|
return;
|
|
7
9
|
const currentKey = req.headers.authorization?.replace('Bearer ', '') ?? '';
|
|
8
|
-
const rows =
|
|
10
|
+
const rows = await dbAll(`
|
|
9
11
|
SELECT id, ip, user_agent, fingerprint_hash, created_at, last_seen_at, revoked_at, api_key
|
|
10
12
|
FROM user_sessions WHERE user_id = ? AND revoked_at IS NULL
|
|
11
13
|
ORDER BY last_seen_at DESC LIMIT 30
|
|
12
|
-
|
|
14
|
+
`, [user.id]);
|
|
13
15
|
res.json({
|
|
14
16
|
sessions: rows.map(r => ({
|
|
15
17
|
id: r.id,
|
|
@@ -23,23 +25,23 @@ export function registerAuthSessionsRoutes(app, deps) {
|
|
|
23
25
|
});
|
|
24
26
|
});
|
|
25
27
|
// 远程吊销某个会话(不影响当前 session)
|
|
26
|
-
app.post('/api/auth/sessions/:id/revoke', (req, res) => {
|
|
28
|
+
app.post('/api/auth/sessions/:id/revoke', async (req, res) => {
|
|
27
29
|
const user = auth(req, res);
|
|
28
30
|
if (!user)
|
|
29
31
|
return;
|
|
30
32
|
const sid = req.params.id;
|
|
31
|
-
const row =
|
|
33
|
+
const row = await dbOne("SELECT user_id, api_key FROM user_sessions WHERE id = ?", [sid]);
|
|
32
34
|
if (!row || row.user_id !== user.id)
|
|
33
35
|
return void res.status(404).json({ error: '会话不存在' });
|
|
34
36
|
const currentKey = req.headers.authorization?.replace('Bearer ', '') ?? '';
|
|
35
37
|
if (row.api_key === currentKey)
|
|
36
38
|
return void res.json({ error: '不能吊销当前会话,请改用「全部登出」' });
|
|
37
|
-
|
|
39
|
+
await dbRun("UPDATE user_sessions SET revoked_at = datetime('now') WHERE id = ?", [sid]);
|
|
38
40
|
res.json({ ok: true });
|
|
39
41
|
});
|
|
40
42
|
// 一键全登出:rotate users.api_key + 吊销所有 session
|
|
41
43
|
// 要求密码二次验证(防 api_key 被盗后攻击者锁死真用户)
|
|
42
|
-
app.post('/api/auth/logout-all', (req, res) => {
|
|
44
|
+
app.post('/api/auth/logout-all', async (req, res) => {
|
|
43
45
|
const user = auth(req, res);
|
|
44
46
|
if (!user)
|
|
45
47
|
return;
|
|
@@ -49,9 +51,8 @@ export function registerAuthSessionsRoutes(app, deps) {
|
|
|
49
51
|
if (!verifyPassword(pwd, user.password_hash))
|
|
50
52
|
return void res.json({ error: '密码错误' });
|
|
51
53
|
const newKey = generateSecureKey('key');
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
.run(user.id);
|
|
54
|
+
await dbRun("UPDATE users SET api_key = ? WHERE id = ?", [newKey, user.id]);
|
|
55
|
+
await dbRun("UPDATE user_sessions SET revoked_at = datetime('now') WHERE user_id = ? AND revoked_at IS NULL", [user.id]);
|
|
55
56
|
// 为新 key 建一个 session 行(让当前发起者继续可用)
|
|
56
57
|
try {
|
|
57
58
|
recordSession(user.id, newKey, req);
|
|
@@ -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 registerBlocklistRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
-
|
|
3
|
+
// db 已走 RFC-016 异步 seam(dbOne/dbAll/dbRun),不再直接用 deps.db
|
|
4
|
+
const { auth } = deps;
|
|
5
|
+
app.post('/api/blocklist/:user_id', async (req, res) => {
|
|
4
6
|
const me = auth(req, res);
|
|
5
7
|
if (!me)
|
|
6
8
|
return;
|
|
@@ -9,52 +11,51 @@ export function registerBlocklistRoutes(app, deps) {
|
|
|
9
11
|
return void res.json({ error: '不能拉黑自己' });
|
|
10
12
|
if (target === 'sys_protocol')
|
|
11
13
|
return void res.json({ error: '不能拉黑系统账户' });
|
|
12
|
-
const exists =
|
|
14
|
+
const exists = await dbOne("SELECT 1 FROM users WHERE id = ?", [target]);
|
|
13
15
|
if (!exists)
|
|
14
16
|
return void res.json({ error: '用户不存在' });
|
|
15
17
|
const reason = (req.body?.reason || '').toString().slice(0, 200);
|
|
16
|
-
|
|
17
|
-
.run(me.id, target, reason || null);
|
|
18
|
+
await dbRun("INSERT OR IGNORE INTO user_blocklist (blocker_id, blocked_id, reason) VALUES (?, ?, ?)", [me.id, target, reason || null]);
|
|
18
19
|
res.json({ ok: true });
|
|
19
20
|
});
|
|
20
|
-
app.delete('/api/blocklist/:user_id', (req, res) => {
|
|
21
|
+
app.delete('/api/blocklist/:user_id', async (req, res) => {
|
|
21
22
|
const me = auth(req, res);
|
|
22
23
|
if (!me)
|
|
23
24
|
return;
|
|
24
|
-
|
|
25
|
+
await dbRun("DELETE FROM user_blocklist WHERE blocker_id = ? AND blocked_id = ?", [me.id, req.params.user_id]);
|
|
25
26
|
res.json({ ok: true });
|
|
26
27
|
});
|
|
27
28
|
// D-2: 列表
|
|
28
|
-
app.get('/api/blocklist', (req, res) => {
|
|
29
|
+
app.get('/api/blocklist', async (req, res) => {
|
|
29
30
|
const me = auth(req, res);
|
|
30
31
|
if (!me)
|
|
31
32
|
return;
|
|
32
|
-
const rows =
|
|
33
|
+
const rows = await dbAll(`
|
|
33
34
|
SELECT b.blocked_id, b.reason, b.created_at,
|
|
34
35
|
u.name, u.handle, u.role
|
|
35
36
|
FROM user_blocklist b
|
|
36
37
|
JOIN users u ON u.id = b.blocked_id
|
|
37
38
|
WHERE b.blocker_id = ?
|
|
38
39
|
ORDER BY b.created_at DESC LIMIT 200
|
|
39
|
-
|
|
40
|
+
`, [me.id]);
|
|
40
41
|
res.json({ items: rows });
|
|
41
42
|
});
|
|
42
|
-
app.get('/api/blocklist/me', (req, res) => {
|
|
43
|
+
app.get('/api/blocklist/me', async (req, res) => {
|
|
43
44
|
const me = auth(req, res);
|
|
44
45
|
if (!me)
|
|
45
46
|
return;
|
|
46
|
-
const rows =
|
|
47
|
+
const rows = await dbAll(`
|
|
47
48
|
SELECT b.blocked_id, b.reason, b.created_at, u.name as blocked_name, u.role as blocked_role
|
|
48
49
|
FROM user_blocklist b LEFT JOIN users u ON u.id = b.blocked_id
|
|
49
50
|
WHERE b.blocker_id = ? ORDER BY b.created_at DESC
|
|
50
|
-
|
|
51
|
+
`, [me.id]);
|
|
51
52
|
res.json({ blocked: rows });
|
|
52
53
|
});
|
|
53
|
-
app.get('/api/blocklist/:user_id/status', (req, res) => {
|
|
54
|
+
app.get('/api/blocklist/:user_id/status', async (req, res) => {
|
|
54
55
|
const me = auth(req, res);
|
|
55
56
|
if (!me)
|
|
56
57
|
return;
|
|
57
|
-
const row =
|
|
58
|
+
const row = await dbOne("SELECT 1 FROM user_blocklist WHERE blocker_id = ? AND blocked_id = ?", [me.id, req.params.user_id]);
|
|
58
59
|
res.json({ blocked: !!row });
|
|
59
60
|
});
|
|
60
61
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { dbOne } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
import { submitBuildFeedback, listMyBuildFeedback, getBuildFeedback, adminListBuildFeedback, adminUpdateBuildFeedback, triagePendingBuildFeedback, } from '../../layer2-business/L2-8-feedback/build-feedback-engine.js';
|
|
2
3
|
export function registerBuildFeedbackRoutes(app, deps) {
|
|
3
4
|
const { db, auth, requireSupportAdmin } = deps;
|
|
4
|
-
const hasPasskey = (userId) => ((
|
|
5
|
+
const hasPasskey = async (userId) => (((await dbOne('SELECT COUNT(*) AS n FROM webauthn_credentials WHERE user_id = ?', [userId]))?.n) || 0) > 0;
|
|
5
6
|
// ── 提交 ──────────────────────────────────────────────
|
|
6
|
-
app.post('/api/build-feedback', (req, res) => {
|
|
7
|
+
app.post('/api/build-feedback', async (req, res) => {
|
|
7
8
|
const user = auth(req, res);
|
|
8
9
|
if (!user)
|
|
9
10
|
return;
|
|
@@ -11,7 +12,7 @@ export function registerBuildFeedbackRoutes(app, deps) {
|
|
|
11
12
|
// 分级门(RFC-004 精确化 2026-06-05):
|
|
12
13
|
// 「报告问题」(ux_issue/bug = 用)→ 登录即可,不要 Passkey(没 Passkey 也要能报问题)
|
|
13
14
|
// 「建设平台」(proposal = 建)→ 必须 Passkey(真人锚点,后期贡献/奖励才有归属)
|
|
14
|
-
if (String(type) === 'proposal' && !hasPasskey(user.id)) {
|
|
15
|
+
if (String(type) === 'proposal' && !(await hasPasskey(user.id))) {
|
|
15
16
|
return void res.status(403).json({
|
|
16
17
|
error: '提交「改进提案 / proposal」需先绑定 Passkey 成为可问责真人 —— 提案是建设行为,被采纳会记入共建信誉,需真人锚点。报告 bug / 体验问题无需 Passkey。请在 webaz.xyz「我的」绑定 Passkey。',
|
|
17
18
|
error_code: 'PROPOSAL_REQUIRES_PASSKEY',
|
|
@@ -29,28 +30,28 @@ export function registerBuildFeedbackRoutes(app, deps) {
|
|
|
29
30
|
res.json(result);
|
|
30
31
|
});
|
|
31
32
|
// ── 闭环:我的反馈进度 ──(必须在 /:id 之前声明)──────────
|
|
32
|
-
app.get('/api/build-feedback/mine', (req, res) => {
|
|
33
|
+
app.get('/api/build-feedback/mine', async (req, res) => {
|
|
33
34
|
const user = auth(req, res);
|
|
34
35
|
if (!user)
|
|
35
36
|
return;
|
|
36
|
-
res.json({ feedback: listMyBuildFeedback(db, user.id) });
|
|
37
|
+
res.json({ feedback: await listMyBuildFeedback(db, user.id) });
|
|
37
38
|
});
|
|
38
|
-
app.get('/api/build-feedback/:id', (req, res) => {
|
|
39
|
+
app.get('/api/build-feedback/:id', async (req, res) => {
|
|
39
40
|
const user = auth(req, res);
|
|
40
41
|
if (!user)
|
|
41
42
|
return;
|
|
42
43
|
const isAdmin = !!(user.is_admin || user.admin_permissions); // 宽松判定;admin 端点另有严格门
|
|
43
|
-
const row = getBuildFeedback(db, String(req.params.id), user.id, !!isAdmin);
|
|
44
|
+
const row = await getBuildFeedback(db, String(req.params.id), user.id, !!isAdmin);
|
|
44
45
|
if (!row)
|
|
45
46
|
return void res.status(404).json({ error: '反馈不存在或无权查看' });
|
|
46
47
|
res.json(row);
|
|
47
48
|
});
|
|
48
49
|
// ── maintainer triage ────────────────────────────────
|
|
49
|
-
app.get('/api/admin/build-feedback', (req, res) => {
|
|
50
|
+
app.get('/api/admin/build-feedback', async (req, res) => {
|
|
50
51
|
if (!requireSupportAdmin(req, res))
|
|
51
52
|
return;
|
|
52
53
|
const status = typeof req.query.status === 'string' ? req.query.status : undefined;
|
|
53
|
-
res.json({ feedback: adminListBuildFeedback(db, status) });
|
|
54
|
+
res.json({ feedback: await adminListBuildFeedback(db, status) });
|
|
54
55
|
});
|
|
55
56
|
// RFC-005 Phase 2:AI 自动 triage(advisory)— 批量处理 received 反馈:去重 + 标风险/摘要 + 置 triaged。
|
|
56
57
|
// 不 resolve、不记功(人类的)。无 AI key 时只做确定性去重 + 置 triaged。
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { getBuildProfile } from '../../layer2-business/L2-9-contribution/build-reputation-engine.js';
|
|
2
|
+
import { withUncommittedValueBoundary } from '../../layer2-business/L2-9-contribution/contribution-display-envelope.js';
|
|
2
3
|
export function registerBuildReputationRoutes(app, deps) {
|
|
3
4
|
const { db, auth } = deps;
|
|
4
|
-
|
|
5
|
+
// PR-5A/5B: this legacy RFC-006 contributor dashboard is a contribution display surface, so its
|
|
6
|
+
// response is wrapped in the uncommitted-value boundary (RFC-017 I-12 / §7) — build_points/tier express
|
|
7
|
+
// BUILD reputation (coordination layer) only and promise no economic value.
|
|
8
|
+
app.get('/api/build-reputation/me', async (req, res) => {
|
|
5
9
|
const user = auth(req, res);
|
|
6
10
|
if (!user)
|
|
7
11
|
return;
|
|
8
|
-
res.json(getBuildProfile(db, user.id));
|
|
12
|
+
res.json(withUncommittedValueBoundary(await getBuildProfile(db, user.id)));
|
|
9
13
|
});
|
|
10
14
|
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import { createBuildTask,
|
|
1
|
+
import { createBuildTask, claimBuildTask, submitBuildTask, releaseBuildTask, resolveBuildTask, } from '../../layer2-business/L2-9-contribution/build-tasks-engine.js';
|
|
2
|
+
// PR9C-1 — read/filter the build_tasks core + PR9B agent metadata (member scope; restricted/internal hidden).
|
|
3
|
+
import { listBuildTasksWithAgentMetadata, getBuildTaskWithAgentMetadata, validateTaskFilters, withContributionReadEnvelope } from '../../layer2-business/L2-9-contribution/build-task-read.js';
|
|
4
|
+
// PR9C-2 — participation guard (claim/submit/release): restricted/internal 404 no-leak, auto_claimable, canonical PR.
|
|
5
|
+
import { guardParticipation, validatePrRefAgainstCanonical } from '../../layer2-business/L2-9-contribution/build-task-participation.js';
|
|
2
6
|
export function registerBuildTasksRoutes(app, deps) {
|
|
3
7
|
const { db, auth, requireSupportAdmin } = deps;
|
|
4
8
|
app.post('/api/build-tasks', (req, res) => {
|
|
@@ -11,53 +15,81 @@ export function registerBuildTasksRoutes(app, deps) {
|
|
|
11
15
|
return void res.status(result.error_code === 'RATE_LIMITED' ? 429 : 400).json(result);
|
|
12
16
|
res.json(result);
|
|
13
17
|
});
|
|
18
|
+
// PR9C-1: list now returns build_tasks core + parsed agent_metadata (null for old tasks) under the
|
|
19
|
+
// uncommitted value_boundary; member scope hides restricted/internal. Bad filter → fail-closed 400.
|
|
14
20
|
app.get('/api/build-tasks', (req, res) => {
|
|
15
21
|
const user = auth(req, res);
|
|
16
22
|
if (!user)
|
|
17
23
|
return;
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
const v = validateTaskFilters(req.query);
|
|
25
|
+
if (!v.ok)
|
|
26
|
+
return void res.status(400).json({ error: v.detail, error_code: v.code });
|
|
27
|
+
if (req.query.mine === '1')
|
|
28
|
+
v.filters.claimerId = user.id;
|
|
29
|
+
const tasks = listBuildTasksWithAgentMetadata(db, v.filters, 'member');
|
|
30
|
+
res.json(withContributionReadEnvelope({ tasks }));
|
|
22
31
|
});
|
|
23
32
|
app.get('/api/build-tasks/:id', (req, res) => {
|
|
24
33
|
const user = auth(req, res);
|
|
25
34
|
if (!user)
|
|
26
35
|
return;
|
|
27
|
-
const
|
|
28
|
-
if (!
|
|
36
|
+
const task = getBuildTaskWithAgentMetadata(db, String(req.params.id), 'member'); // null → not found OR restricted/internal (no leak)
|
|
37
|
+
if (!task)
|
|
29
38
|
return void res.status(404).json({ error: '任务不存在' });
|
|
30
|
-
|
|
39
|
+
// backward-compat: spread the legacy build_tasks fields + events at top level; only append the new fields.
|
|
40
|
+
res.json(withContributionReadEnvelope(task));
|
|
31
41
|
});
|
|
42
|
+
// PR9C-2: participation guard runs BEFORE the engine — restricted/internal → 404 no-leak; metadata public
|
|
43
|
+
// task → claim respects auto_claimable. Success appends value_boundary + canonical_contribution_target only.
|
|
32
44
|
app.post('/api/build-tasks/:id/claim', (req, res) => {
|
|
33
45
|
const user = auth(req, res);
|
|
34
46
|
if (!user)
|
|
35
47
|
return;
|
|
48
|
+
const g = guardParticipation(db, String(req.params.id), 'claim');
|
|
49
|
+
if (!g.ok)
|
|
50
|
+
return void res.status(g.status).json({ error: g.message, error_code: g.code });
|
|
36
51
|
const result = claimBuildTask(db, String(req.params.id), user.id, (req.body ?? {}).provenance);
|
|
37
52
|
if ('error' in result) {
|
|
38
53
|
const code = result.error_code === 'NOT_FOUND' ? 404 : result.error_code === 'TOO_MANY_CLAIMS' ? 429 : 409;
|
|
39
54
|
return void res.status(code).json(result);
|
|
40
55
|
}
|
|
41
|
-
res.json(result);
|
|
56
|
+
res.json(withContributionReadEnvelope(result));
|
|
42
57
|
});
|
|
43
58
|
app.post('/api/build-tasks/:id/submit', (req, res) => {
|
|
44
59
|
const user = auth(req, res);
|
|
45
60
|
if (!user)
|
|
46
61
|
return;
|
|
47
|
-
const
|
|
48
|
-
|
|
62
|
+
const g = guardParticipation(db, String(req.params.id), 'submit');
|
|
63
|
+
if (!g.ok)
|
|
64
|
+
return void res.status(g.status).json({ error: g.message, error_code: g.code });
|
|
65
|
+
const { pr_ref, note, verification_summary } = req.body ?? {};
|
|
66
|
+
// anti GitHub-target confusion: a PR must target the canonical repo (the response shows where to submit).
|
|
67
|
+
const pr = validatePrRefAgainstCanonical(pr_ref);
|
|
68
|
+
if (!pr.ok)
|
|
69
|
+
return void res.status(400).json(withContributionReadEnvelope({ error: pr.message, error_code: pr.code }));
|
|
70
|
+
// submit evidence (design contract): a PR/ref alone is not enough — the contributor must summarize what
|
|
71
|
+
// they ran/verified (the task's verification_commands + results). Fail-closed; stored in the event log.
|
|
72
|
+
const vs = typeof verification_summary === 'string' ? verification_summary.trim() : '';
|
|
73
|
+
if (vs.length < 1)
|
|
74
|
+
return void res.status(400).json(withContributionReadEnvelope({ error: 'submit requires a verification_summary — summarize what you ran/verified (the task verification_commands and their results)', error_code: 'VERIFICATION_SUMMARY_REQUIRED' }));
|
|
75
|
+
if (vs.length > 2000)
|
|
76
|
+
return void res.status(400).json(withContributionReadEnvelope({ error: 'verification_summary too long (max 2000 chars)', error_code: 'VERIFICATION_SUMMARY_TOO_LONG' }));
|
|
77
|
+
const result = submitBuildTask(db, String(req.params.id), user.id, pr_ref, note, vs);
|
|
49
78
|
if ('error' in result)
|
|
50
79
|
return void res.status(result.error_code === 'NOT_FOUND' ? 404 : 400).json(result);
|
|
51
|
-
res.json(result);
|
|
80
|
+
res.json(withContributionReadEnvelope(result));
|
|
52
81
|
});
|
|
53
82
|
app.post('/api/build-tasks/:id/release', (req, res) => {
|
|
54
83
|
const user = auth(req, res);
|
|
55
84
|
if (!user)
|
|
56
85
|
return;
|
|
86
|
+
const g = guardParticipation(db, String(req.params.id), 'release');
|
|
87
|
+
if (!g.ok)
|
|
88
|
+
return void res.status(g.status).json({ error: g.message, error_code: g.code });
|
|
57
89
|
const result = releaseBuildTask(db, String(req.params.id), user.id);
|
|
58
90
|
if ('error' in result)
|
|
59
91
|
return void res.status(result.error_code === 'NOT_FOUND' ? 404 : 400).json(result);
|
|
60
|
-
res.json(result);
|
|
92
|
+
res.json(withContributionReadEnvelope(result));
|
|
61
93
|
});
|
|
62
94
|
// 验收终态 —— 仅 admin/maintainer(验收=真人,RFC-006 不变量 2;不发奖励/不记信誉)
|
|
63
95
|
app.post('/api/admin/build-tasks/:id/resolve', (req, res) => {
|
|
@@ -1,21 +1,23 @@
|
|
|
1
|
+
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerBuyerFeedsRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
-
|
|
3
|
+
// db 已走 RFC-016 异步 seam(dbOne/dbAll),不再直接用 deps.db
|
|
4
|
+
const { auth, isTrustedRole, errorRes, getNearbyCellPrecision, getProtocolParam } = deps;
|
|
5
|
+
app.get('/api/recommendations/me', async (req, res) => {
|
|
4
6
|
const user = auth(req, res);
|
|
5
7
|
if (!user)
|
|
6
8
|
return;
|
|
7
9
|
if (isTrustedRole(user))
|
|
8
10
|
return void errorRes(res, 403, 'TRUSTED_ROLE_NO_TRADE', '受信角色无购物功能');
|
|
9
11
|
const limit = Math.min(30, Math.max(5, Number(req.query.limit) || 20));
|
|
10
|
-
const wishlistRows =
|
|
12
|
+
const wishlistRows = await dbAll(`
|
|
11
13
|
SELECT w.product_id, p.category, p.seller_id FROM user_wishlist w
|
|
12
14
|
JOIN products p ON p.id = w.product_id
|
|
13
15
|
WHERE w.user_id = ? AND p.status = 'active' LIMIT 50
|
|
14
|
-
|
|
15
|
-
const purchasedRows =
|
|
16
|
+
`, [user.id]);
|
|
17
|
+
const purchasedRows = await dbAll(`
|
|
16
18
|
SELECT DISTINCT product_id, seller_id FROM orders WHERE buyer_id = ? AND status = 'completed' LIMIT 200
|
|
17
|
-
|
|
18
|
-
const followedRows =
|
|
19
|
+
`, [user.id]);
|
|
20
|
+
const followedRows = await dbAll(`SELECT followee_id FROM follows WHERE follower_id = ?`, [user.id]);
|
|
19
21
|
const wishCats = new Set(wishlistRows.map(r => r.category).filter(Boolean));
|
|
20
22
|
const knownProductIds = new Set([...wishlistRows.map(r => r.product_id), ...purchasedRows.map(r => r.product_id)]);
|
|
21
23
|
const knownSellerIds = new Set([...wishlistRows.map(r => r.seller_id), ...purchasedRows.map(r => r.seller_id)]);
|
|
@@ -31,40 +33,40 @@ export function registerBuyerFeedsRoutes(app, deps) {
|
|
|
31
33
|
let followedProducts = [];
|
|
32
34
|
if (followedSellerIds.length > 0) {
|
|
33
35
|
const ph = followedSellerIds.map(() => '?').join(',');
|
|
34
|
-
followedProducts =
|
|
36
|
+
followedProducts = await dbAll(`
|
|
35
37
|
SELECT ${baseCols}
|
|
36
38
|
FROM products p JOIN users u ON u.id = p.seller_id
|
|
37
39
|
WHERE p.seller_id IN (${ph}) AND p.status = 'active' AND p.stock > 0 ${exclSql}
|
|
38
40
|
ORDER BY p.created_at DESC LIMIT 10
|
|
39
|
-
|
|
41
|
+
`, [...followedSellerIds, ...exclArgs]);
|
|
40
42
|
}
|
|
41
43
|
let categoryProducts = [];
|
|
42
44
|
if (wishCats.size > 0) {
|
|
43
45
|
const ph = [...wishCats].map(() => '?').join(',');
|
|
44
|
-
categoryProducts =
|
|
46
|
+
categoryProducts = await dbAll(`
|
|
45
47
|
SELECT ${baseCols}
|
|
46
48
|
FROM products p JOIN users u ON u.id = p.seller_id
|
|
47
49
|
WHERE p.category IN (${ph}) AND p.status = 'active' AND p.stock > 0 ${exclSql}
|
|
48
50
|
ORDER BY sales_count DESC LIMIT 10
|
|
49
|
-
|
|
51
|
+
`, [...wishCats, ...exclArgs]);
|
|
50
52
|
}
|
|
51
53
|
let pastSellerProducts = [];
|
|
52
54
|
const pastSellers = [...knownSellerIds].filter(s => !followedSellerIds.includes(s));
|
|
53
55
|
if (pastSellers.length > 0) {
|
|
54
56
|
const ph = pastSellers.map(() => '?').join(',');
|
|
55
|
-
pastSellerProducts =
|
|
57
|
+
pastSellerProducts = await dbAll(`
|
|
56
58
|
SELECT ${baseCols}
|
|
57
59
|
FROM products p JOIN users u ON u.id = p.seller_id
|
|
58
60
|
WHERE p.seller_id IN (${ph}) AND p.status = 'active' AND p.stock > 0 ${exclSql}
|
|
59
61
|
ORDER BY p.created_at DESC LIMIT 10
|
|
60
|
-
|
|
62
|
+
`, [...pastSellers, ...exclArgs]);
|
|
61
63
|
}
|
|
62
|
-
const fallback =
|
|
64
|
+
const fallback = await dbAll(`
|
|
63
65
|
SELECT ${baseCols}
|
|
64
66
|
FROM products p JOIN users u ON u.id = p.seller_id
|
|
65
67
|
WHERE p.status = 'active' AND p.stock > 0 ${exclSql}
|
|
66
68
|
ORDER BY sales_count DESC, p.created_at DESC LIMIT 10
|
|
67
|
-
|
|
69
|
+
`, exclArgs);
|
|
68
70
|
const seen = new Set();
|
|
69
71
|
const labeled = (bucket, arr) => arr.filter(it => {
|
|
70
72
|
const id = String(it.id);
|
|
@@ -88,7 +90,7 @@ export function registerBuyerFeedsRoutes(app, deps) {
|
|
|
88
90
|
},
|
|
89
91
|
});
|
|
90
92
|
});
|
|
91
|
-
app.get('/api/feed', (req, res) => {
|
|
93
|
+
app.get('/api/feed', async (req, res) => {
|
|
92
94
|
const user = auth(req, res);
|
|
93
95
|
if (!user)
|
|
94
96
|
return;
|
|
@@ -137,13 +139,13 @@ export function registerBuyerFeedsRoutes(app, deps) {
|
|
|
137
139
|
WHERE ts IS NOT NULL
|
|
138
140
|
ORDER BY ts DESC LIMIT 50
|
|
139
141
|
`;
|
|
140
|
-
const events =
|
|
142
|
+
const events = await dbAll(sql, params);
|
|
141
143
|
res.json({ events, scope });
|
|
142
144
|
});
|
|
143
145
|
// 雷达扫描 MVP (2026-05-29):scope 范围档 + window 时间窗,k≥3 守护贯穿
|
|
144
146
|
// scope: cell(本格) / neighbors(周边 3×3) / region(同城) / global(全网)
|
|
145
147
|
// window: 24h / 7d / 30d
|
|
146
|
-
app.get('/api/nearby', (req, res) => {
|
|
148
|
+
app.get('/api/nearby', async (req, res) => {
|
|
147
149
|
const user = auth(req, res);
|
|
148
150
|
if (!user)
|
|
149
151
|
return;
|
|
@@ -153,7 +155,7 @@ export function registerBuyerFeedsRoutes(app, deps) {
|
|
|
153
155
|
const days = windowKey === '24h' ? 1 : windowKey === '30d' ? 30 : 7;
|
|
154
156
|
const { precision_deg, approx_km } = getNearbyCellPrecision();
|
|
155
157
|
const K = getProtocolParam('nearby_k_anonymity', 3);
|
|
156
|
-
const u =
|
|
158
|
+
const u = (await dbOne("SELECT geo_lat, geo_lng, geo_updated_at, region FROM users WHERE id = ?", [user.id]));
|
|
157
159
|
const needsGeo = scope === 'cell' || scope === 'neighbors';
|
|
158
160
|
if (needsGeo && (u?.geo_lat == null || u?.geo_lng == null)) {
|
|
159
161
|
// 本格/周边需定位;同城/全网不需 → 前端可引导切到更大范围
|
|
@@ -186,24 +188,24 @@ export function registerBuyerFeedsRoutes(app, deps) {
|
|
|
186
188
|
scopeLabel = '全网';
|
|
187
189
|
}
|
|
188
190
|
const dayClause = `o.updated_at > datetime('now', '-${days} day')`;
|
|
189
|
-
const totals =
|
|
191
|
+
const totals = (await dbOne(`
|
|
190
192
|
SELECT COUNT(DISTINCT o.buyer_id) as au, COUNT(*) as orders
|
|
191
193
|
FROM orders o JOIN users u ON u.id = o.buyer_id
|
|
192
194
|
WHERE ${where} AND o.status = 'completed' AND ${dayClause}
|
|
193
|
-
|
|
195
|
+
`, args));
|
|
194
196
|
const sufficient = Number(totals.au) >= K;
|
|
195
|
-
const topProducts = sufficient ?
|
|
197
|
+
const topProducts = sufficient ? await dbAll(`
|
|
196
198
|
SELECT p.id, p.title, p.price, p.category, p.images, COUNT(DISTINCT o.buyer_id) as buyers
|
|
197
199
|
FROM orders o JOIN users u ON u.id = o.buyer_id JOIN products p ON p.id = o.product_id
|
|
198
200
|
WHERE ${where} AND o.status = 'completed' AND ${dayClause}
|
|
199
201
|
GROUP BY p.id HAVING buyers >= ? ORDER BY buyers DESC LIMIT 10
|
|
200
|
-
|
|
201
|
-
const topCategories = sufficient ?
|
|
202
|
+
`, [...args, K]) : [];
|
|
203
|
+
const topCategories = sufficient ? await dbAll(`
|
|
202
204
|
SELECT p.category, COUNT(*) as orders, COUNT(DISTINCT o.buyer_id) as buyers
|
|
203
205
|
FROM orders o JOIN users u ON u.id = o.buyer_id JOIN products p ON p.id = o.product_id
|
|
204
206
|
WHERE ${where} AND o.status = 'completed' AND ${dayClause} AND p.category IS NOT NULL
|
|
205
207
|
GROUP BY p.category HAVING buyers >= ? ORDER BY orders DESC LIMIT 6
|
|
206
|
-
|
|
208
|
+
`, [...args, K]) : [];
|
|
207
209
|
const staleDays = u?.geo_updated_at
|
|
208
210
|
? Math.floor((Date.now() - new Date(u.geo_updated_at.replace(' ', 'T') + 'Z').getTime()) / 86400_000)
|
|
209
211
|
: null;
|
package/dist/pwa/routes/cart.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { transition } from '../../layer0-foundation/L0-2-state-machine/engine.js';
|
|
2
2
|
import { notifyTransition } from '../../layer2-business/L2-6-notifications/notification-engine.js';
|
|
3
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
3
4
|
export function registerCartRoutes(app, deps) {
|
|
4
5
|
const { db, generateId, auth, isTrustedRole, errorRes, broadcastSystemEvent, checkStockAndMaybeDelist, addHours } = deps;
|
|
5
|
-
app.get('/api/cart', (req, res) => {
|
|
6
|
+
app.get('/api/cart', async (req, res) => {
|
|
6
7
|
const user = auth(req, res);
|
|
7
8
|
if (!user)
|
|
8
9
|
return;
|
|
9
|
-
const items =
|
|
10
|
+
const items = await dbAll(`
|
|
10
11
|
SELECT c.product_id, c.qty, c.added_at,
|
|
11
12
|
p.title, p.price, p.category, p.commission_rate, p.stock, p.status as product_status,
|
|
12
13
|
u.name as seller_name
|
|
@@ -15,10 +16,10 @@ export function registerCartRoutes(app, deps) {
|
|
|
15
16
|
JOIN users u ON u.id = p.seller_id
|
|
16
17
|
WHERE c.user_id = ?
|
|
17
18
|
ORDER BY c.added_at DESC
|
|
18
|
-
|
|
19
|
+
`, [user.id]);
|
|
19
20
|
res.json({ items });
|
|
20
21
|
});
|
|
21
|
-
app.post('/api/cart', (req, res) => {
|
|
22
|
+
app.post('/api/cart', async (req, res) => {
|
|
22
23
|
const user = auth(req, res);
|
|
23
24
|
if (!user)
|
|
24
25
|
return;
|
|
@@ -26,29 +27,29 @@ export function registerCartRoutes(app, deps) {
|
|
|
26
27
|
const q = Math.max(1, Math.min(99, Number(qty) || 1));
|
|
27
28
|
if (!product_id)
|
|
28
29
|
return void res.json({ error: 'product_id 必填' });
|
|
29
|
-
const product =
|
|
30
|
+
const product = await dbOne("SELECT id, status FROM products WHERE id = ?", [product_id]);
|
|
30
31
|
if (!product)
|
|
31
32
|
return void res.json({ error: '商品不存在' });
|
|
32
33
|
if (product.status !== 'active')
|
|
33
34
|
return void res.json({ error: '商品已下架' });
|
|
34
|
-
|
|
35
|
+
await dbRun(`
|
|
35
36
|
INSERT INTO cart_items (user_id, product_id, qty) VALUES (?, ?, ?)
|
|
36
37
|
ON CONFLICT(user_id, product_id) DO UPDATE SET qty = MIN(99, cart_items.qty + ?)
|
|
37
|
-
|
|
38
|
+
`, [user.id, product_id, q, q]);
|
|
38
39
|
res.json({ ok: true });
|
|
39
40
|
});
|
|
40
|
-
app.patch('/api/cart/:product_id', (req, res) => {
|
|
41
|
+
app.patch('/api/cart/:product_id', async (req, res) => {
|
|
41
42
|
const user = auth(req, res);
|
|
42
43
|
if (!user)
|
|
43
44
|
return;
|
|
44
45
|
const q = Math.max(1, Math.min(99, Number(req.body.qty) || 1));
|
|
45
|
-
const r =
|
|
46
|
+
const r = await dbRun("UPDATE cart_items SET qty = ? WHERE user_id = ? AND product_id = ?", [q, user.id, req.params.product_id]);
|
|
46
47
|
if (r.changes === 0)
|
|
47
48
|
return void res.json({ error: '购物车中没有此商品' });
|
|
48
49
|
res.json({ ok: true, qty: q });
|
|
49
50
|
});
|
|
50
51
|
// C-1: 购物车批量下单(按 seller 自动分订单)
|
|
51
|
-
app.post('/api/cart/checkout', (req, res) => {
|
|
52
|
+
app.post('/api/cart/checkout', async (req, res) => {
|
|
52
53
|
const user = auth(req, res);
|
|
53
54
|
if (!user)
|
|
54
55
|
return;
|
|
@@ -59,11 +60,11 @@ export function registerCartRoutes(app, deps) {
|
|
|
59
60
|
const { shipping_address, notes } = req.body || {};
|
|
60
61
|
if (!shipping_address)
|
|
61
62
|
return void res.status(400).json({ error: '请填写收货地址' });
|
|
62
|
-
const items =
|
|
63
|
+
const items = await dbAll(`
|
|
63
64
|
SELECT c.product_id, c.qty, p.title, p.price, p.stock, p.seller_id, p.has_variants, p.status
|
|
64
65
|
FROM cart_items c JOIN products p ON p.id = c.product_id
|
|
65
66
|
WHERE c.user_id = ?
|
|
66
|
-
|
|
67
|
+
`, [user.id]);
|
|
67
68
|
if (items.length === 0)
|
|
68
69
|
return void res.status(400).json({ error: '购物车为空' });
|
|
69
70
|
const skipped = [];
|
|
@@ -93,7 +94,7 @@ export function registerCartRoutes(app, deps) {
|
|
|
93
94
|
if (ok.length === 0) {
|
|
94
95
|
return void res.status(400).json({ error: '购物车中无可下单商品', skipped });
|
|
95
96
|
}
|
|
96
|
-
const wallet =
|
|
97
|
+
const wallet = await dbOne('SELECT balance FROM wallets WHERE user_id = ?', [user.id]);
|
|
97
98
|
if (!wallet)
|
|
98
99
|
return void res.status(500).json({ error: '钱包记录缺失' });
|
|
99
100
|
if (wallet.balance < totalNeed)
|
|
@@ -145,11 +146,11 @@ export function registerCartRoutes(app, deps) {
|
|
|
145
146
|
total_paid: created.reduce((s, c) => s + c.total, 0),
|
|
146
147
|
});
|
|
147
148
|
});
|
|
148
|
-
app.delete('/api/cart/:product_id', (req, res) => {
|
|
149
|
+
app.delete('/api/cart/:product_id', async (req, res) => {
|
|
149
150
|
const user = auth(req, res);
|
|
150
151
|
if (!user)
|
|
151
152
|
return;
|
|
152
|
-
|
|
153
|
+
await dbRun("DELETE FROM cart_items WHERE user_id = ? AND product_id = ?", [user.id, req.params.product_id]);
|
|
153
154
|
res.json({ ok: true });
|
|
154
155
|
});
|
|
155
156
|
}
|