@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,6 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
2
|
import { getQuestionsForRole, scoreQuiz } from '../data/onboarding-quiz.js';
|
|
3
3
|
import { getCasesForRole, getCasesForMaintainer, validateCaseReviews } from '../data/onboarding-cases.js';
|
|
4
|
+
// RFC-016 Phase 1 — 申请/题目/案例/申诉的校验读 + 列表读 + 单语句写 → async seam;
|
|
5
|
+
// activate/resign/resolve-appeal 的 3 个角色态 CAS db.transaction 保持同步(Phase 3 迁 pg)。
|
|
6
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js';
|
|
4
7
|
// PR #22 review fix P1-2:披露文本版本(server 与 client 同步,变更披露需 bump)
|
|
5
8
|
// client 在 src/pwa/public/app.js 必须用相同 version
|
|
6
9
|
export const GOVERNANCE_APPLY_DISCLOSURE_VERSION = 'v1.0-2026-06-02';
|
|
@@ -12,7 +15,7 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
12
15
|
function expectedConsentHash(role, userId, pageLoadedAt) {
|
|
13
16
|
return sha256_hex(`governance_apply|disclosure=${GOVERNANCE_APPLY_DISCLOSURE_VERSION}|role=${role}|user=${userId}|page_loaded_at=${pageLoadedAt}`);
|
|
14
17
|
}
|
|
15
|
-
app.post('/api/governance/onboarding/apply', (req, res) => {
|
|
18
|
+
app.post('/api/governance/onboarding/apply', async (req, res) => {
|
|
16
19
|
const user = auth(req, res);
|
|
17
20
|
if (!user)
|
|
18
21
|
return;
|
|
@@ -65,11 +68,11 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
65
68
|
return void errorRes(res, 401, 'PASSKEY_INVALID', `Passkey 验证失败: ${result.reason || '未知'}`);
|
|
66
69
|
}
|
|
67
70
|
}
|
|
68
|
-
const existing =
|
|
71
|
+
const existing = await dbOne(`
|
|
69
72
|
SELECT id, status, cooldown_until FROM governance_applications
|
|
70
73
|
WHERE user_id = ? AND role = ? AND status IN ('pending_onboarding', 'active', 'cooldown')
|
|
71
74
|
ORDER BY created_at DESC LIMIT 1
|
|
72
|
-
|
|
75
|
+
`, [userId, role]);
|
|
73
76
|
if (existing) {
|
|
74
77
|
if (existing.status === 'active') {
|
|
75
78
|
return void errorRes(res, 409, 'ALREADY_ACTIVE', `你已是 ${role},无需重复申请`);
|
|
@@ -100,11 +103,11 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
100
103
|
const id = generateId('gapp');
|
|
101
104
|
const ip = String(req.ip || req.headers['x-forwarded-for'] || 'unknown').split(',')[0].trim();
|
|
102
105
|
const ua = String(req.headers['user-agent'] || 'unknown');
|
|
103
|
-
|
|
106
|
+
await dbRun(`
|
|
104
107
|
INSERT INTO governance_applications
|
|
105
108
|
(id, user_id, role, action, status, consent_hash, passkey_sig, iron_rule_method, ip_hash, ua_hash)
|
|
106
109
|
VALUES (?, ?, ?, 'apply', 'pending_onboarding', ?, ?, ?, ?, ?)
|
|
107
|
-
|
|
110
|
+
`, [id, userId, role, consent_hash, passkey_sig, iron_rule_method, sha256_16(ip), sha256_16(ua)]);
|
|
108
111
|
res.json({
|
|
109
112
|
success: true,
|
|
110
113
|
application_id: id,
|
|
@@ -113,18 +116,18 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
113
116
|
note: 'Maintainer 将 review 你的申请。下一步:完成 onboarding 学习 + 案例分析 + 题目(本阶段未上线)',
|
|
114
117
|
});
|
|
115
118
|
});
|
|
116
|
-
app.get('/api/governance/onboarding/my', (req, res) => {
|
|
119
|
+
app.get('/api/governance/onboarding/my', async (req, res) => {
|
|
117
120
|
const user = auth(req, res);
|
|
118
121
|
if (!user)
|
|
119
122
|
return;
|
|
120
123
|
const userId = user.id;
|
|
121
|
-
const items =
|
|
124
|
+
const items = await dbAll(`
|
|
122
125
|
SELECT id, role, action, status, quiz_score, quiz_passed_at, cooldown_until, appeal_reason, appeal_resolution, created_at
|
|
123
126
|
FROM governance_applications
|
|
124
127
|
WHERE user_id = ?
|
|
125
128
|
ORDER BY created_at DESC
|
|
126
129
|
LIMIT 50
|
|
127
|
-
|
|
130
|
+
`, [userId]);
|
|
128
131
|
res.json({ items, count: items.length });
|
|
129
132
|
});
|
|
130
133
|
// GET /api/governance/onboarding/quiz?role=arbitrator|verifier
|
|
@@ -144,7 +147,7 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
144
147
|
// POST /api/governance/onboarding/quiz-submit
|
|
145
148
|
// 提交题目答案 → server-side scoring → 写 governance_applications.quiz_score
|
|
146
149
|
// body: { role, answers: [{question_id, answer}] }
|
|
147
|
-
app.post('/api/governance/onboarding/quiz-submit', (req, res) => {
|
|
150
|
+
app.post('/api/governance/onboarding/quiz-submit', async (req, res) => {
|
|
148
151
|
const user = auth(req, res);
|
|
149
152
|
if (!user)
|
|
150
153
|
return;
|
|
@@ -159,30 +162,35 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
159
162
|
return void errorRes(res, 400, 'MISSING_ANSWERS', 'answers 数组为空');
|
|
160
163
|
}
|
|
161
164
|
// 找用户的最新 pending_onboarding application(必须先 apply 才能提交 quiz)
|
|
162
|
-
const app_ =
|
|
165
|
+
const app_ = await dbOne(`
|
|
163
166
|
SELECT id, status FROM governance_applications
|
|
164
167
|
WHERE user_id = ? AND role = ? AND status = 'pending_onboarding'
|
|
165
168
|
ORDER BY created_at DESC LIMIT 1
|
|
166
|
-
|
|
169
|
+
`, [userId, role]);
|
|
167
170
|
if (!app_) {
|
|
168
171
|
return void errorRes(res, 404, 'NO_PENDING_APPLICATION', `未找到 ${role} 待审申请,请先提交申请`);
|
|
169
172
|
}
|
|
170
173
|
// 读 quiz_pass_score(protocol_params,默认 80)
|
|
171
|
-
const param =
|
|
174
|
+
const param = await dbOne("SELECT value FROM protocol_params WHERE key = ?", ['governance_onboarding.quiz_pass_score']);
|
|
172
175
|
const passThreshold = param ? Number(param.value) : 80;
|
|
173
176
|
// 评分
|
|
174
177
|
const result = scoreQuiz(role, answers, passThreshold);
|
|
175
178
|
// 更新 quiz_score(只在分数有提升时更新,允许重考)
|
|
176
|
-
const existing =
|
|
179
|
+
const existing = (await dbOne("SELECT quiz_score, quiz_passed_at FROM governance_applications WHERE id = ?", [app_.id]));
|
|
177
180
|
const newScore = result.score_pct;
|
|
181
|
+
// Codex #234 P1:await 预读与写之间 maintainer 可能激活/解决该申请;所有写必须带
|
|
182
|
+
// status='pending_onboarding' 守卫,否则会篡改已离开 pending 的 onboarding audit 证据。
|
|
178
183
|
if (existing.quiz_score == null || newScore > existing.quiz_score) {
|
|
179
|
-
|
|
184
|
+
const u = await dbRun("UPDATE governance_applications SET quiz_score = ? WHERE id = ? AND status = 'pending_onboarding'", [newScore, app_.id]);
|
|
185
|
+
if (u.changes === 0)
|
|
186
|
+
return void errorRes(res, 409, 'APPLICATION_MOVED', '申请状态已变更(已被激活/解决),无法更新成绩');
|
|
180
187
|
}
|
|
181
188
|
// PR #22 review fix P1-3:quiz pass 推进环节状态(quiz_passed_at 时间戳)
|
|
182
189
|
// 一旦合格,记录时间戳;后续不变(即便重考更低分,也不抹掉已合格状态)
|
|
190
|
+
// quiz_passed_at IS NULL 守卫保证"只盖一次戳",叠加 status 守卫防离开 pending 后篡改
|
|
183
191
|
if (result.passed && !existing.quiz_passed_at) {
|
|
184
192
|
const now = Math.floor(Date.now() / 1000);
|
|
185
|
-
|
|
193
|
+
await dbRun("UPDATE governance_applications SET quiz_passed_at = ? WHERE id = ? AND status = 'pending_onboarding' AND quiz_passed_at IS NULL", [now, app_.id]);
|
|
186
194
|
}
|
|
187
195
|
res.json({
|
|
188
196
|
success: true,
|
|
@@ -214,7 +222,7 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
214
222
|
// body: { role, reviews: [{case_id, chosen_verdict, reasoning}] }
|
|
215
223
|
// 写 governance_applications.case_review_text(JSON string)
|
|
216
224
|
// 不立即评分 — maintainer 上岗签字前(阶段 3 #1093)对比 expected_verdict
|
|
217
|
-
app.post('/api/governance/onboarding/case-review', (req, res) => {
|
|
225
|
+
app.post('/api/governance/onboarding/case-review', async (req, res) => {
|
|
218
226
|
const user = auth(req, res);
|
|
219
227
|
if (!user)
|
|
220
228
|
return;
|
|
@@ -229,11 +237,11 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
229
237
|
return void errorRes(res, 400, 'MISSING_REVIEWS', 'reviews 数组为空');
|
|
230
238
|
}
|
|
231
239
|
// 找用户的 pending_onboarding application
|
|
232
|
-
const app_ =
|
|
240
|
+
const app_ = await dbOne(`
|
|
233
241
|
SELECT id, status FROM governance_applications
|
|
234
242
|
WHERE user_id = ? AND role = ? AND status = 'pending_onboarding'
|
|
235
243
|
ORDER BY created_at DESC LIMIT 1
|
|
236
|
-
|
|
244
|
+
`, [userId, role]);
|
|
237
245
|
if (!app_) {
|
|
238
246
|
return void errorRes(res, 404, 'NO_PENDING_APPLICATION', `未找到 ${role} 待审申请,请先提交申请`);
|
|
239
247
|
}
|
|
@@ -255,8 +263,10 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
255
263
|
reasoning: r.reasoning.trim(),
|
|
256
264
|
})),
|
|
257
265
|
};
|
|
258
|
-
|
|
259
|
-
|
|
266
|
+
// Codex #234 P1:status 守卫,防 await 后申请被激活/解决时仍篡改 case_review audit 证据
|
|
267
|
+
const cu = await dbRun("UPDATE governance_applications SET case_review_text = ? WHERE id = ? AND status = 'pending_onboarding'", [JSON.stringify(payload), app_.id]);
|
|
268
|
+
if (cu.changes === 0)
|
|
269
|
+
return void errorRes(res, 409, 'APPLICATION_MOVED', '申请状态已变更(已被激活/解决),无法提交案例 review');
|
|
260
270
|
res.json({
|
|
261
271
|
success: true,
|
|
262
272
|
application_id: app_.id,
|
|
@@ -267,11 +277,11 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
267
277
|
// ─── Admin (maintainer activation flow, #1093 阶段 3) ─────────
|
|
268
278
|
// spec docs/GOVERNANCE-ONBOARDING.md §4.4
|
|
269
279
|
// GET /api/admin/governance/applications — 列出 pending_onboarding(可筛 quiz_passed + has_case_review)
|
|
270
|
-
app.get('/api/admin/governance/applications', (req, res) => {
|
|
280
|
+
app.get('/api/admin/governance/applications', async (req, res) => {
|
|
271
281
|
const admin = requireGovernanceAdmin(req, res);
|
|
272
282
|
if (!admin)
|
|
273
283
|
return;
|
|
274
|
-
const items =
|
|
284
|
+
const items = await dbAll(`
|
|
275
285
|
SELECT ga.id, ga.user_id, ga.role, ga.action, ga.status, ga.quiz_score, ga.quiz_passed_at,
|
|
276
286
|
CASE WHEN ga.case_review_text IS NOT NULL THEN 1 ELSE 0 END AS has_case_review,
|
|
277
287
|
ga.created_at,
|
|
@@ -281,21 +291,21 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
281
291
|
WHERE ga.status = 'pending_onboarding' AND ga.action = 'apply'
|
|
282
292
|
ORDER BY ga.created_at ASC
|
|
283
293
|
LIMIT 100
|
|
284
|
-
`)
|
|
294
|
+
`);
|
|
285
295
|
res.json({ items, count: items.length });
|
|
286
296
|
});
|
|
287
297
|
// GET /api/admin/governance/application/:id — 详情(含 expected_verdict 用于对比 — 仅 maintainer 看)
|
|
288
|
-
app.get('/api/admin/governance/application/:id', (req, res) => {
|
|
298
|
+
app.get('/api/admin/governance/application/:id', async (req, res) => {
|
|
289
299
|
const admin = requireGovernanceAdmin(req, res);
|
|
290
300
|
if (!admin)
|
|
291
301
|
return;
|
|
292
302
|
const id = req.params.id;
|
|
293
|
-
const row =
|
|
303
|
+
const row = await dbOne(`
|
|
294
304
|
SELECT ga.*, u.name AS user_name, u.handle, u.email
|
|
295
305
|
FROM governance_applications ga
|
|
296
306
|
JOIN users u ON u.id = ga.user_id
|
|
297
307
|
WHERE ga.id = ?
|
|
298
|
-
|
|
308
|
+
`, [id]);
|
|
299
309
|
if (!row)
|
|
300
310
|
return void errorRes(res, 404, 'NOT_FOUND', 'application 不存在');
|
|
301
311
|
const role = row.role;
|
|
@@ -319,7 +329,7 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
319
329
|
// 4. INSERT action='activate' row + users.roles 加 role
|
|
320
330
|
// 5. logAdminAction 留痕
|
|
321
331
|
// body: { application_id, webauthn_token, note? }
|
|
322
|
-
app.post('/api/admin/governance/activate', (req, res) => {
|
|
332
|
+
app.post('/api/admin/governance/activate', async (req, res) => {
|
|
323
333
|
const admin = requireGovernanceAdmin(req, res);
|
|
324
334
|
if (!admin)
|
|
325
335
|
return;
|
|
@@ -332,10 +342,10 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
332
342
|
return void errorRes(res, 400, 'MISSING_APPLICATION_ID', 'application_id 必填');
|
|
333
343
|
}
|
|
334
344
|
// 1. 找 pending application(predicate read,真 status check 在 transaction 内)
|
|
335
|
-
const app_ =
|
|
345
|
+
const app_ = await dbOne(`
|
|
336
346
|
SELECT id, user_id, role, status, quiz_passed_at, case_review_text
|
|
337
347
|
FROM governance_applications WHERE id = ?
|
|
338
|
-
|
|
348
|
+
`, [application_id]);
|
|
339
349
|
if (!app_)
|
|
340
350
|
return void errorRes(res, 404, 'NOT_FOUND', 'application 不存在');
|
|
341
351
|
if (app_.status !== 'pending_onboarding') {
|
|
@@ -424,8 +434,10 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
424
434
|
logAdminAction(adminId, 'governance_activate', 'user', app_.user_id, { role, application_id, note });
|
|
425
435
|
// 7. 通知 user(站内)
|
|
426
436
|
try {
|
|
427
|
-
|
|
428
|
-
|
|
437
|
+
await dbRun(`INSERT INTO notifications (id, user_id, type, title, body, order_id) VALUES (?,?,?,?,?,?)`, [generateId('ntf'), app_.user_id, 'governance',
|
|
438
|
+
`🎉 你的 ${role} 申请已通过`,
|
|
439
|
+
`你已正式上岗 ${role}。本通知由 maintainer ${adminId} 签发。详 #me 治理面板。`,
|
|
440
|
+
null]);
|
|
429
441
|
}
|
|
430
442
|
catch (_e) { /* notification 失败不阻塞 activate */ }
|
|
431
443
|
res.json({
|
|
@@ -441,7 +453,7 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
441
453
|
// POST /api/governance/onboarding/resign — 主动卸任
|
|
442
454
|
// body: { role, confirm_text, webauthn_token }
|
|
443
455
|
// confirm_text 必须等于 'RESIGN arbitrator' 或 'RESIGN verifier'(type-to-confirm 防误触)
|
|
444
|
-
app.post('/api/governance/onboarding/resign', (req, res) => {
|
|
456
|
+
app.post('/api/governance/onboarding/resign', async (req, res) => {
|
|
445
457
|
const user = auth(req, res);
|
|
446
458
|
if (!user)
|
|
447
459
|
return;
|
|
@@ -458,7 +470,7 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
458
470
|
return void errorRes(res, 400, 'CONFIRM_MISMATCH', `请准确输入 "${expectedConfirm}" 确认卸任(type-to-confirm 防误触)`);
|
|
459
471
|
}
|
|
460
472
|
// 真源判定:users.roles JSON 包含该 role(防 1 user 多 active 行 / 防 active 行存量不一致)
|
|
461
|
-
const userRow =
|
|
473
|
+
const userRow = await dbOne("SELECT roles FROM users WHERE id = ?", [userId]);
|
|
462
474
|
let currentRoles = [];
|
|
463
475
|
try {
|
|
464
476
|
currentRoles = JSON.parse(userRow?.roles || '[]');
|
|
@@ -470,21 +482,21 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
470
482
|
return void errorRes(res, 404, 'NOT_ACTIVE', `你当前不是 ${role},无需卸任`);
|
|
471
483
|
}
|
|
472
484
|
// 用于日志显示 + 后续 INSERT 关联(取最新的 active 行;若没有也容许,以 user.roles 为准)
|
|
473
|
-
const activeRow =
|
|
485
|
+
const activeRow = await dbOne(`
|
|
474
486
|
SELECT id FROM governance_applications
|
|
475
487
|
WHERE user_id = ? AND role = ? AND status = 'active'
|
|
476
488
|
ORDER BY created_at DESC LIMIT 1
|
|
477
|
-
|
|
489
|
+
`, [userId, role]);
|
|
478
490
|
// active case 检查(spec §6.1:有未结案 → block,要求先 transfer/完成)
|
|
479
491
|
// 只对 arbitrator 适用 — verifier 投票无长期 assigned
|
|
480
492
|
if (role === 'arbitrator') {
|
|
481
493
|
// disputes.assigned_arbitrators 是 JSON 数组;ruling_type IS NULL = 未结案
|
|
482
|
-
const openCases =
|
|
494
|
+
const openCases = await dbAll(`
|
|
483
495
|
SELECT id FROM disputes
|
|
484
496
|
WHERE ruling_type IS NULL
|
|
485
497
|
AND assigned_arbitrators IS NOT NULL
|
|
486
498
|
AND assigned_arbitrators LIKE ?
|
|
487
|
-
|
|
499
|
+
`, [`%"${userId}"%`]);
|
|
488
500
|
if (openCases.length > 0) {
|
|
489
501
|
return void res.status(409).json({
|
|
490
502
|
error: '尚有未结案 dispute',
|
|
@@ -563,7 +575,7 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
563
575
|
// POST /api/governance/onboarding/appeal — auto_deactivate 后申诉
|
|
564
576
|
// body: { source_application_id, appeal_reason }
|
|
565
577
|
// 必须:source 行 action='auto_deactivate' + window 内 + 未已 appeal + reason 长度
|
|
566
|
-
app.post('/api/governance/onboarding/appeal', (req, res) => {
|
|
578
|
+
app.post('/api/governance/onboarding/appeal', async (req, res) => {
|
|
567
579
|
const user = auth(req, res);
|
|
568
580
|
if (!user)
|
|
569
581
|
return;
|
|
@@ -578,10 +590,10 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
578
590
|
if (appeal_reason.length < minChars) {
|
|
579
591
|
return void errorRes(res, 400, 'REASON_TOO_SHORT', `申诉理由至少 ${minChars} 字符,当前 ${appeal_reason.length}`);
|
|
580
592
|
}
|
|
581
|
-
const source =
|
|
593
|
+
const source = await dbOne(`
|
|
582
594
|
SELECT id, user_id, role, action, status, created_at
|
|
583
595
|
FROM governance_applications WHERE id = ?
|
|
584
|
-
|
|
596
|
+
`, [source_application_id]);
|
|
585
597
|
if (!source)
|
|
586
598
|
return void errorRes(res, 404, 'SOURCE_NOT_FOUND', '原 application 不存在');
|
|
587
599
|
if (source.user_id !== userId)
|
|
@@ -595,20 +607,20 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
595
607
|
return void errorRes(res, 400, 'APPEAL_WINDOW_EXPIRED', `申诉窗口已过期(${windowDays} 天内可申诉)`);
|
|
596
608
|
}
|
|
597
609
|
// 已 appeal 过?(防重复)
|
|
598
|
-
const existing =
|
|
610
|
+
const existing = await dbOne(`
|
|
599
611
|
SELECT id, status FROM governance_applications
|
|
600
612
|
WHERE source_application_id = ? AND action = 'appeal'
|
|
601
613
|
ORDER BY created_at DESC LIMIT 1
|
|
602
|
-
|
|
614
|
+
`, [source_application_id]);
|
|
603
615
|
if (existing) {
|
|
604
616
|
return void errorRes(res, 409, 'APPEAL_EXISTS', `已对该 application 提交过申诉(id=${existing.id} status=${existing.status})`);
|
|
605
617
|
}
|
|
606
618
|
const id = generateId('gapp');
|
|
607
|
-
|
|
619
|
+
await dbRun(`
|
|
608
620
|
INSERT INTO governance_applications
|
|
609
621
|
(id, user_id, role, action, status, appeal_reason, source_application_id)
|
|
610
622
|
VALUES (?, ?, ?, 'appeal', 'pending_review', ?, ?)
|
|
611
|
-
|
|
623
|
+
`, [id, userId, source.role, appeal_reason, source_application_id]);
|
|
612
624
|
res.json({
|
|
613
625
|
success: true,
|
|
614
626
|
appeal_application_id: id,
|
|
@@ -618,12 +630,12 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
618
630
|
});
|
|
619
631
|
// GET /api/admin/governance/auto-deactivations — recent auto_deactivate audit
|
|
620
632
|
// spec §6.2 公示触发原因(透明 — 元规则 #1)
|
|
621
|
-
app.get('/api/admin/governance/auto-deactivations', (req, res) => {
|
|
633
|
+
app.get('/api/admin/governance/auto-deactivations', async (req, res) => {
|
|
622
634
|
const admin = requireGovernanceAdmin(req, res);
|
|
623
635
|
if (!admin)
|
|
624
636
|
return;
|
|
625
637
|
const limit = Math.min(200, Math.max(1, Number(req.query.limit) || 50));
|
|
626
|
-
const items =
|
|
638
|
+
const items = await dbAll(`
|
|
627
639
|
SELECT ga.id, ga.user_id, ga.role, ga.appeal_reason AS trigger_reason,
|
|
628
640
|
ga.cooldown_until, ga.created_at,
|
|
629
641
|
u.name AS user_name, u.handle,
|
|
@@ -634,15 +646,15 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
634
646
|
WHERE ga.action = 'auto_deactivate'
|
|
635
647
|
ORDER BY ga.created_at DESC
|
|
636
648
|
LIMIT ?
|
|
637
|
-
|
|
649
|
+
`, [limit]);
|
|
638
650
|
res.json({ items, count: items.length });
|
|
639
651
|
});
|
|
640
652
|
// GET /api/admin/governance/appeals — maintainer 看待裁决申诉
|
|
641
|
-
app.get('/api/admin/governance/appeals', (req, res) => {
|
|
653
|
+
app.get('/api/admin/governance/appeals', async (req, res) => {
|
|
642
654
|
const admin = requireGovernanceAdmin(req, res);
|
|
643
655
|
if (!admin)
|
|
644
656
|
return;
|
|
645
|
-
const items =
|
|
657
|
+
const items = await dbAll(`
|
|
646
658
|
SELECT ga.id, ga.user_id, ga.role, ga.appeal_reason, ga.source_application_id, ga.created_at,
|
|
647
659
|
u.name AS user_name, u.handle, u.email,
|
|
648
660
|
src.created_at AS auto_deactivate_at
|
|
@@ -652,13 +664,13 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
652
664
|
WHERE ga.action = 'appeal' AND ga.status = 'pending_review'
|
|
653
665
|
ORDER BY ga.created_at ASC
|
|
654
666
|
LIMIT 100
|
|
655
|
-
`)
|
|
667
|
+
`);
|
|
656
668
|
res.json({ items, count: items.length });
|
|
657
669
|
});
|
|
658
670
|
// POST /api/admin/governance/resolve-appeal — maintainer 裁决申诉
|
|
659
671
|
// body: { appeal_application_id, decision: 'accept' | 'reject', resolution_text, webauthn_token }
|
|
660
672
|
// accept → 恢复 active(spec §7.2) ;reject → 维持 inactive,公开理由
|
|
661
|
-
app.post('/api/admin/governance/resolve-appeal', (req, res) => {
|
|
673
|
+
app.post('/api/admin/governance/resolve-appeal', async (req, res) => {
|
|
662
674
|
const admin = requireGovernanceAdmin(req, res);
|
|
663
675
|
if (!admin)
|
|
664
676
|
return;
|
|
@@ -676,10 +688,10 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
676
688
|
if (resolution_text.length < 30) {
|
|
677
689
|
return void errorRes(res, 400, 'RESOLUTION_TOO_SHORT', '处置理由至少 30 字符(spec §7.2 公开理由)');
|
|
678
690
|
}
|
|
679
|
-
const appeal =
|
|
691
|
+
const appeal = await dbOne(`
|
|
680
692
|
SELECT id, user_id, role, action, status, source_application_id
|
|
681
693
|
FROM governance_applications WHERE id = ?
|
|
682
|
-
|
|
694
|
+
`, [appeal_application_id]);
|
|
683
695
|
if (!appeal)
|
|
684
696
|
return void errorRes(res, 404, 'NOT_FOUND', 'appeal 不存在');
|
|
685
697
|
if (appeal.action !== 'appeal')
|
|
@@ -748,8 +760,7 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
748
760
|
const title = decision === 'accept'
|
|
749
761
|
? `✅ 你的 ${appeal.role} 申诉已通过`
|
|
750
762
|
: `❌ 你的 ${appeal.role} 申诉被驳回`;
|
|
751
|
-
|
|
752
|
-
.run(generateId('ntf'), appeal.user_id, 'governance', title, resolution_text, null);
|
|
763
|
+
await dbRun(`INSERT INTO notifications (id, user_id, type, title, body, order_id) VALUES (?,?,?,?,?,?)`, [generateId('ntf'), appeal.user_id, 'governance', title, resolution_text, null]);
|
|
753
764
|
}
|
|
754
765
|
catch (_e) { /* ignore */ }
|
|
755
766
|
res.json({
|
|
@@ -762,19 +773,19 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
762
773
|
});
|
|
763
774
|
// GET /api/governance/onboarding/progress
|
|
764
775
|
// 返回 onboarding 整体进度(spec §4):申请状态 + 学习包(client localStorage) + 题目分数 + 案例(后续)
|
|
765
|
-
app.get('/api/governance/onboarding/progress', (req, res) => {
|
|
776
|
+
app.get('/api/governance/onboarding/progress', async (req, res) => {
|
|
766
777
|
const user = auth(req, res);
|
|
767
778
|
if (!user)
|
|
768
779
|
return;
|
|
769
780
|
const userId = user.id;
|
|
770
781
|
// 各 role 最新 application
|
|
771
|
-
const applications =
|
|
782
|
+
const applications = await dbAll(`
|
|
772
783
|
SELECT id, role, action, status, quiz_score, quiz_passed_at, case_review_text, cooldown_until, created_at
|
|
773
784
|
FROM governance_applications
|
|
774
785
|
WHERE user_id = ?
|
|
775
786
|
ORDER BY created_at DESC
|
|
776
|
-
|
|
777
|
-
const param =
|
|
787
|
+
`, [userId]);
|
|
788
|
+
const param = await dbOne("SELECT value FROM protocol_params WHERE key = ?", ['governance_onboarding.quiz_pass_score']);
|
|
778
789
|
const passThreshold = param ? Number(param.value) : 80;
|
|
779
790
|
res.json({
|
|
780
791
|
applications,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
const VALID_GB_DISCOUNT_MIN = 0.05;
|
|
2
3
|
const VALID_GB_DISCOUNT_MAX = 0.50;
|
|
3
4
|
/** 结算团购 — 成团创建订单 + 退差价;未达成全员退款。export 仅供 cron。 */
|
|
@@ -64,14 +65,14 @@ export function sweepExpiredGroupBuys(db, generateId, broadcastSystemEvent) {
|
|
|
64
65
|
export function registerGroupBuysRoutes(app, deps) {
|
|
65
66
|
const { db, generateId, auth, isTrustedRole, errorRes, broadcastSystemEvent } = deps;
|
|
66
67
|
// 卖家开团
|
|
67
|
-
app.post('/api/group-buys', (req, res) => {
|
|
68
|
+
app.post('/api/group-buys', async (req, res) => {
|
|
68
69
|
const user = auth(req, res);
|
|
69
70
|
if (!user)
|
|
70
71
|
return;
|
|
71
72
|
const { product_id, variant_id, target_count, discount_pct, duration_hours } = req.body || {};
|
|
72
73
|
if (!product_id)
|
|
73
74
|
return void res.status(400).json({ error: 'product_id 必填' });
|
|
74
|
-
const p =
|
|
75
|
+
const p = await dbOne('SELECT id, seller_id, price, has_variants FROM products WHERE id = ? AND status = \'active\'', [product_id]);
|
|
75
76
|
if (!p)
|
|
76
77
|
return void res.status(404).json({ error: '商品不存在或已下架' });
|
|
77
78
|
if (p.seller_id !== user.id)
|
|
@@ -86,13 +87,12 @@ export function registerGroupBuysRoutes(app, deps) {
|
|
|
86
87
|
if (Number(p.has_variants) === 1 && !variant_id)
|
|
87
88
|
return void res.status(400).json({ error: '该商品有规格,请指定 variant_id' });
|
|
88
89
|
if (variant_id) {
|
|
89
|
-
const v =
|
|
90
|
+
const v = await dbOne('SELECT id FROM product_variants WHERE id = ? AND product_id = ? AND is_active = 1', [variant_id, p.id]);
|
|
90
91
|
if (!v)
|
|
91
92
|
return void res.status(400).json({ error: 'variant 不存在' });
|
|
92
93
|
}
|
|
93
94
|
const id = generateId('gb');
|
|
94
|
-
|
|
95
|
-
.run(id, user.id, p.id, variant_id || null, target, disc, endsAt);
|
|
95
|
+
await dbRun(`INSERT INTO group_buys (id, seller_id, product_id, variant_id, target_count, discount_pct, ends_at) VALUES (?,?,?,?,?,?,?)`, [id, user.id, p.id, variant_id || null, target, disc, endsAt]);
|
|
96
96
|
try {
|
|
97
97
|
broadcastSystemEvent('group_buy_created', '👥', `团购创建 ${id} · 目标 ${target} 人 · ${(disc * 100).toFixed(0)}% off`, p.id);
|
|
98
98
|
}
|
|
@@ -100,8 +100,8 @@ export function registerGroupBuysRoutes(app, deps) {
|
|
|
100
100
|
res.json({ success: true, id, ends_at: endsAt });
|
|
101
101
|
});
|
|
102
102
|
// 公开列表
|
|
103
|
-
app.get('/api/group-buys/live', (_req, res) => {
|
|
104
|
-
const rows =
|
|
103
|
+
app.get('/api/group-buys/live', async (_req, res) => {
|
|
104
|
+
const rows = await dbAll(`
|
|
105
105
|
SELECT gb.*, p.title as product_title, p.price as original_price, p.images, p.category,
|
|
106
106
|
u.handle as seller_handle, u.name as seller_name,
|
|
107
107
|
(SELECT COUNT(*) FROM group_buy_participants WHERE group_buy_id = gb.id AND status != 'refunded') as joined_count
|
|
@@ -110,31 +110,31 @@ export function registerGroupBuysRoutes(app, deps) {
|
|
|
110
110
|
JOIN users u ON u.id = gb.seller_id
|
|
111
111
|
WHERE gb.status = 'active' AND gb.ends_at > datetime('now')
|
|
112
112
|
ORDER BY gb.ends_at ASC LIMIT 100
|
|
113
|
-
|
|
113
|
+
`, []);
|
|
114
114
|
res.json({ items: rows });
|
|
115
115
|
});
|
|
116
116
|
// 详情 + participants
|
|
117
|
-
app.get('/api/group-buys/:id', (req, res) => {
|
|
118
|
-
const gb =
|
|
117
|
+
app.get('/api/group-buys/:id', async (req, res) => {
|
|
118
|
+
const gb = await dbOne(`
|
|
119
119
|
SELECT gb.*, p.title as product_title, p.price as original_price, p.images, p.category,
|
|
120
120
|
u.handle as seller_handle, u.name as seller_name
|
|
121
121
|
FROM group_buys gb
|
|
122
122
|
JOIN products p ON p.id = gb.product_id
|
|
123
123
|
JOIN users u ON u.id = gb.seller_id
|
|
124
124
|
WHERE gb.id = ?
|
|
125
|
-
|
|
125
|
+
`, [req.params.id]);
|
|
126
126
|
if (!gb)
|
|
127
127
|
return void res.status(404).json({ error: '团购不存在' });
|
|
128
|
-
const participants =
|
|
128
|
+
const participants = await dbAll(`
|
|
129
129
|
SELECT p.id, p.buyer_id, p.status, p.created_at, u.handle as buyer_handle
|
|
130
130
|
FROM group_buy_participants p JOIN users u ON u.id = p.buyer_id
|
|
131
131
|
WHERE p.group_buy_id = ? AND p.status != 'refunded'
|
|
132
132
|
ORDER BY p.created_at ASC
|
|
133
|
-
|
|
133
|
+
`, [req.params.id]);
|
|
134
134
|
res.json({ ...gb, participants });
|
|
135
135
|
});
|
|
136
136
|
// 加入团购
|
|
137
|
-
app.post('/api/group-buys/:id/join', (req, res) => {
|
|
137
|
+
app.post('/api/group-buys/:id/join', async (req, res) => {
|
|
138
138
|
const user = auth(req, res);
|
|
139
139
|
if (!user)
|
|
140
140
|
return;
|
|
@@ -143,7 +143,7 @@ export function registerGroupBuysRoutes(app, deps) {
|
|
|
143
143
|
const { shipping_address } = req.body || {};
|
|
144
144
|
if (!shipping_address)
|
|
145
145
|
return void res.status(400).json({ error: '请填写收货地址' });
|
|
146
|
-
const gb =
|
|
146
|
+
const gb = await dbOne('SELECT id, seller_id, product_id, status, target_count, ends_at, discount_pct FROM group_buys WHERE id = ?', [req.params.id]);
|
|
147
147
|
if (!gb)
|
|
148
148
|
return void res.status(404).json({ error: '团购不存在' });
|
|
149
149
|
if (gb.status !== 'active')
|
|
@@ -152,14 +152,14 @@ export function registerGroupBuysRoutes(app, deps) {
|
|
|
152
152
|
return void res.status(400).json({ error: '团购已结束' });
|
|
153
153
|
if (gb.seller_id === user.id)
|
|
154
154
|
return void res.status(400).json({ error: '不可加入自己的团购' });
|
|
155
|
-
const existing =
|
|
155
|
+
const existing = await dbOne('SELECT id FROM group_buy_participants WHERE group_buy_id = ? AND buyer_id = ? AND status != \'refunded\'', [gb.id, user.id]);
|
|
156
156
|
if (existing)
|
|
157
157
|
return void res.status(400).json({ error: '已加入此团购' });
|
|
158
|
-
const product =
|
|
158
|
+
const product = await dbOne('SELECT price FROM products WHERE id = ?', [gb.product_id]);
|
|
159
159
|
if (!product)
|
|
160
160
|
return void res.status(500).json({ error: '商品记录缺失' });
|
|
161
161
|
const escrow = Number(product.price);
|
|
162
|
-
const wallet =
|
|
162
|
+
const wallet = await dbOne('SELECT balance FROM wallets WHERE user_id = ?', [user.id]);
|
|
163
163
|
if (!wallet || wallet.balance < escrow)
|
|
164
164
|
return void res.status(400).json({ error: `余额不足:需 ${escrow} WAZ` });
|
|
165
165
|
const id = generateId('gbp');
|
|
@@ -168,7 +168,7 @@ export function registerGroupBuysRoutes(app, deps) {
|
|
|
168
168
|
.run(id, gb.id, user.id, String(shipping_address).slice(0, 200), escrow);
|
|
169
169
|
db.prepare('UPDATE wallets SET balance = balance - ?, escrowed = escrowed + ? WHERE user_id = ?').run(escrow, escrow, user.id);
|
|
170
170
|
})();
|
|
171
|
-
const joined =
|
|
171
|
+
const joined = (await dbOne(`SELECT COUNT(*) as n FROM group_buy_participants WHERE group_buy_id = ? AND status != 'refunded'`, [gb.id])).n;
|
|
172
172
|
if (joined >= gb.target_count) {
|
|
173
173
|
try {
|
|
174
174
|
settleGroupBuy(db, generateId, broadcastSystemEvent, gb.id);
|
|
@@ -184,13 +184,13 @@ export function registerGroupBuysRoutes(app, deps) {
|
|
|
184
184
|
res.json({ success: true, id, joined_count: joined, target_count: gb.target_count });
|
|
185
185
|
});
|
|
186
186
|
// 离开团购
|
|
187
|
-
app.delete('/api/group-buys/:id/leave', (req, res) => {
|
|
187
|
+
app.delete('/api/group-buys/:id/leave', async (req, res) => {
|
|
188
188
|
const user = auth(req, res);
|
|
189
189
|
if (!user)
|
|
190
190
|
return;
|
|
191
|
-
const p =
|
|
191
|
+
const p = await dbOne(`SELECT p.id, p.escrow_amount, p.status, gb.status as gb_status FROM group_buy_participants p
|
|
192
192
|
JOIN group_buys gb ON gb.id = p.group_buy_id
|
|
193
|
-
WHERE p.group_buy_id = ? AND p.buyer_id =
|
|
193
|
+
WHERE p.group_buy_id = ? AND p.buyer_id = ?`, [req.params.id, user.id]);
|
|
194
194
|
if (!p)
|
|
195
195
|
return void res.status(404).json({ error: '未加入此团购' });
|
|
196
196
|
if (p.status === 'fulfilled')
|