@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
|
@@ -2,8 +2,55 @@ import { buildCartMandate, buildPaymentMandate, signMandate } from './ap2-mandat
|
|
|
2
2
|
// RFC-014 PR3 — 金额走整数 base-units;钱包写绝对值(防 REAL 浮点加法 dust)。
|
|
3
3
|
import { toUnits, toDecimal, mulQty, mulRate } from '../../money.js';
|
|
4
4
|
import { applyWalletDelta } from '../../ledger.js';
|
|
5
|
+
import { dbOne } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam(仅下单事务外的预检查;事务内 escrow/INSERT 保持同步)
|
|
6
|
+
// 店铺推荐 → 商品三级归因的【懒升级】(sync,跑在下单事务内、getProductShareChain 之前)。
|
|
7
|
+
// 严格门槛(任一不满足则不升级,绝不覆盖已有有效直接归因):
|
|
8
|
+
// ① 无未过期的现有商品归因(direct share 优先,不被店铺推荐覆盖)
|
|
9
|
+
// ② 该 seller 下有未过期的 shop_referral_attribution(recipient=buyer)
|
|
10
|
+
// ③ referrer ≠ buyer、referrer ≠ seller、referrer 非 sys/internal
|
|
11
|
+
// ④ referrer rewards_opted_in=1 且通过 isAllowedSponsor(经济边界)
|
|
12
|
+
// ⑤ referrer 自己 completed 买过【同一个】商品,且该订单的【完成时间 ≤ 店铺推荐锚定时间】——
|
|
13
|
+
// 必须"先真实成交同款、后分享店铺",不允许先 touch 旧店铺锚点、事后补购同款再反向升级。
|
|
14
|
+
// 完成时间取 order_state_history 中 to_status='completed' 的 MIN(created_at);无 history 行时
|
|
15
|
+
// 兼容回退 orders.updated_at(绝不用 orders.created_at 当完成时间)。
|
|
16
|
+
// 通过后写 product_share_attribution(shareable_id=NULL,带 shop_referral_verified_purchase provenance);
|
|
17
|
+
// 已有但过期的行可被刷新。不改任何结算数学,只新增一条归因来源。模块级导出供测试直测。
|
|
18
|
+
export function maybePromoteShopReferralToProductAttribution(db, opts, productId, sellerId, buyerId) {
|
|
19
|
+
const liveDirect = db.prepare("SELECT 1 FROM product_share_attribution WHERE product_id = ? AND recipient_id = ? AND expires_at > datetime('now')").get(productId, buyerId);
|
|
20
|
+
if (liveDirect)
|
|
21
|
+
return; // ① 已有有效归因(含直接分享)→ 绝不覆盖
|
|
22
|
+
const referral = db.prepare("SELECT referrer_id, ref_code, created_at FROM shop_referral_attribution WHERE seller_id = ? AND recipient_id = ? AND expires_at > datetime('now')").get(sellerId, buyerId);
|
|
23
|
+
if (!referral)
|
|
24
|
+
return; // ②
|
|
25
|
+
const r = referral.referrer_id;
|
|
26
|
+
if (r === buyerId || r === sellerId || r === 'sys_protocol' || r === opts.internalAuditorId)
|
|
27
|
+
return; // ③ (referrer===seller → 记录关系但不升级分润)
|
|
28
|
+
const optedIn = db.prepare("SELECT rewards_opted_in FROM users WHERE id = ?").get(r)?.rewards_opted_in === 1;
|
|
29
|
+
if (!optedIn || !opts.isAllowedSponsor(r))
|
|
30
|
+
return; // ④
|
|
31
|
+
const qual = db.prepare(`
|
|
32
|
+
SELECT o.id FROM orders o
|
|
33
|
+
WHERE o.buyer_id = ? AND o.product_id = ? AND o.status = 'completed'
|
|
34
|
+
AND COALESCE(
|
|
35
|
+
(SELECT MIN(h.created_at) FROM order_state_history h WHERE h.order_id = o.id AND h.to_status = 'completed'),
|
|
36
|
+
o.updated_at
|
|
37
|
+
) <= ?
|
|
38
|
+
ORDER BY o.created_at ASC LIMIT 1
|
|
39
|
+
`).get(r, productId, referral.created_at);
|
|
40
|
+
if (!qual)
|
|
41
|
+
return; // ⑤ 推荐人没在【锚定店铺之前】真实成交过同款 → 不升级
|
|
42
|
+
const hadRow = db.prepare("SELECT 1 FROM product_share_attribution WHERE product_id = ? AND recipient_id = ?").get(productId, buyerId);
|
|
43
|
+
if (hadRow) {
|
|
44
|
+
db.prepare("UPDATE product_share_attribution SET sharer_id = ?, shareable_id = NULL, created_at = datetime('now'), expires_at = datetime('now','+30 days'), source_type = 'shop_referral_verified_purchase', source_ref = ?, source_shop_seller_id = ?, source_qualified_order_id = ? WHERE product_id = ? AND recipient_id = ?")
|
|
45
|
+
.run(r, referral.ref_code, sellerId, qual.id, productId, buyerId);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
db.prepare("INSERT INTO product_share_attribution (product_id, recipient_id, sharer_id, shareable_id, expires_at, source_type, source_ref, source_shop_seller_id, source_qualified_order_id) VALUES (?,?,?,NULL,datetime('now','+30 days'),'shop_referral_verified_purchase',?,?,?)")
|
|
49
|
+
.run(productId, buyerId, r, referral.ref_code, sellerId, qual.id);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
5
52
|
export function registerOrdersCreateRoutes(app, deps) {
|
|
6
|
-
const { db, auth, isTrustedRole, generateId, generateRecipientCode, DONATION_VALID_PCTS, INTERNAL_AUDITOR_ID, addHours, getActiveFlashSale, applyCouponToOrder, getProtocolParam, getProductShareChain, isAllowedSponsor, checkStockAndMaybeDelist, auditSponsorChainCross, appendOrderEvent, transition, notifyTransition, shouldAutoAccept, ensureCharityRep, broadcastSystemEvent, signPassport, issuerAddress } = deps;
|
|
53
|
+
const { db, auth, isTrustedRole, generateId, generateRecipientCode, DONATION_VALID_PCTS, INTERNAL_AUDITOR_ID, addHours, getActiveFlashSale, applyCouponToOrder, getProtocolParam, getProductShareChain, isAllowedSponsor, resolveInviteCodeRef, checkStockAndMaybeDelist, auditSponsorChainCross, appendOrderEvent, transition, notifyTransition, shouldAutoAccept, ensureCharityRep, broadcastSystemEvent, signPassport, issuerAddress } = deps;
|
|
7
54
|
app.post('/api/orders', async (req, res) => {
|
|
8
55
|
const user = auth(req, res);
|
|
9
56
|
if (!user)
|
|
@@ -16,8 +63,8 @@ export function registerOrdersCreateRoutes(app, deps) {
|
|
|
16
63
|
// 2026-05-23 P0 audit fix 2.1:agent_attestations spend_cap 强制
|
|
17
64
|
const apiKey = req.headers.authorization?.replace('Bearer ', '');
|
|
18
65
|
if (apiKey) {
|
|
19
|
-
const cap =
|
|
20
|
-
WHERE api_key = ? AND user_id = ? AND revoked_at IS NULL
|
|
66
|
+
const cap = await dbOne(`SELECT spend_cap_per_order, spend_cap_daily FROM agent_attestations
|
|
67
|
+
WHERE api_key = ? AND user_id = ? AND revoked_at IS NULL`, [apiKey, user.id]);
|
|
21
68
|
if (cap) {
|
|
22
69
|
const estQty = Math.max(1, Math.floor(Number(req.body?.quantity ?? 1)));
|
|
23
70
|
const estPrice = Number(req.body?.expected_price ?? 0);
|
|
@@ -30,9 +77,8 @@ export function registerOrdersCreateRoutes(app, deps) {
|
|
|
30
77
|
});
|
|
31
78
|
}
|
|
32
79
|
if (cap.spend_cap_daily != null) {
|
|
33
|
-
const todaySpent =
|
|
34
|
-
FROM orders WHERE buyer_id = ? AND created_at > datetime('now', '-24 hours') AND status != 'cancelled'
|
|
35
|
-
.get(user.id).t;
|
|
80
|
+
const todaySpent = (await dbOne(`SELECT COALESCE(SUM(total_amount), 0) as t
|
|
81
|
+
FROM orders WHERE buyer_id = ? AND created_at > datetime('now', '-24 hours') AND status != 'cancelled'`, [user.id])).t;
|
|
36
82
|
if (todaySpent + estTotal > cap.spend_cap_daily) {
|
|
37
83
|
return void res.status(403).json({
|
|
38
84
|
error: `24h 累计 ${todaySpent}+${estTotal} 超 agent 日上限 ${cap.spend_cap_daily} WAZ(用户设定)`,
|
|
@@ -93,17 +139,16 @@ export function registerOrdersCreateRoutes(app, deps) {
|
|
|
93
139
|
deliveryWindowJson = JSON.stringify({ day_type: dt, time_range: tr, flexible: fl });
|
|
94
140
|
}
|
|
95
141
|
}
|
|
96
|
-
const product =
|
|
97
|
-
JOIN users u ON p.seller_id = u.id WHERE p.id = ? AND p.status = 'active'
|
|
142
|
+
const product = await dbOne(`SELECT p.*, u.id as seller_uid FROM products p
|
|
143
|
+
JOIN users u ON p.seller_id = u.id WHERE p.id = ? AND p.status = 'active'`, [product_id]);
|
|
98
144
|
if (!product)
|
|
99
145
|
return void res.json({ error: '商品不存在或已下架' });
|
|
100
146
|
let variant = null;
|
|
101
147
|
if (Number(product.has_variants) === 1) {
|
|
102
148
|
if (!variant_id)
|
|
103
149
|
return void res.json({ error: '该商品需选择规格', error_code: 'VARIANT_REQUIRED' });
|
|
104
|
-
const v =
|
|
105
|
-
FROM product_variants WHERE id = ? AND product_id = ? AND is_active = 1
|
|
106
|
-
.get(variant_id, product_id);
|
|
150
|
+
const v = await dbOne(`SELECT id, price_override, stock, options_json
|
|
151
|
+
FROM product_variants WHERE id = ? AND product_id = ? AND is_active = 1`, [variant_id, product_id]);
|
|
107
152
|
if (!v)
|
|
108
153
|
return void res.json({ error: '规格不存在或已下架' });
|
|
109
154
|
if (Number(v.stock) < reqQty)
|
|
@@ -151,6 +196,9 @@ export function registerOrdersCreateRoutes(app, deps) {
|
|
|
151
196
|
couponDiscount = result.discount || 0;
|
|
152
197
|
}
|
|
153
198
|
// 验证 session_token(如果提供)
|
|
199
|
+
// RFC-016: 价格锁是【一次性】消费 — SELECT 校验 → mark used 之间【不能有 await】,
|
|
200
|
+
// 否则两个并发下单会都读到 used_at=NULL 再各自 mark,复用同一 token(Codex #224)。
|
|
201
|
+
// 故整块保持同步 better-sqlite3 调用(Node 单线程内原子,无让步);Phase 3 随订单路径迁 pg 行锁。
|
|
154
202
|
if (session_token) {
|
|
155
203
|
const session = db.prepare(`
|
|
156
204
|
SELECT * FROM price_sessions WHERE token = ? AND product_id = ? AND user_id = ?
|
|
@@ -188,7 +236,8 @@ export function registerOrdersCreateRoutes(app, deps) {
|
|
|
188
236
|
const insurancePremium = toDecimal(insurancePremiumU);
|
|
189
237
|
const totalAmount = toDecimal(totalAmountU);
|
|
190
238
|
const donationAmount = toDecimal(donationAmountU);
|
|
191
|
-
|
|
239
|
+
// 友好预检查(读):真正的守恒在下面的同步事务内(applyWalletDelta 绝对值落库)。
|
|
240
|
+
const wallet = await dbOne('SELECT balance FROM wallets WHERE user_id = ?', [user.id]);
|
|
192
241
|
if (!wallet)
|
|
193
242
|
return void res.status(500).json({ error: '钱包记录缺失', error_code: 'WALLET_MISSING' });
|
|
194
243
|
if (toUnits(wallet.balance) < totalAmountU + donationAmountU)
|
|
@@ -203,10 +252,13 @@ export function registerOrdersCreateRoutes(app, deps) {
|
|
|
203
252
|
const buyer = db.prepare("SELECT sponsor_id, sponsor_path, region FROM users WHERE id = ?").get(user.id);
|
|
204
253
|
// 孤儿用户首次绑 sponsor:buyer 无 sponsor + 客户端传 sponsor_hint
|
|
205
254
|
// 校验:① 非自己 ② 防环路 ③ hint 必须是 verified buyer
|
|
206
|
-
|
|
207
|
-
|
|
255
|
+
// sponsor_hint from the client is now an invite code (permanent_code [+ -L/-R]) — resolve it to a
|
|
256
|
+
// user id first; usr_xxx / @handle / handle no longer bind a sponsor (matches the narrowed surface).
|
|
257
|
+
const sponsorHintRaw = (typeof req.body.sponsor_hint === 'string' && req.body.sponsor_hint) ? String(req.body.sponsor_hint) : null;
|
|
258
|
+
const sponsorHintRef = sponsorHintRaw ? resolveInviteCodeRef(sponsorHintRaw) : null;
|
|
259
|
+
if (!buyer.sponsor_id && sponsorHintRef && sponsorHintRef.userId !== user.id) {
|
|
208
260
|
const hint = db.prepare("SELECT id, sponsor_path FROM users WHERE id = ? AND id NOT IN ('sys_protocol', ?)")
|
|
209
|
-
.get(
|
|
261
|
+
.get(sponsorHintRef.userId, INTERNAL_AUDITOR_ID);
|
|
210
262
|
if (hint && isAllowedSponsor(hint.id)) {
|
|
211
263
|
const hintPath = hint.sponsor_path || '';
|
|
212
264
|
if (!hintPath.split('>').includes(user.id)) {
|
|
@@ -222,6 +274,9 @@ export function registerOrdersCreateRoutes(app, deps) {
|
|
|
222
274
|
const r = db.prepare("SELECT max_levels FROM region_config WHERE region = ? AND active = 1").get(buyer?.region || 'global');
|
|
223
275
|
return r?.max_levels ?? 3;
|
|
224
276
|
})();
|
|
277
|
+
// 店铺推荐懒升级:在反推商品链【之前】尝试把"店铺推荐 + 推荐人真实成交过同款"升级为本商品归因,
|
|
278
|
+
// 这样本订单快照就能拿到 L1/L2/L3(不覆盖已有有效直接归因)。
|
|
279
|
+
maybePromoteShopReferralToProductAttribution(db, { internalAuditorId: INTERNAL_AUDITOR_ID, isAllowedSponsor }, product.id, product.seller_uid, user.id);
|
|
225
280
|
// 商品分享奖励链(per-product),与 PV 系统 sponsor_path 完全解耦
|
|
226
281
|
// 反推方向:谁分享了 product 给 buyer? → 该 sharer 是 L1
|
|
227
282
|
const productChain = getProductShareChain(product.id, user.id, 3);
|
|
@@ -233,11 +288,11 @@ export function registerOrdersCreateRoutes(app, deps) {
|
|
|
233
288
|
const buyerRegionSnapshot = buyer?.region || 'global';
|
|
234
289
|
// P2P:若为 P2P 商品,下单时快照 content_hash(争议时凭买家所见 hash 判定)
|
|
235
290
|
const contentHashSnapshot = (Number(product.p2p_mode) === 1 && product.content_hash) ? String(product.content_hash) : null;
|
|
236
|
-
// RFC-008 stage 1
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
291
|
+
// RFC-008 stage 1:赔付背书快照恒为 0 → 违约只退款不没收、不印钱、零门槛(起步免赔付)。
|
|
292
|
+
// stake-required 模式(require_seller_stake=1)【尚未实现】且该 param 已被 governance 锁在 0(max=0,
|
|
293
|
+
// 见 server.ts DEFAULT_PARAMS + 迁移,Codex #111)—— 故不读它(读了也只会是 0),避免"假开关"误导。
|
|
294
|
+
// Phase 3 钱路径迁移时在此按信誉算 backing = total×stake_rate 并原子锁 balance→staked、放开该 param。
|
|
295
|
+
const stakeBacking = 0;
|
|
241
296
|
db.prepare(`INSERT INTO orders (
|
|
242
297
|
id, product_id, buyer_id, seller_id, quantity, unit_price, total_amount, escrow_amount,
|
|
243
298
|
status, shipping_address, notes, pay_deadline, accept_deadline, ship_deadline,
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// RFC-011 §⑥ 事件游标流(纯 db 函数,party-gated)
|
|
2
2
|
import { listOrderEventsSince } from '../../layer0-foundation/L0-2-state-machine/order-chain.js';
|
|
3
|
+
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
3
4
|
export function registerOrdersReadRoutes(app, deps) {
|
|
4
5
|
const { db, auth, getOrderStatus, getOrderChain, verifyOrderChain, getOrderDispute } = deps;
|
|
5
|
-
app.get('/api/orders', (req, res) => {
|
|
6
|
+
app.get('/api/orders', async (req, res) => {
|
|
6
7
|
const user = auth(req, res);
|
|
7
8
|
if (!user)
|
|
8
9
|
return;
|
|
9
|
-
const orders =
|
|
10
|
+
const orders = await dbAll(`
|
|
10
11
|
SELECT o.*, p.title as product_title, p.images,
|
|
11
12
|
ub.name as buyer_name, us.name as seller_name
|
|
12
13
|
FROM orders o
|
|
@@ -15,7 +16,7 @@ export function registerOrdersReadRoutes(app, deps) {
|
|
|
15
16
|
JOIN users us ON o.seller_id = us.id
|
|
16
17
|
WHERE o.buyer_id = ? OR o.seller_id = ? OR o.logistics_id = ?
|
|
17
18
|
ORDER BY o.created_at DESC LIMIT 50
|
|
18
|
-
|
|
19
|
+
`, [user.id, user.id, user.id]);
|
|
19
20
|
// B2 隐私购物:列表里也做相同 mask(防 seller/logistics 通过列表绕过详情 mask)
|
|
20
21
|
for (const o of orders) {
|
|
21
22
|
if (Number(o.anonymous_recipient) === 1 && o.buyer_id !== user.id) {
|
|
@@ -28,7 +29,7 @@ export function registerOrdersReadRoutes(app, deps) {
|
|
|
28
29
|
res.json(orders);
|
|
29
30
|
});
|
|
30
31
|
// Wave D-2: 订单导出 CSV
|
|
31
|
-
app.get('/api/orders/export', (req, res) => {
|
|
32
|
+
app.get('/api/orders/export', async (req, res) => {
|
|
32
33
|
const user = auth(req, res);
|
|
33
34
|
if (!user)
|
|
34
35
|
return;
|
|
@@ -47,7 +48,7 @@ export function registerOrdersReadRoutes(app, deps) {
|
|
|
47
48
|
params.push(to);
|
|
48
49
|
}
|
|
49
50
|
const EXPORT_LIMIT = 5000;
|
|
50
|
-
const rows =
|
|
51
|
+
const rows = await dbAll(`
|
|
51
52
|
SELECT o.id, o.created_at, o.status, o.quantity, o.unit_price, o.total_amount,
|
|
52
53
|
o.coupon_discount, o.variant_options_snapshot, o.shipping_address,
|
|
53
54
|
p.title as product_title, p.category,
|
|
@@ -59,7 +60,7 @@ export function registerOrdersReadRoutes(app, deps) {
|
|
|
59
60
|
JOIN users us ON us.id = o.seller_id
|
|
60
61
|
WHERE ${where.join(' AND ')}
|
|
61
62
|
ORDER BY o.created_at DESC LIMIT ?
|
|
62
|
-
|
|
63
|
+
`, [...params, EXPORT_LIMIT + 1]);
|
|
63
64
|
// P1-4: 触达上限 → X-Truncated 头
|
|
64
65
|
const truncated = rows.length > EXPORT_LIMIT;
|
|
65
66
|
if (truncated)
|
|
@@ -99,38 +100,38 @@ export function registerOrdersReadRoutes(app, deps) {
|
|
|
99
100
|
res.send('' + lines.join('\n'));
|
|
100
101
|
});
|
|
101
102
|
// 订单签名链 — 当事人 + arbitrator + admin 可查
|
|
102
|
-
app.get('/api/orders/:id/chain', (req, res) => {
|
|
103
|
+
app.get('/api/orders/:id/chain', async (req, res) => {
|
|
103
104
|
const user = auth(req, res);
|
|
104
105
|
if (!user)
|
|
105
106
|
return;
|
|
106
|
-
const order =
|
|
107
|
+
const order = await dbOne('SELECT buyer_id, seller_id, logistics_id FROM orders WHERE id = ?', [req.params.id]);
|
|
107
108
|
if (!order)
|
|
108
109
|
return void res.status(404).json({ error: '订单不存在' });
|
|
109
110
|
const uid = user.id;
|
|
110
111
|
const isParty = uid === order.buyer_id || uid === order.seller_id || uid === order.logistics_id || user.role === 'arbitrator' || user.role === 'admin';
|
|
111
112
|
if (!isParty)
|
|
112
113
|
return void res.status(403).json({ error: '无权查看此订单链' });
|
|
113
|
-
const chain = getOrderChain(db, req.params.id);
|
|
114
|
-
const verification = verifyOrderChain(db, req.params.id);
|
|
114
|
+
const chain = await getOrderChain(db, req.params.id);
|
|
115
|
+
const verification = await verifyOrderChain(db, req.params.id);
|
|
115
116
|
res.json({ chain, verification });
|
|
116
117
|
});
|
|
117
118
|
// RFC-011 §⑥:事件游标流 —— 集成方 agent 拉"自 cursor 以来与我相关的订单变化"(agent 拉,非 webhook)。
|
|
118
119
|
// party-gated(只见自己当事的订单事件,= /chain 同口径,不变量 2:活性 ≤ 读边界);
|
|
119
120
|
// 结构性事件 + 哈希链字段(验链防篡改),完整 payload 仍走 party-gated /chain。
|
|
120
|
-
app.get('/api/agent/events', (req, res) => {
|
|
121
|
+
app.get('/api/agent/events', async (req, res) => {
|
|
121
122
|
const user = auth(req, res);
|
|
122
123
|
if (!user)
|
|
123
124
|
return;
|
|
124
125
|
const since = typeof req.query.since === 'string' ? req.query.since : undefined;
|
|
125
126
|
const limit = Number(req.query.limit) || 50;
|
|
126
|
-
const r = listOrderEventsSince(db, user.id, since, limit);
|
|
127
|
+
const r = await listOrderEventsSince(db, user.id, since, limit);
|
|
127
128
|
res.setHeader('Cache-Control', 'no-store'); // 事件流不缓存
|
|
128
129
|
res.json({
|
|
129
130
|
...r,
|
|
130
131
|
note: 'Cursor stream of order events you are party to. Pass ?since=<next_cursor> to page. Pull, not push. event_hash+prev_event_hash verify chain integrity; full payload via GET /api/orders/:id/chain.',
|
|
131
132
|
});
|
|
132
133
|
});
|
|
133
|
-
app.get('/api/orders/:id', (req, res) => {
|
|
134
|
+
app.get('/api/orders/:id', async (req, res) => {
|
|
134
135
|
const user = auth(req, res);
|
|
135
136
|
if (!user)
|
|
136
137
|
return;
|
|
@@ -145,8 +146,8 @@ export function registerOrdersReadRoutes(app, deps) {
|
|
|
145
146
|
}
|
|
146
147
|
// M8: 二手订单从 secondhand_items 查;商家订单从 products 查
|
|
147
148
|
const product = order.source === 'secondhand'
|
|
148
|
-
? (() => {
|
|
149
|
-
const si =
|
|
149
|
+
? await (async () => {
|
|
150
|
+
const si = await dbOne('SELECT title, price, images FROM secondhand_items WHERE id = ?', [order.product_id]);
|
|
150
151
|
if (!si)
|
|
151
152
|
return null;
|
|
152
153
|
try {
|
|
@@ -156,10 +157,10 @@ export function registerOrdersReadRoutes(app, deps) {
|
|
|
156
157
|
return { title: si.title, price: si.price, images: [] };
|
|
157
158
|
}
|
|
158
159
|
})()
|
|
159
|
-
:
|
|
160
|
-
const dispute = getOrderDispute(db, req.params.id);
|
|
160
|
+
: await dbOne('SELECT id, title, price, images, return_days FROM products WHERE id = ?', [order.product_id]);
|
|
161
|
+
const dispute = await getOrderDispute(db, req.params.id);
|
|
161
162
|
// 为每条历史记录附上证据描述
|
|
162
|
-
const history = statusInfo.history.map(h => {
|
|
163
|
+
const history = await Promise.all(statusInfo.history.map(async (h) => {
|
|
163
164
|
// P1 fix: 单条脏 evidence_ids 不应封死整个 order 详情
|
|
164
165
|
let ids = [];
|
|
165
166
|
try {
|
|
@@ -169,10 +170,10 @@ export function registerOrdersReadRoutes(app, deps) {
|
|
|
169
170
|
}
|
|
170
171
|
catch { }
|
|
171
172
|
const evidenceItems = ids.length
|
|
172
|
-
?
|
|
173
|
+
? await dbAll(`SELECT description, type FROM evidence WHERE id IN (${ids.map(() => '?').join(',')})`, ids)
|
|
173
174
|
: [];
|
|
174
175
|
return { ...h, evidence_items: evidenceItems };
|
|
175
|
-
});
|
|
176
|
+
}));
|
|
176
177
|
// 物流跟踪摘要:从历史中提取所有物流操作的证据
|
|
177
178
|
const LOGISTICS_STEPS = ['shipped', 'picked_up', 'in_transit', 'delivered'];
|
|
178
179
|
const trackingInfo = history
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerP2pProductsRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
+
// db 已走 RFC-016 异步 seam(dbOne/dbAll/dbRun),不再直接用 deps.db
|
|
4
|
+
const { auth, generateId, verifyP2pSig, isValidPeerEndpoint, isFreshSignedAt, P2P_TITLE_MAX, P2P_THUMB_MAX, P2P_DAILY_CAP, RFQ_MAX_PRICE, RFQ_MAX_QTY } = deps;
|
|
3
5
|
// 发布 / 重发 P2P 商品
|
|
4
|
-
app.post('/api/p2p-products', (req, res) => {
|
|
6
|
+
app.post('/api/p2p-products', async (req, res) => {
|
|
5
7
|
const user = auth(req, res);
|
|
6
8
|
if (!user)
|
|
7
9
|
return;
|
|
@@ -40,26 +42,36 @@ export function registerP2pProductsRoutes(app, deps) {
|
|
|
40
42
|
if (thumbnail && thumbnail.length > P2P_THUMB_MAX)
|
|
41
43
|
return void res.json({ error: `thumbnail 超过 ${P2P_THUMB_MAX} 字节` });
|
|
42
44
|
// 频率限制
|
|
43
|
-
const today =
|
|
45
|
+
const today = (await dbOne("SELECT COUNT(1) as n FROM products WHERE seller_id = ? AND p2p_mode = 1 AND created_at > datetime('now','-1 day')", [user.id])).n;
|
|
44
46
|
if (today >= P2P_DAILY_CAP)
|
|
45
47
|
return void res.json({ error: `今日 P2P 上架已达上限 ${P2P_DAILY_CAP}` });
|
|
46
48
|
const category = String(body.category || 'general');
|
|
47
49
|
const region = String(body.region || user.region || '全国');
|
|
48
50
|
const id = generateId('p');
|
|
49
|
-
|
|
51
|
+
await dbRun(`
|
|
50
52
|
INSERT INTO products (id, seller_id, title, description, price, stock, status, images, ship_regions,
|
|
51
53
|
handling_hours, commission_rate, category_id, stake_amount, p2p_mode, content_hash, peer_endpoint,
|
|
52
54
|
content_signature, content_signed_at)
|
|
53
55
|
VALUES (?,?,?,?,?,?,'active',?,?,24,0.10,'cat_default',0,1,?,?,?,?)
|
|
54
|
-
|
|
56
|
+
`, [
|
|
57
|
+
id, user.id, title,
|
|
58
|
+
`[P2P] ${title}(完整详情见卖家节点)`,
|
|
59
|
+
price, stock,
|
|
60
|
+
thumbnail ? JSON.stringify([thumbnail]) : '[]',
|
|
61
|
+
region,
|
|
62
|
+
contentHash,
|
|
63
|
+
peerEndpoint || null,
|
|
64
|
+
signature,
|
|
65
|
+
signedAt,
|
|
66
|
+
]);
|
|
55
67
|
res.json({ id, content_hash: contentHash });
|
|
56
68
|
});
|
|
57
69
|
// 更新(重发 hash + signature,价格/库存/标题可改;旧 hash 给在途订单保留)
|
|
58
|
-
app.patch('/api/p2p-products/:id', (req, res) => {
|
|
70
|
+
app.patch('/api/p2p-products/:id', async (req, res) => {
|
|
59
71
|
const user = auth(req, res);
|
|
60
72
|
if (!user)
|
|
61
73
|
return;
|
|
62
|
-
const product =
|
|
74
|
+
const product = await dbOne("SELECT * FROM products WHERE id = ? AND p2p_mode = 1", [req.params.id]);
|
|
63
75
|
if (!product)
|
|
64
76
|
return void res.status(404).json({ error: 'P2P 商品不存在' });
|
|
65
77
|
if (product.seller_id !== user.id)
|
|
@@ -128,28 +140,28 @@ export function registerP2pProductsRoutes(app, deps) {
|
|
|
128
140
|
return void res.json({ error: '无任何修改' });
|
|
129
141
|
updates.push("updated_at = datetime('now')");
|
|
130
142
|
args.push(req.params.id);
|
|
131
|
-
|
|
143
|
+
await dbRun(`UPDATE products SET ${updates.join(', ')} WHERE id = ?`, args);
|
|
132
144
|
res.json({ success: true });
|
|
133
145
|
});
|
|
134
146
|
// 下架(保留行 + status='warehouse',在途订单 hash 仍可证)
|
|
135
|
-
app.delete('/api/p2p-products/:id', (req, res) => {
|
|
147
|
+
app.delete('/api/p2p-products/:id', async (req, res) => {
|
|
136
148
|
const user = auth(req, res);
|
|
137
149
|
if (!user)
|
|
138
150
|
return;
|
|
139
|
-
const product =
|
|
151
|
+
const product = await dbOne("SELECT seller_id, status FROM products WHERE id = ? AND p2p_mode = 1", [req.params.id]);
|
|
140
152
|
if (!product)
|
|
141
153
|
return void res.status(404).json({ error: 'P2P 商品不存在' });
|
|
142
154
|
if (product.seller_id !== user.id)
|
|
143
155
|
return void res.status(403).json({ error: '仅卖家本人可下架' });
|
|
144
|
-
const pendingOrders =
|
|
156
|
+
const pendingOrders = (await dbOne("SELECT COUNT(1) as n FROM orders WHERE product_id = ? AND status NOT IN ('completed','cancelled','refunded','expired')", [req.params.id])).n;
|
|
145
157
|
if (pendingOrders > 0)
|
|
146
158
|
return void res.json({ error: `该商品有 ${pendingOrders} 个进行中订单,无法下架` });
|
|
147
|
-
|
|
159
|
+
await dbRun("UPDATE products SET status = 'warehouse', updated_at = datetime('now') WHERE id = ?", [req.params.id]);
|
|
148
160
|
res.json({ success: true });
|
|
149
161
|
});
|
|
150
162
|
// 公开:列表
|
|
151
|
-
app.get('/api/p2p-products', (_req, res) => {
|
|
152
|
-
const rows =
|
|
163
|
+
app.get('/api/p2p-products', async (_req, res) => {
|
|
164
|
+
const rows = await dbAll(`
|
|
153
165
|
SELECT p.id, p.seller_id, p.title, p.price, p.stock, p.images as thumbnail_json,
|
|
154
166
|
p.ship_regions as region, p.content_hash, p.peer_endpoint, p.content_signed_at,
|
|
155
167
|
u.handle as seller_handle, u.region as seller_region
|
|
@@ -158,19 +170,19 @@ export function registerP2pProductsRoutes(app, deps) {
|
|
|
158
170
|
WHERE p.status = 'active' AND p.stock > 0 AND p.p2p_mode = 1
|
|
159
171
|
ORDER BY p.created_at DESC
|
|
160
172
|
LIMIT 50
|
|
161
|
-
`)
|
|
173
|
+
`);
|
|
162
174
|
res.json({ items: rows });
|
|
163
175
|
});
|
|
164
176
|
// 公开:详情(含 hash + peer_endpoint)
|
|
165
|
-
app.get('/api/p2p-products/:id', (req, res) => {
|
|
166
|
-
const row =
|
|
177
|
+
app.get('/api/p2p-products/:id', async (req, res) => {
|
|
178
|
+
const row = await dbOne(`
|
|
167
179
|
SELECT p.id, p.seller_id, p.title, p.price, p.stock, p.images as thumbnail_json,
|
|
168
180
|
p.ship_regions as region, p.content_hash, p.peer_endpoint, p.content_signature, p.content_signed_at,
|
|
169
181
|
u.handle as seller_handle, u.region as seller_region, u.permanent_code as seller_code
|
|
170
182
|
FROM products p
|
|
171
183
|
LEFT JOIN users u ON u.id = p.seller_id
|
|
172
184
|
WHERE p.id = ? AND p.p2p_mode = 1
|
|
173
|
-
|
|
185
|
+
`, [req.params.id]);
|
|
174
186
|
if (!row)
|
|
175
187
|
return void res.status(404).json({ error: 'P2P 商品不存在' });
|
|
176
188
|
res.json({ product: row });
|