@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,7 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
// RFC-016 Phase 1 — cron 的 currentMajor + 候选扫描读 → async seam;逐用户降级 db.transaction 写仍同步(Phase 3)。
|
|
2
|
+
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js';
|
|
3
|
+
export async function runAutoDowngradeSweep(deps) {
|
|
2
4
|
const { db, getProtocolParam } = deps;
|
|
3
5
|
// Current major consent
|
|
4
|
-
const currentMajor =
|
|
6
|
+
const currentMajor = await dbOne(`SELECT version, effective_at FROM rewards_consent_texts WHERE change_class='major' ORDER BY effective_at DESC LIMIT 1`);
|
|
5
7
|
if (!currentMajor)
|
|
6
8
|
return { scanned: 0, downgraded: [], skip_reason: 'no major consent text in rewards_consent_texts' };
|
|
7
9
|
const graceDays = Number(getProtocolParam('rewards_opt_in.reconfirm_grace_days', 14));
|
|
@@ -11,14 +13,14 @@ export function runAutoDowngradeSweep(deps) {
|
|
|
11
13
|
return { scanned: 0, downgraded: [], skip_reason: `current_major ${currentMajor.version} grace not yet expired (deadline=${deadline})` };
|
|
12
14
|
// Candidates: opted-in users whose LATEST activate-or-reconfirm consent_version
|
|
13
15
|
// is older than the current major.
|
|
14
|
-
const candidates =
|
|
16
|
+
const candidates = await dbAll(`
|
|
15
17
|
SELECT u.id AS user_id, (
|
|
16
18
|
SELECT consent_version FROM rewards_applications
|
|
17
19
|
WHERE user_id = u.id AND action IN ('activate','reconfirm')
|
|
18
20
|
ORDER BY created_at DESC LIMIT 1
|
|
19
21
|
) AS last_version
|
|
20
22
|
FROM users u WHERE u.rewards_opted_in = 1
|
|
21
|
-
`)
|
|
23
|
+
`);
|
|
22
24
|
const downgraded = [];
|
|
23
25
|
for (const c of candidates) {
|
|
24
26
|
if (c.last_version === currentMajor.version)
|
|
@@ -40,7 +42,7 @@ export function runAutoDowngradeSweep(deps) {
|
|
|
40
42
|
// Failure here is non-fatal — notification is best-effort, downgrade itself is the source of truth.
|
|
41
43
|
try {
|
|
42
44
|
db.prepare(`INSERT INTO notifications (id, user_id, type, title, body) VALUES (?, ?, 'rewards_auto_downgrade', ?, ?)`)
|
|
43
|
-
.run(`ntf_rwd_${c.user_id}_${now}`, c.user_id, '
|
|
45
|
+
.run(`ntf_rwd_${c.user_id}_${now}`, c.user_id, '分享分润已自动降级 / Rewards auto-downgraded', `新 consent 版本 ${currentMajor.version} 未在 grace 期内重新确认。未来 commission 进入 escrow(30 天可激活领回)。前往 #rewards-me 重新申请。 / New consent ${currentMajor.version} not re-confirmed within grace window. Future commission flows to escrow (30d recovery window). Visit #rewards-me to re-apply.`);
|
|
44
46
|
}
|
|
45
47
|
catch { /* notifications schema diff between envs; best-effort */ }
|
|
46
48
|
downgraded.push({ user_id: c.user_id, last_version: c.last_version, current_major: currentMajor.version, effective_at: currentMajor.effective_at });
|
|
@@ -50,9 +52,9 @@ export function runAutoDowngradeSweep(deps) {
|
|
|
50
52
|
}
|
|
51
53
|
export function startAutoDowngradeCron(deps) {
|
|
52
54
|
const ms = 24 * 60 * 60 * 1000; // 1d fixed
|
|
53
|
-
setInterval(() => {
|
|
55
|
+
setInterval(async () => {
|
|
54
56
|
try {
|
|
55
|
-
const r = runAutoDowngradeSweep(deps);
|
|
57
|
+
const r = await runAutoDowngradeSweep(deps);
|
|
56
58
|
if (r.downgraded.length > 0) {
|
|
57
59
|
console.log(`[rewards-auto-downgrade] swept ${r.scanned}, downgraded ${r.downgraded.length}: ${r.downgraded.map(d => `${d.user_id} ${d.last_version || '(none)'}→${d.current_major}`).join(', ')}`);
|
|
58
60
|
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
// RFC-016 Phase 1 — cron 扫描读 → async seam;到期 materialize 的 db.transaction 写仍同步(Phase 3 迁 pg)。
|
|
2
|
+
import { dbAll } from '../../layer0-foundation/L0-1-database/db.js';
|
|
3
|
+
export async function runEscrowExpireSweep(deps) {
|
|
2
4
|
const { db, redirectToCommissionReserve } = deps;
|
|
3
5
|
const now = Date.now();
|
|
4
|
-
const rows =
|
|
6
|
+
const rows = await dbAll(`
|
|
5
7
|
SELECT id, recipient_user_id, order_id, amount, attribution_path, expires_at
|
|
6
8
|
FROM pending_commission_escrow
|
|
7
9
|
WHERE status = 'pending' AND expires_at <= ?
|
|
8
10
|
ORDER BY expires_at ASC
|
|
9
11
|
LIMIT 1000
|
|
10
|
-
|
|
12
|
+
`, [now]);
|
|
11
13
|
const expired = [];
|
|
12
14
|
for (const r of rows) {
|
|
13
15
|
db.transaction(() => {
|
|
@@ -33,9 +35,9 @@ export function runEscrowExpireSweep(deps) {
|
|
|
33
35
|
}
|
|
34
36
|
export function startEscrowExpireCron(deps) {
|
|
35
37
|
const ms = 60 * 60 * 1000; // 1h fixed (escrow_days is in days; sub-day granularity unnecessary)
|
|
36
|
-
setInterval(() => {
|
|
38
|
+
setInterval(async () => {
|
|
37
39
|
try {
|
|
38
|
-
const r = runEscrowExpireSweep(deps);
|
|
40
|
+
const r = await runEscrowExpireSweep(deps);
|
|
39
41
|
if (r.expired.length > 0) {
|
|
40
42
|
console.log(`[rewards-escrow-expire] swept ${r.scanned}, expired ${r.expired.length}: ${r.expired.map(e => `${e.recipient_user_id}/${e.attribution_path}/${e.amount}`).join(', ')}`);
|
|
41
43
|
}
|
package/dist/pwa/routes/rfqs.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js';
|
|
1
2
|
export function registerRfqsRoutes(app, deps) {
|
|
2
3
|
const { db, auth, generateId, VALID_RFQ_URGENCIES, VALID_AWARD_MODES, RFQ_MAX_QTY, RFQ_MAX_PRICE, RFQ_DAILY_CAP_PER_BUYER, RFQ_MAX_WINDOW_MIN, RFQ_DEFAULT_WINDOW_MIN, BID_DAILY_CAP_PER_SELLER, BID_STAKE_RATE, VALID_FULFILLMENT_TYPES, isListingCategoryKey, LISTING_CATEGORIES, awardBidAndCreateOrder, notifyMatchedSellers, evaluateAutoBidsForRfq, shouldAutoAccept, transition, notifyTransition } = deps;
|
|
3
4
|
// 内联押金 helper(小巧、仅 rfq 域用)
|
|
@@ -7,7 +8,7 @@ export function registerRfqsRoutes(app, deps) {
|
|
|
7
8
|
};
|
|
8
9
|
const bidStakeFor = (price, qty) => Math.max(0.5, Math.round(price * qty * BID_STAKE_RATE * 100) / 100);
|
|
9
10
|
// 买家:创建 RFQ
|
|
10
|
-
app.post('/api/rfqs', (req, res) => {
|
|
11
|
+
app.post('/api/rfqs', async (req, res) => {
|
|
11
12
|
const user = auth(req, res);
|
|
12
13
|
if (!user)
|
|
13
14
|
return;
|
|
@@ -36,17 +37,17 @@ export function registerRfqsRoutes(app, deps) {
|
|
|
36
37
|
return void res.json({ error: '类目无效' });
|
|
37
38
|
const explicitWindow = body.award_window_min != null ? Math.max(5, Math.min(RFQ_MAX_WINDOW_MIN, Math.floor(Number(body.award_window_min)))) : null;
|
|
38
39
|
const windowMin = explicitWindow ?? RFQ_DEFAULT_WINDOW_MIN[urgency];
|
|
39
|
-
const todayCount =
|
|
40
|
+
const todayCount = (await dbOne("SELECT COUNT(1) as n FROM rfqs WHERE buyer_id = ? AND created_at > datetime('now','-1 day')", [user.id])).n;
|
|
40
41
|
if (todayCount >= RFQ_DAILY_CAP_PER_BUYER) {
|
|
41
42
|
return void res.json({ error: `今日已达上限 ${RFQ_DAILY_CAP_PER_BUYER} 单求购` });
|
|
42
43
|
}
|
|
43
44
|
const deposit = buyerRfqDeposit(maxPrice, qty);
|
|
44
|
-
const wallet =
|
|
45
|
+
const wallet = await dbOne('SELECT balance FROM wallets WHERE user_id = ?', [user.id]);
|
|
45
46
|
if (!wallet || Number(wallet.balance) < deposit) {
|
|
46
47
|
return void res.json({ error: `余额不足,发求购需 ${deposit} WAZ 押金(中标后释放,撤销扣 30%)` });
|
|
47
48
|
}
|
|
48
49
|
// P3c:award 自动建单需要收货地址。优先 body,否则 buyer 的默认地址
|
|
49
|
-
const buyerProfile =
|
|
50
|
+
const buyerProfile = await dbOne('SELECT default_address_text, default_address_json FROM users WHERE id = ?', [user.id]);
|
|
50
51
|
let shippingAddress = body.shipping_address ? String(body.shipping_address).trim() : null;
|
|
51
52
|
if (!shippingAddress) {
|
|
52
53
|
if (buyerProfile?.default_address_text)
|
|
@@ -66,14 +67,25 @@ export function registerRfqsRoutes(app, deps) {
|
|
|
66
67
|
const id = generateId('rfq');
|
|
67
68
|
const regionRequired = body.region_required ? String(body.region_required) : user.region || null;
|
|
68
69
|
const fulfillmentRequired = body.fulfillment_required ? JSON.stringify(body.fulfillment_required) : null;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
70
|
+
// Codex #236 P1:await 余额预检与同步 tx 间有 yield;扣款带 balance>=deposit 守卫,
|
|
71
|
+
// changes!==1 即并发已花掉余额 → 抛回滚(连带回滚已插入的 rfq),杜绝超额。
|
|
72
|
+
try {
|
|
73
|
+
db.transaction(() => {
|
|
74
|
+
db.prepare(`
|
|
75
|
+
INSERT INTO rfqs (id, buyer_id, listing_id, title, spec_json, qty, category, region_required, urgency,
|
|
76
|
+
max_price, fulfillment_required, award_mode, award_window_min, deadline_at, buyer_stake_locked, notes, shipping_address)
|
|
77
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,datetime('now', '+' || ? || ' minutes'),?,?,?)
|
|
78
|
+
`).run(id, user.id, body.listing_id ? String(body.listing_id) : null, title, body.spec_json ? JSON.stringify(body.spec_json) : null, qty, category, regionRequired, urgency, maxPrice, fulfillmentRequired, awardMode, windowMin, windowMin, deposit, body.notes ? String(body.notes) : null, shippingAddress);
|
|
79
|
+
const d = db.prepare('UPDATE wallets SET balance = balance - ?, staked = staked + ? WHERE user_id = ? AND balance >= ?').run(deposit, deposit, user.id, deposit);
|
|
80
|
+
if (d.changes !== 1)
|
|
81
|
+
throw new Error('RFQ_INSUFFICIENT_BALANCE');
|
|
82
|
+
})();
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
if (e.message === 'RFQ_INSUFFICIENT_BALANCE')
|
|
86
|
+
return void res.json({ error: `余额不足,发求购需 ${deposit} WAZ 押金(中标后释放,撤销扣 30%)` });
|
|
87
|
+
throw e;
|
|
88
|
+
}
|
|
77
89
|
try {
|
|
78
90
|
notifyMatchedSellers(id);
|
|
79
91
|
}
|
|
@@ -90,7 +102,7 @@ export function registerRfqsRoutes(app, deps) {
|
|
|
90
102
|
res.json({ id, deposit, window_min: windowMin, deadline_at_minutes: windowMin, auto_bids: autoBidCount });
|
|
91
103
|
});
|
|
92
104
|
// 卖家 RFQ 看板
|
|
93
|
-
app.get('/api/rfqs', (req, res) => {
|
|
105
|
+
app.get('/api/rfqs', async (req, res) => {
|
|
94
106
|
const user = auth(req, res);
|
|
95
107
|
if (!user)
|
|
96
108
|
return;
|
|
@@ -118,7 +130,7 @@ export function registerRfqsRoutes(app, deps) {
|
|
|
118
130
|
args.push('%' + qE + '%', '%' + qE + '%');
|
|
119
131
|
}
|
|
120
132
|
const limit = Math.min(100, Math.max(1, Number(req.query.limit) || 30));
|
|
121
|
-
const rows =
|
|
133
|
+
const rows = await dbAll(`
|
|
122
134
|
SELECT r.id, r.title, r.qty, r.category, r.region_required, r.urgency, r.max_price,
|
|
123
135
|
r.award_mode, r.deadline_at, r.bid_count, r.created_at,
|
|
124
136
|
(SELECT MIN(price) FROM bids b WHERE b.rfq_id = r.id AND b.status = 'active') as current_lowest_bid,
|
|
@@ -127,32 +139,32 @@ export function registerRfqsRoutes(app, deps) {
|
|
|
127
139
|
WHERE ${where.join(' AND ')}
|
|
128
140
|
ORDER BY r.created_at DESC
|
|
129
141
|
LIMIT ?
|
|
130
|
-
|
|
142
|
+
`, [user.id, ...args, limit]);
|
|
131
143
|
res.json({ items: rows, urgencies: ['now', 'today', 'flex'], categories: LISTING_CATEGORIES });
|
|
132
144
|
});
|
|
133
|
-
app.get('/api/rfqs/mine', (req, res) => {
|
|
145
|
+
app.get('/api/rfqs/mine', async (req, res) => {
|
|
134
146
|
const user = auth(req, res);
|
|
135
147
|
if (!user)
|
|
136
148
|
return;
|
|
137
|
-
const rows =
|
|
149
|
+
const rows = await dbAll(`
|
|
138
150
|
SELECT r.*,
|
|
139
151
|
(SELECT MIN(price) FROM bids b WHERE b.rfq_id = r.id AND b.status = 'active') as current_lowest_bid
|
|
140
152
|
FROM rfqs r
|
|
141
153
|
WHERE r.buyer_id = ?
|
|
142
154
|
ORDER BY r.created_at DESC
|
|
143
155
|
LIMIT 100
|
|
144
|
-
|
|
156
|
+
`, [user.id]);
|
|
145
157
|
res.json({ items: rows });
|
|
146
158
|
});
|
|
147
|
-
app.get('/api/rfqs/:id', (req, res) => {
|
|
159
|
+
app.get('/api/rfqs/:id', async (req, res) => {
|
|
148
160
|
const user = auth(req, res);
|
|
149
161
|
if (!user)
|
|
150
162
|
return;
|
|
151
|
-
const rfq =
|
|
163
|
+
const rfq = await dbOne('SELECT * FROM rfqs WHERE id = ?', [req.params.id]);
|
|
152
164
|
if (!rfq)
|
|
153
165
|
return void res.status(404).json({ error: 'RFQ 不存在' });
|
|
154
166
|
const isOwner = rfq.buyer_id === user.id;
|
|
155
|
-
const bids =
|
|
167
|
+
const bids = await dbAll(`
|
|
156
168
|
SELECT b.id, b.seller_id, b.price, b.qty_offered, b.eta_hours, b.fulfillment_type, b.note,
|
|
157
169
|
b.auto_bid_skill, b.status, b.submitted_at, b.offer_id,
|
|
158
170
|
u.handle as seller_handle, u.region as seller_region,
|
|
@@ -161,7 +173,7 @@ export function registerRfqsRoutes(app, deps) {
|
|
|
161
173
|
LEFT JOIN users u ON u.id = b.seller_id
|
|
162
174
|
WHERE b.rfq_id = ?
|
|
163
175
|
ORDER BY b.price ASC, b.submitted_at ASC
|
|
164
|
-
|
|
176
|
+
`, [req.params.id]);
|
|
165
177
|
// 仅 owner 看全部;第三方只看自己 + 计数
|
|
166
178
|
const visibleBids = isOwner ? bids : bids.filter(b => b.seller_id === user.id);
|
|
167
179
|
// P1:非 owner 时 buyer 身份脱敏(防止私下交易)
|
|
@@ -173,11 +185,11 @@ export function registerRfqsRoutes(app, deps) {
|
|
|
173
185
|
}
|
|
174
186
|
res.json({ rfq: safeRfq, bids: visibleBids, bid_count: bids.length, is_owner: isOwner });
|
|
175
187
|
});
|
|
176
|
-
app.delete('/api/rfqs/:id', (req, res) => {
|
|
188
|
+
app.delete('/api/rfqs/:id', async (req, res) => {
|
|
177
189
|
const user = auth(req, res);
|
|
178
190
|
if (!user)
|
|
179
191
|
return;
|
|
180
|
-
const rfq =
|
|
192
|
+
const rfq = await dbOne('SELECT * FROM rfqs WHERE id = ?', [req.params.id]);
|
|
181
193
|
if (!rfq)
|
|
182
194
|
return void res.status(404).json({ error: 'RFQ 不存在' });
|
|
183
195
|
if (rfq.buyer_id !== user.id)
|
|
@@ -187,26 +199,39 @@ export function registerRfqsRoutes(app, deps) {
|
|
|
187
199
|
const deposit = Number(rfq.buyer_stake_locked) || 0;
|
|
188
200
|
const forfeit = Math.round(deposit * 0.30 * 100) / 100;
|
|
189
201
|
const refund = Math.round((deposit - forfeit) * 100) / 100;
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
+
// Codex #236 P1:tx 内先 CAS RFQ open→cancelled,changes!==1 即并发已取消/中标 → 抛回滚,
|
|
203
|
+
// 先于释放买家/bid stake,杜绝重复释放。
|
|
204
|
+
let releasedCount = 0;
|
|
205
|
+
try {
|
|
206
|
+
db.transaction(() => {
|
|
207
|
+
const c = db.prepare("UPDATE rfqs SET status = 'cancelled', updated_at = datetime('now') WHERE id = ? AND status = 'open'").run(req.params.id);
|
|
208
|
+
if (c.changes !== 1)
|
|
209
|
+
throw new Error('RFQ_NOT_OPEN');
|
|
210
|
+
if (refund > 0)
|
|
211
|
+
db.prepare('UPDATE wallets SET balance = balance + ?, staked = staked - ? WHERE user_id = ?').run(refund, deposit, user.id);
|
|
212
|
+
const activeBids = db.prepare("SELECT id, seller_id, stake_locked FROM bids WHERE rfq_id = ? AND status = 'active'").all(req.params.id);
|
|
213
|
+
for (const b of activeBids) {
|
|
214
|
+
db.prepare("UPDATE bids SET status = 'cancelled', resolved_at = datetime('now') WHERE id = ?").run(b.id);
|
|
215
|
+
if (b.stake_locked > 0)
|
|
216
|
+
db.prepare('UPDATE wallets SET balance = balance + ?, staked = staked - ? WHERE user_id = ?').run(b.stake_locked, b.stake_locked, b.seller_id);
|
|
217
|
+
releasedCount++;
|
|
218
|
+
}
|
|
219
|
+
})();
|
|
220
|
+
}
|
|
221
|
+
catch (e) {
|
|
222
|
+
if (e.message === 'RFQ_NOT_OPEN')
|
|
223
|
+
return void res.json({ error: `当前状态不可取消(可能已取消/中标)` });
|
|
224
|
+
throw e;
|
|
225
|
+
}
|
|
226
|
+
res.json({ success: true, refund, forfeit, active_bids_released: releasedCount });
|
|
202
227
|
});
|
|
203
|
-
app.post('/api/rfqs/:id/bids', (req, res) => {
|
|
228
|
+
app.post('/api/rfqs/:id/bids', async (req, res) => {
|
|
204
229
|
const user = auth(req, res);
|
|
205
230
|
if (!user)
|
|
206
231
|
return;
|
|
207
232
|
if (user.role !== 'seller')
|
|
208
233
|
return void res.json({ error: '仅卖家可报价' });
|
|
209
|
-
const rfq =
|
|
234
|
+
const rfq = await dbOne('SELECT * FROM rfqs WHERE id = ?', [req.params.id]);
|
|
210
235
|
if (!rfq)
|
|
211
236
|
return void res.status(404).json({ error: 'RFQ 不存在' });
|
|
212
237
|
if (rfq.status !== 'open')
|
|
@@ -228,32 +253,48 @@ export function registerRfqsRoutes(app, deps) {
|
|
|
228
253
|
const fulfillmentType = String(body.fulfillment_type || 'standard');
|
|
229
254
|
if (!VALID_FULFILLMENT_TYPES.has(fulfillmentType))
|
|
230
255
|
return void res.json({ error: 'fulfillment_type 无效' });
|
|
231
|
-
const today =
|
|
256
|
+
const today = (await dbOne("SELECT COUNT(1) as n FROM bids WHERE seller_id = ? AND submitted_at > datetime('now','-1 day')", [user.id])).n;
|
|
232
257
|
if (today >= BID_DAILY_CAP_PER_SELLER) {
|
|
233
258
|
return void res.json({ error: `今日报价已达上限 ${BID_DAILY_CAP_PER_SELLER} 条` });
|
|
234
259
|
}
|
|
235
260
|
// 一卖家 × 一 RFQ = 一 bid(已有则用 PATCH)
|
|
236
|
-
const existing =
|
|
261
|
+
const existing = await dbOne("SELECT id, status FROM bids WHERE rfq_id = ? AND seller_id = ?", [req.params.id, user.id]);
|
|
237
262
|
if (existing && existing.status === 'active')
|
|
238
263
|
return void res.json({ error: '已有进行中的 bid,请改用 PATCH 修改', bid_id: existing.id });
|
|
239
264
|
const stake = bidStakeFor(price, qtyOffered);
|
|
240
|
-
const wallet =
|
|
265
|
+
const wallet = await dbOne('SELECT balance FROM wallets WHERE user_id = ?', [user.id]);
|
|
241
266
|
if (!wallet || Number(wallet.balance) < stake) {
|
|
242
267
|
return void res.json({ error: `余额不足,bid 押金 ${stake} WAZ(落选/取消立即释放,中标后转 escrow)` });
|
|
243
268
|
}
|
|
244
269
|
const id = generateId('bid');
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
|
249
|
-
`).run(id, req.params.id, user.id, body.offer_id ? String(body.offer_id) : null, price, qtyOffered, body.eta_hours != null ? Number(body.eta_hours) : null, fulfillmentType, body.note ? String(body.note).slice(0, 500) : null, stake, body.auto_bid_skill ? 1 : 0);
|
|
250
|
-
db.prepare('UPDATE wallets SET balance = balance - ?, staked = staked + ? WHERE user_id = ?').run(stake, stake, user.id);
|
|
251
|
-
db.prepare(`UPDATE rfqs SET bid_count = bid_count + 1, status = 'open', updated_at = datetime('now') WHERE id = ?`).run(req.params.id);
|
|
252
|
-
})();
|
|
270
|
+
// Codex #236 P1:await 预检(rfq open / 余额)与同步 tx 间有 yield。tx 内先原子确认 RFQ 仍 open
|
|
271
|
+
// (WHERE status='open' bump bid_count;原来无条件 SET status='open' 会把已中标/取消的 RFQ 复活,一并修掉),
|
|
272
|
+
// 再插 bid + 带 balance>=stake 守卫扣款;任一失败抛回滚(连带回滚已插 bid),杜绝超额/向已关闭 RFQ 报价。
|
|
253
273
|
try {
|
|
254
|
-
db.
|
|
255
|
-
|
|
256
|
-
|
|
274
|
+
db.transaction(() => {
|
|
275
|
+
const rOpen = db.prepare(`UPDATE rfqs SET bid_count = bid_count + 1, updated_at = datetime('now') WHERE id = ? AND status = 'open'`).run(req.params.id);
|
|
276
|
+
if (rOpen.changes !== 1)
|
|
277
|
+
throw new Error('RFQ_NOT_OPEN');
|
|
278
|
+
db.prepare(`
|
|
279
|
+
INSERT INTO bids (id, rfq_id, seller_id, offer_id, price, qty_offered, eta_hours, fulfillment_type, note, stake_locked, auto_bid_skill)
|
|
280
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
|
281
|
+
`).run(id, req.params.id, user.id, body.offer_id ? String(body.offer_id) : null, price, qtyOffered, body.eta_hours != null ? Number(body.eta_hours) : null, fulfillmentType, body.note ? String(body.note).slice(0, 500) : null, stake, body.auto_bid_skill ? 1 : 0);
|
|
282
|
+
const d = db.prepare('UPDATE wallets SET balance = balance - ?, staked = staked + ? WHERE user_id = ? AND balance >= ?').run(stake, stake, user.id, stake);
|
|
283
|
+
if (d.changes !== 1)
|
|
284
|
+
throw new Error('BID_INSUFFICIENT_BALANCE');
|
|
285
|
+
})();
|
|
286
|
+
}
|
|
287
|
+
catch (e) {
|
|
288
|
+
const m = e.message;
|
|
289
|
+
if (m === 'RFQ_NOT_OPEN')
|
|
290
|
+
return void res.json({ error: `当前状态不接受报价(可能已中标/取消)` });
|
|
291
|
+
if (m === 'BID_INSUFFICIENT_BALANCE')
|
|
292
|
+
return void res.json({ error: `余额不足,bid 押金 ${stake} WAZ(落选/取消立即释放,中标后转 escrow)` });
|
|
293
|
+
throw e;
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
await dbRun(`INSERT INTO notifications (id, user_id, type, title, body, created_at)
|
|
297
|
+
VALUES (?,?,'rfq_bid',?,?,datetime('now'))`, [generateId('ntf'), rfq.buyer_id, `💰 新报价 ${price} WAZ`, `RFQ:${rfq.title} · #${req.params.id}`]);
|
|
257
298
|
}
|
|
258
299
|
catch (e) {
|
|
259
300
|
console.error('[P3 notify bid]', e);
|
|
@@ -277,9 +318,8 @@ export function registerRfqsRoutes(app, deps) {
|
|
|
277
318
|
if (result.ok) {
|
|
278
319
|
autoAwardedOrder = result.order_id;
|
|
279
320
|
try {
|
|
280
|
-
|
|
281
|
-
VALUES (?,?,'rfq_won',?,?,datetime('now'))`)
|
|
282
|
-
.run(generateId('ntf'), user.id, `🎉 中标(first_match 自动选)`, `订单 ${result.order_id}`);
|
|
321
|
+
await dbRun(`INSERT INTO notifications (id, user_id, type, title, body, created_at)
|
|
322
|
+
VALUES (?,?,'rfq_won',?,?,datetime('now'))`, [generateId('ntf'), user.id, `🎉 中标(first_match 自动选)`, `订单 ${result.order_id}`]);
|
|
283
323
|
}
|
|
284
324
|
catch (e) {
|
|
285
325
|
console.error('[P3c notify first_match won]', e);
|
|
@@ -294,18 +334,18 @@ export function registerRfqsRoutes(app, deps) {
|
|
|
294
334
|
res.json({ id, stake_locked: stake, auto_awarded_order_id: autoAwardedOrder });
|
|
295
335
|
});
|
|
296
336
|
// 卖家:修改 bid(仅 active;stake 差额自动结算)
|
|
297
|
-
app.patch('/api/bids/:id', (req, res) => {
|
|
337
|
+
app.patch('/api/bids/:id', async (req, res) => {
|
|
298
338
|
const user = auth(req, res);
|
|
299
339
|
if (!user)
|
|
300
340
|
return;
|
|
301
|
-
const bid =
|
|
341
|
+
const bid = await dbOne('SELECT * FROM bids WHERE id = ?', [req.params.id]);
|
|
302
342
|
if (!bid)
|
|
303
343
|
return void res.status(404).json({ error: 'bid 不存在' });
|
|
304
344
|
if (bid.seller_id !== user.id)
|
|
305
345
|
return void res.status(403).json({ error: '仅本人可修改' });
|
|
306
346
|
if (bid.status !== 'active')
|
|
307
347
|
return void res.json({ error: `当前状态 ${bid.status} 不可修改` });
|
|
308
|
-
const rfq =
|
|
348
|
+
const rfq = await dbOne('SELECT max_price, status, deadline_at FROM rfqs WHERE id = ?', [bid.rfq_id]);
|
|
309
349
|
if (!rfq || rfq.status !== 'open')
|
|
310
350
|
return void res.json({ error: 'RFQ 已不接受改价' });
|
|
311
351
|
const body = req.body;
|
|
@@ -342,50 +382,89 @@ export function registerRfqsRoutes(app, deps) {
|
|
|
342
382
|
const newStake = bidStakeFor(newPrice, newQty);
|
|
343
383
|
const delta = Math.round((newStake - oldStake) * 100) / 100;
|
|
344
384
|
if (delta > 0) {
|
|
345
|
-
const wallet =
|
|
385
|
+
const wallet = await dbOne('SELECT balance FROM wallets WHERE user_id = ?', [user.id]);
|
|
346
386
|
if (!wallet || Number(wallet.balance) < delta) {
|
|
347
387
|
return void res.json({ error: `余额不足补足 stake 差额 ${delta} WAZ` });
|
|
348
388
|
}
|
|
349
389
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
db.prepare('
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
390
|
+
// Codex #236 P1:await 预检后进入同步 tx 前,bid/rfq 状态与 stake 可能变。tx 内重读 bid(必须仍
|
|
391
|
+
// active)+ rfq(必须仍 open),delta 用【tx 内重读的 stake】重算,正 delta 扣款带 balance 守卫。
|
|
392
|
+
let txDelta = delta;
|
|
393
|
+
try {
|
|
394
|
+
db.transaction(() => {
|
|
395
|
+
const freshBid = db.prepare('SELECT status, stake_locked FROM bids WHERE id = ?').get(req.params.id);
|
|
396
|
+
if (!freshBid || freshBid.status !== 'active')
|
|
397
|
+
throw new Error('BID_NOT_ACTIVE');
|
|
398
|
+
const freshRfq = db.prepare('SELECT status FROM rfqs WHERE id = ?').get(bid.rfq_id);
|
|
399
|
+
if (!freshRfq || freshRfq.status !== 'open')
|
|
400
|
+
throw new Error('RFQ_NOT_OPEN');
|
|
401
|
+
txDelta = Math.round((newStake - (Number(freshBid.stake_locked) || 0)) * 100) / 100;
|
|
402
|
+
db.prepare(`UPDATE bids SET price = ?, qty_offered = ?, eta_hours = ?, fulfillment_type = ?, note = ?, stake_locked = ?
|
|
403
|
+
WHERE id = ?`).run(newPrice, newQty, newEta, newFt, newNote, newStake, req.params.id);
|
|
404
|
+
if (txDelta > 0) {
|
|
405
|
+
const d = db.prepare('UPDATE wallets SET balance = balance - ?, staked = staked + ? WHERE user_id = ? AND balance >= ?').run(txDelta, txDelta, user.id, txDelta);
|
|
406
|
+
if (d.changes !== 1)
|
|
407
|
+
throw new Error('PATCH_INSUFFICIENT_BALANCE');
|
|
408
|
+
}
|
|
409
|
+
else if (txDelta < 0) {
|
|
410
|
+
const back = -txDelta;
|
|
411
|
+
db.prepare('UPDATE wallets SET balance = balance + ?, staked = staked - ? WHERE user_id = ?').run(back, back, user.id);
|
|
412
|
+
}
|
|
413
|
+
})();
|
|
414
|
+
}
|
|
415
|
+
catch (e) {
|
|
416
|
+
const m = e.message;
|
|
417
|
+
if (m === 'BID_NOT_ACTIVE')
|
|
418
|
+
return void res.json({ error: 'bid 已不是 active 状态,不可修改' });
|
|
419
|
+
if (m === 'RFQ_NOT_OPEN')
|
|
420
|
+
return void res.json({ error: 'RFQ 已不接受改价' });
|
|
421
|
+
if (m === 'PATCH_INSUFFICIENT_BALANCE')
|
|
422
|
+
return void res.json({ error: `余额不足补足 stake 差额 ${txDelta} WAZ` });
|
|
423
|
+
throw e;
|
|
424
|
+
}
|
|
425
|
+
res.json({ success: true, stake_locked: newStake, stake_delta: txDelta });
|
|
362
426
|
});
|
|
363
427
|
// 卖家:撤回 bid(释放 stake)
|
|
364
|
-
app.delete('/api/bids/:id', (req, res) => {
|
|
428
|
+
app.delete('/api/bids/:id', async (req, res) => {
|
|
365
429
|
const user = auth(req, res);
|
|
366
430
|
if (!user)
|
|
367
431
|
return;
|
|
368
|
-
const bid =
|
|
432
|
+
const bid = await dbOne('SELECT * FROM bids WHERE id = ?', [req.params.id]);
|
|
369
433
|
if (!bid)
|
|
370
434
|
return void res.status(404).json({ error: 'bid 不存在' });
|
|
371
435
|
if (bid.seller_id !== user.id)
|
|
372
436
|
return void res.status(403).json({ error: '仅本人可撤回' });
|
|
373
437
|
if (bid.status !== 'active')
|
|
374
438
|
return void res.json({ error: `当前状态 ${bid.status} 不可撤回` });
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
439
|
+
// Codex #236 P1:tx 内先 CAS bid active→cancelled,changes!==1 即并发已撤回/中标 → 抛回滚,
|
|
440
|
+
// 先于释放 stake + 减 bid_count;释放额用 tx 内重读的 stake_locked(防并发 patch 改过)。
|
|
441
|
+
let releasedStake = 0;
|
|
442
|
+
try {
|
|
443
|
+
db.transaction(() => {
|
|
444
|
+
const c = db.prepare("UPDATE bids SET status = 'cancelled', resolved_at = datetime('now') WHERE id = ? AND status = 'active'").run(req.params.id);
|
|
445
|
+
if (c.changes !== 1)
|
|
446
|
+
throw new Error('BID_NOT_ACTIVE');
|
|
447
|
+
const fresh = db.prepare('SELECT stake_locked FROM bids WHERE id = ?').get(req.params.id);
|
|
448
|
+
releasedStake = Number(fresh.stake_locked) || 0;
|
|
449
|
+
if (releasedStake > 0)
|
|
450
|
+
db.prepare('UPDATE wallets SET balance = balance + ?, staked = staked - ? WHERE user_id = ?').run(releasedStake, releasedStake, user.id);
|
|
451
|
+
db.prepare("UPDATE rfqs SET bid_count = MAX(0, bid_count - 1), updated_at = datetime('now') WHERE id = ?").run(String(bid.rfq_id));
|
|
452
|
+
})();
|
|
453
|
+
}
|
|
454
|
+
catch (e) {
|
|
455
|
+
if (e.message === 'BID_NOT_ACTIVE')
|
|
456
|
+
return void res.json({ error: `当前状态不可撤回(可能已撤回/中标)` });
|
|
457
|
+
throw e;
|
|
458
|
+
}
|
|
459
|
+
res.json({ success: true, stake_released: releasedStake });
|
|
383
460
|
});
|
|
384
461
|
// 买家:选定 winning bid
|
|
385
|
-
app.post('/api/rfqs/:id/award', (req, res) => {
|
|
462
|
+
app.post('/api/rfqs/:id/award', async (req, res) => {
|
|
386
463
|
const user = auth(req, res);
|
|
387
464
|
if (!user)
|
|
388
465
|
return;
|
|
466
|
+
// 选标读保持同步:rfq/winner 直接作为权威 subject 喂进 awardBidAndCreateOrder,
|
|
467
|
+
// 而该函数事务内不再 re-read,async 化会在读→建单事务之间插入 await gap → 破坏原子性。
|
|
389
468
|
const rfq = db.prepare('SELECT * FROM rfqs WHERE id = ?').get(req.params.id);
|
|
390
469
|
if (!rfq)
|
|
391
470
|
return void res.status(404).json({ error: 'RFQ 不存在' });
|
|
@@ -417,9 +496,8 @@ export function registerRfqsRoutes(app, deps) {
|
|
|
417
496
|
}
|
|
418
497
|
// 通知(事务外):中标
|
|
419
498
|
try {
|
|
420
|
-
|
|
421
|
-
VALUES (?,?,'rfq_won',?,?,datetime('now'))`)
|
|
422
|
-
.run(generateId('ntf'), winner.seller_id, `🎉 中标:${rfq.title}`, `订单 ${result.order_id}`);
|
|
499
|
+
await dbRun(`INSERT INTO notifications (id, user_id, type, title, body, created_at)
|
|
500
|
+
VALUES (?,?,'rfq_won',?,?,datetime('now'))`, [generateId('ntf'), winner.seller_id, `🎉 中标:${rfq.title}`, `订单 ${result.order_id}`]);
|
|
423
501
|
}
|
|
424
502
|
catch (e) {
|
|
425
503
|
console.error('[P3 notify won]', e);
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerSearchRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
-
|
|
3
|
+
// db 已走 RFC-016 异步 seam(dbOne/dbAll);applyCouponToOrder 是注入的同步 wrapper(订单金钱路径)
|
|
4
|
+
const { auth, applyCouponToOrder, extractUrlFromText, extractTitleFromText, parsePlatformUrl, searchByExternalLink, detectShareCommandFormat, formatProductForAgent } = deps;
|
|
5
|
+
app.get('/api/coupons/preview', async (req, res) => {
|
|
4
6
|
const user = auth(req, res);
|
|
5
7
|
if (!user)
|
|
6
8
|
return;
|
|
7
9
|
const { code, product_id } = req.query;
|
|
8
10
|
if (!code || !product_id)
|
|
9
11
|
return void res.status(400).json({ error: '需提供 code + product_id' });
|
|
10
|
-
const p =
|
|
12
|
+
const p = await dbOne('SELECT seller_id, price FROM products WHERE id = ?', [product_id]);
|
|
11
13
|
if (!p)
|
|
12
14
|
return void res.status(404).json({ error: '商品不存在' });
|
|
13
15
|
const result = applyCouponToOrder(code, p.seller_id, product_id, Number(p.price));
|
|
@@ -15,11 +17,11 @@ export function registerSearchRoutes(app, deps) {
|
|
|
15
17
|
return void res.json({ ok: false, error: result.error });
|
|
16
18
|
res.json({ ok: true, discount: result.discount, final_price: Math.max(0, Number(p.price) - (result.discount || 0)) });
|
|
17
19
|
});
|
|
18
|
-
app.get('/api/my-products', (req, res) => {
|
|
20
|
+
app.get('/api/my-products', async (req, res) => {
|
|
19
21
|
const user = auth(req, res);
|
|
20
22
|
if (!user)
|
|
21
23
|
return;
|
|
22
|
-
const products =
|
|
24
|
+
const products = await dbAll(`
|
|
23
25
|
SELECT p.*,
|
|
24
26
|
CASE WHEN EXISTS (
|
|
25
27
|
SELECT 1 FROM verify_tasks WHERE product_id=p.id AND status IN ('code_issued','open')
|
|
@@ -28,7 +30,7 @@ export function registerSearchRoutes(app, deps) {
|
|
|
28
30
|
AND NOT EXISTS (SELECT 1 FROM product_external_links WHERE product_id=p.id AND verified=1 AND (revoked IS NULL OR revoked=0))
|
|
29
31
|
THEN 1 ELSE 0 END as all_links_revoked
|
|
30
32
|
FROM products p WHERE p.seller_id = ? ORDER BY p.created_at DESC
|
|
31
|
-
|
|
33
|
+
`, [user.id]);
|
|
32
34
|
res.json(products);
|
|
33
35
|
});
|
|
34
36
|
app.post('/api/search-by-link', (req, res) => {
|
|
@@ -83,7 +85,7 @@ export function registerSearchRoutes(app, deps) {
|
|
|
83
85
|
...(unsupportedHint ? { unsupported_format: true, hint: unsupportedHint } : {}),
|
|
84
86
|
});
|
|
85
87
|
});
|
|
86
|
-
app.get('/api/search-fuzzy', (req, res) => {
|
|
88
|
+
app.get('/api/search-fuzzy', async (req, res) => {
|
|
87
89
|
const q = String(req.query.q ?? '').trim();
|
|
88
90
|
const threshold = 0.5;
|
|
89
91
|
if (!q)
|
|
@@ -103,14 +105,14 @@ export function registerSearchRoutes(app, deps) {
|
|
|
103
105
|
return out;
|
|
104
106
|
};
|
|
105
107
|
const qg = grams(qn);
|
|
106
|
-
const rows =
|
|
108
|
+
const rows = await dbAll(`
|
|
107
109
|
SELECT p.*, u.name as seller_name,
|
|
108
110
|
COALESCE(rs.total_points, 0) as rep_points, COALESCE(rs.level, 'new') as rep_level
|
|
109
111
|
FROM products p
|
|
110
112
|
JOIN users u ON p.seller_id = u.id
|
|
111
113
|
LEFT JOIN reputation_scores rs ON rs.user_id = p.seller_id
|
|
112
114
|
WHERE p.status = 'active' AND p.stock > 0
|
|
113
|
-
`)
|
|
115
|
+
`);
|
|
114
116
|
const scored = rows
|
|
115
117
|
.map((r) => {
|
|
116
118
|
const tn = norm(String(r.title ?? ''));
|
|
@@ -144,26 +146,26 @@ export function registerSearchRoutes(app, deps) {
|
|
|
144
146
|
score_threshold: threshold,
|
|
145
147
|
});
|
|
146
148
|
});
|
|
147
|
-
app.get('/api/check-url', (req, res) => {
|
|
149
|
+
app.get('/api/check-url', async (req, res) => {
|
|
148
150
|
const user = auth(req, res);
|
|
149
151
|
if (!user)
|
|
150
152
|
return;
|
|
151
153
|
const url = req.query.url;
|
|
152
154
|
if (!url)
|
|
153
155
|
return void res.json({ error: '请提供 url 参数' });
|
|
154
|
-
const selfClaim =
|
|
156
|
+
const selfClaim = await dbOne(`
|
|
155
157
|
SELECT p.id as product_id, p.title FROM product_external_links pel
|
|
156
158
|
JOIN products p ON pel.product_id = p.id
|
|
157
159
|
WHERE pel.url = ? AND p.seller_id = ?
|
|
158
|
-
|
|
160
|
+
`, [url, user.id]);
|
|
159
161
|
if (selfClaim) {
|
|
160
162
|
return void res.json({ claimed: true, self: true, product_title: selfClaim.title, message: `您已在商品「${selfClaim.title}」中关联了此链接` });
|
|
161
163
|
}
|
|
162
|
-
const otherClaim =
|
|
164
|
+
const otherClaim = await dbOne(`
|
|
163
165
|
SELECT p.title as product_title FROM product_external_links pel
|
|
164
166
|
JOIN products p ON pel.product_id = p.id
|
|
165
167
|
WHERE pel.url = ? AND pel.verified = 1 AND p.seller_id != ?
|
|
166
|
-
|
|
168
|
+
`, [url, user.id]);
|
|
167
169
|
if (otherClaim) {
|
|
168
170
|
return void res.json({ claimed: true, self: false, message: `此链接已被其他商家认领,不能直接添加,上架后请在商品编辑页发起认领验证任务` });
|
|
169
171
|
}
|