@seasonkoh/webaz 0.1.8 → 0.1.9

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.
Files changed (153) hide show
  1. package/LICENSE +48 -0
  2. package/README.md +156 -20
  3. package/dist/layer0-foundation/L0-1-database/schema.js +5 -4
  4. package/dist/layer0-foundation/L0-2-state-machine/engine.js +228 -7
  5. package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +156 -0
  6. package/dist/layer0-foundation/L0-2-state-machine/transitions.js +53 -12
  7. package/dist/layer0-foundation/L0-5-manifest/manifest.js +14 -1
  8. package/dist/layer1-agent/L1-1-mcp-server/auth.js +1 -1
  9. package/dist/layer1-agent/L1-1-mcp-server/server.js +3543 -852
  10. package/dist/layer1-agent/L1-2-external-anchor/anchor-engine.js +324 -0
  11. package/dist/layer1-agent/L1-2-identity/agent-passport.js +100 -0
  12. package/dist/layer2-business/L2-6-notifications/notification-engine.js +72 -5
  13. package/dist/layer2-business/L2-7-snf/snf-engine.js +287 -0
  14. package/dist/layer2-business/L2-anchor-registry/anchor-registry.js +396 -0
  15. package/dist/layer2-business/L2-notes/note-photo-storage.js +133 -0
  16. package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +6 -6
  17. package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +246 -0
  18. package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +95 -1
  19. package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +31 -2
  20. package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +358 -0
  21. package/dist/pwa/public/app.js +31230 -2345
  22. package/dist/pwa/public/i18n.js +5282 -111
  23. package/dist/pwa/public/icon.svg +11 -0
  24. package/dist/pwa/public/index.html +4 -1
  25. package/dist/pwa/public/manifest.json +39 -4
  26. package/dist/pwa/public/openapi.json +5946 -0
  27. package/dist/pwa/public/style.css +278 -5
  28. package/dist/pwa/public/sw.js +41 -2
  29. package/dist/pwa/public/vendor/jsQR.js +10102 -0
  30. package/dist/pwa/public/webaz-logo.png +0 -0
  31. package/dist/pwa/routes/account-deletion.js +53 -0
  32. package/dist/pwa/routes/addresses.js +105 -0
  33. package/dist/pwa/routes/admin-admins.js +151 -0
  34. package/dist/pwa/routes/admin-analytics.js +253 -0
  35. package/dist/pwa/routes/admin-atomic.js +21 -0
  36. package/dist/pwa/routes/admin-catalog.js +64 -0
  37. package/dist/pwa/routes/admin-editor-picks.js +45 -0
  38. package/dist/pwa/routes/admin-events.js +60 -0
  39. package/dist/pwa/routes/admin-health.js +66 -0
  40. package/dist/pwa/routes/admin-moderation.js +120 -0
  41. package/dist/pwa/routes/admin-ops.js +179 -0
  42. package/dist/pwa/routes/admin-protocol-params.js +79 -0
  43. package/dist/pwa/routes/admin-reports.js +154 -0
  44. package/dist/pwa/routes/admin-tokenomics.js +113 -0
  45. package/dist/pwa/routes/admin-users-lifecycle.js +237 -0
  46. package/dist/pwa/routes/admin-users-query.js +390 -0
  47. package/dist/pwa/routes/admin-verifier-flow.js +126 -0
  48. package/dist/pwa/routes/admin-verifier-whitelist.js +111 -0
  49. package/dist/pwa/routes/admin-wallet-ops.js +66 -0
  50. package/dist/pwa/routes/agent-buy.js +215 -0
  51. package/dist/pwa/routes/agent-governance.js +341 -0
  52. package/dist/pwa/routes/agent-reputation.js +34 -0
  53. package/dist/pwa/routes/ai.js +101 -0
  54. package/dist/pwa/routes/analytics.js +272 -0
  55. package/dist/pwa/routes/anchors.js +169 -0
  56. package/dist/pwa/routes/announcements.js +110 -0
  57. package/dist/pwa/routes/arbitrator.js +117 -0
  58. package/dist/pwa/routes/auction.js +436 -0
  59. package/dist/pwa/routes/auth-login.js +40 -0
  60. package/dist/pwa/routes/auth-read.js +66 -0
  61. package/dist/pwa/routes/auth-register.js +138 -0
  62. package/dist/pwa/routes/auth-sessions.js +62 -0
  63. package/dist/pwa/routes/blocklist.js +60 -0
  64. package/dist/pwa/routes/buyer-feeds.js +224 -0
  65. package/dist/pwa/routes/cart.js +155 -0
  66. package/dist/pwa/routes/charity.js +816 -0
  67. package/dist/pwa/routes/chat.js +318 -0
  68. package/dist/pwa/routes/checkin-tasks.js +122 -0
  69. package/dist/pwa/routes/checkout-helpers.js +85 -0
  70. package/dist/pwa/routes/claim-initiators.js +88 -0
  71. package/dist/pwa/routes/claim-verify.js +615 -0
  72. package/dist/pwa/routes/claim-voting.js +114 -0
  73. package/dist/pwa/routes/claim-withdrawals.js +20 -0
  74. package/dist/pwa/routes/coupons.js +165 -0
  75. package/dist/pwa/routes/dashboards.js +99 -0
  76. package/dist/pwa/routes/dispute-cases.js +267 -0
  77. package/dist/pwa/routes/disputes-read.js +358 -0
  78. package/dist/pwa/routes/disputes-write.js +475 -0
  79. package/dist/pwa/routes/evidence.js +86 -0
  80. package/dist/pwa/routes/external-anchors.js +107 -0
  81. package/dist/pwa/routes/feedback.js +270 -0
  82. package/dist/pwa/routes/flash-sales.js +130 -0
  83. package/dist/pwa/routes/follows.js +103 -0
  84. package/dist/pwa/routes/group-buys.js +208 -0
  85. package/dist/pwa/routes/growth.js +199 -0
  86. package/dist/pwa/routes/import-product.js +153 -0
  87. package/dist/pwa/routes/kyc.js +40 -0
  88. package/dist/pwa/routes/leaderboard.js +149 -0
  89. package/dist/pwa/routes/listings.js +281 -0
  90. package/dist/pwa/routes/logistics.js +35 -0
  91. package/dist/pwa/routes/manifests.js +126 -0
  92. package/dist/pwa/routes/me-data.js +101 -0
  93. package/dist/pwa/routes/notifications.js +48 -0
  94. package/dist/pwa/routes/offers.js +96 -0
  95. package/dist/pwa/routes/orders-action.js +285 -0
  96. package/dist/pwa/routes/orders-create.js +339 -0
  97. package/dist/pwa/routes/orders-read.js +180 -0
  98. package/dist/pwa/routes/p2p-products.js +178 -0
  99. package/dist/pwa/routes/payments-governance.js +311 -0
  100. package/dist/pwa/routes/peers.js +34 -0
  101. package/dist/pwa/routes/pin-receipts.js +39 -0
  102. package/dist/pwa/routes/products-aliases.js +119 -0
  103. package/dist/pwa/routes/products-claims.js +60 -0
  104. package/dist/pwa/routes/products-create.js +206 -0
  105. package/dist/pwa/routes/products-crud.js +73 -0
  106. package/dist/pwa/routes/products-links.js +129 -0
  107. package/dist/pwa/routes/products-list.js +424 -0
  108. package/dist/pwa/routes/products-meta.js +155 -0
  109. package/dist/pwa/routes/products-update.js +125 -0
  110. package/dist/pwa/routes/profile-credentials.js +105 -0
  111. package/dist/pwa/routes/profile-identity.js +174 -0
  112. package/dist/pwa/routes/profile-location.js +35 -0
  113. package/dist/pwa/routes/profile-placement.js +70 -0
  114. package/dist/pwa/routes/profile-prefs.js +93 -0
  115. package/dist/pwa/routes/promoter.js +208 -0
  116. package/dist/pwa/routes/public-utils.js +170 -0
  117. package/dist/pwa/routes/push.js +54 -0
  118. package/dist/pwa/routes/ratings.js +220 -0
  119. package/dist/pwa/routes/recover-key.js +100 -0
  120. package/dist/pwa/routes/referral.js +58 -0
  121. package/dist/pwa/routes/reputation.js +34 -0
  122. package/dist/pwa/routes/returns.js +493 -0
  123. package/dist/pwa/routes/reviews.js +81 -0
  124. package/dist/pwa/routes/rfqs.js +443 -0
  125. package/dist/pwa/routes/search.js +172 -0
  126. package/dist/pwa/routes/secondhand.js +278 -0
  127. package/dist/pwa/routes/seller-quota.js +225 -0
  128. package/dist/pwa/routes/share-redirects.js +164 -0
  129. package/dist/pwa/routes/shareables-interactions.js +212 -0
  130. package/dist/pwa/routes/shareables.js +470 -0
  131. package/dist/pwa/routes/shops.js +98 -0
  132. package/dist/pwa/routes/signaling.js +43 -0
  133. package/dist/pwa/routes/skill-market.js +173 -0
  134. package/dist/pwa/routes/skills.js +174 -0
  135. package/dist/pwa/routes/snf.js +126 -0
  136. package/dist/pwa/routes/tags.js +47 -0
  137. package/dist/pwa/routes/trial.js +333 -0
  138. package/dist/pwa/routes/trusted-kpi.js +87 -0
  139. package/dist/pwa/routes/url-claim.js +113 -0
  140. package/dist/pwa/routes/users-public.js +317 -0
  141. package/dist/pwa/routes/variants.js +156 -0
  142. package/dist/pwa/routes/verifier-user.js +107 -0
  143. package/dist/pwa/routes/verify-tasks.js +120 -0
  144. package/dist/pwa/routes/waitlist.js +65 -0
  145. package/dist/pwa/routes/wallet-read.js +218 -0
  146. package/dist/pwa/routes/wallet-write.js +273 -0
  147. package/dist/pwa/routes/webauthn.js +188 -0
  148. package/dist/pwa/routes/webhooks.js +162 -0
  149. package/dist/pwa/routes/welcome.js +226 -0
  150. package/dist/pwa/routes/wishlist-qa.js +135 -0
  151. package/dist/pwa/security/ssrf.js +110 -0
  152. package/dist/pwa/server.js +9247 -2097
  153. package/package.json +8 -3
@@ -0,0 +1,339 @@
1
+ export function registerOrdersCreateRoutes(app, deps) {
2
+ 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 } = deps;
3
+ app.post('/api/orders', (req, res) => {
4
+ const user = auth(req, res);
5
+ if (!user)
6
+ return;
7
+ // P0 fix: 受信角色不可下单(铁律)
8
+ if (isTrustedRole(user))
9
+ return void res.status(403).json({ error: '受信角色不可参与交易', error_code: 'TRUSTED_ROLE_NO_TRADE' });
10
+ if (user.role !== 'buyer')
11
+ return void res.json({ error: '仅买家可下单' });
12
+ // 2026-05-23 P0 audit fix 2.1:agent_attestations spend_cap 强制
13
+ const apiKey = req.headers.authorization?.replace('Bearer ', '');
14
+ if (apiKey) {
15
+ const cap = db.prepare(`SELECT spend_cap_per_order, spend_cap_daily FROM agent_attestations
16
+ WHERE api_key = ? AND user_id = ? AND revoked_at IS NULL`).get(apiKey, user.id);
17
+ if (cap) {
18
+ const estQty = Math.max(1, Math.floor(Number(req.body?.quantity ?? 1)));
19
+ const estPrice = Number(req.body?.expected_price ?? 0);
20
+ const estTotal = estPrice * estQty;
21
+ if (cap.spend_cap_per_order != null && estTotal > 0 && estTotal > cap.spend_cap_per_order) {
22
+ return void res.status(403).json({
23
+ error: `本笔订单 ${estTotal} WAZ 超过 agent 单笔上限 ${cap.spend_cap_per_order} WAZ(用户设定)`,
24
+ error_code: 'AGENT_SPEND_CAP_PER_ORDER',
25
+ spend_cap: cap.spend_cap_per_order,
26
+ });
27
+ }
28
+ if (cap.spend_cap_daily != null) {
29
+ const todaySpent = db.prepare(`SELECT COALESCE(SUM(total_amount), 0) as t
30
+ FROM orders WHERE buyer_id = ? AND created_at > datetime('now', '-24 hours') AND status != 'cancelled'`)
31
+ .get(user.id).t;
32
+ if (todaySpent + estTotal > cap.spend_cap_daily) {
33
+ return void res.status(403).json({
34
+ error: `24h 累计 ${todaySpent}+${estTotal} 超 agent 日上限 ${cap.spend_cap_daily} WAZ(用户设定)`,
35
+ error_code: 'AGENT_SPEND_CAP_DAILY',
36
+ spend_cap: cap.spend_cap_daily, today_spent: todaySpent,
37
+ });
38
+ }
39
+ }
40
+ }
41
+ }
42
+ const { product_id, shipping_address, notes, session_token, coupon_code, delivery_window, variant_id, expected_price,
43
+ // C-2: 礼物订单字段
44
+ is_gift, gift_recipient_name, gift_recipient_phone, gift_message,
45
+ // C-3: 订单保险
46
+ buy_insurance,
47
+ // B2 隐私购物:买家选匿名 → 生成 PR-XXXX 代号,shipping_address 应是中介点
48
+ anonymous_recipient,
49
+ // B5 主动捐赠
50
+ donation_pct } = req.body;
51
+ if (!product_id || !shipping_address)
52
+ return void res.json({ error: '请提供商品ID和收货地址' });
53
+ const anonymousFlag = anonymous_recipient ? 1 : 0;
54
+ const recipientCode = anonymousFlag === 1 ? generateRecipientCode() : null;
55
+ const donationPctNum = Number(donation_pct || 0);
56
+ if (!DONATION_VALID_PCTS.has(donationPctNum)) {
57
+ return void res.json({ error: 'donation_pct 必须是 0 / 0.005 / 0.01 / 0.02 / 0.05 之一', error_code: 'DONATION_PCT_INVALID' });
58
+ }
59
+ // 数量校验 + 限购
60
+ const MAX_PER_ORDER = 10;
61
+ const reqQty = Math.floor(Number(req.body?.quantity ?? 1));
62
+ if (!Number.isFinite(reqQty) || reqQty < 1)
63
+ return void res.json({ error: '数量需 ≥ 1', error_code: 'QTY_INVALID' });
64
+ if (reqQty > MAX_PER_ORDER)
65
+ return void res.json({ error: `单笔订单最多 ${MAX_PER_ORDER} 件(限购)`, error_code: 'QTY_EXCEEDS_LIMIT', max_per_order: MAX_PER_ORDER });
66
+ // C-2: 礼物订单参数校验
67
+ let giftRecipientName = null;
68
+ let giftRecipientPhone = null;
69
+ let giftMessage = null;
70
+ if (is_gift) {
71
+ if (!gift_recipient_name || String(gift_recipient_name).trim().length < 1)
72
+ return void res.json({ error: '礼物订单需填收件人姓名' });
73
+ giftRecipientName = String(gift_recipient_name).slice(0, 60);
74
+ giftRecipientPhone = gift_recipient_phone ? String(gift_recipient_phone).slice(0, 30) : null;
75
+ giftMessage = gift_message ? String(gift_message).slice(0, 300) : null;
76
+ }
77
+ // Wave B-5: 校验配送时间窗(可选)
78
+ const VALID_DAY = new Set(['weekday', 'weekend', 'any']);
79
+ const VALID_TIME = new Set(['morning', 'afternoon', 'evening', 'any']);
80
+ let deliveryWindowJson = null;
81
+ if (delivery_window && typeof delivery_window === 'object') {
82
+ const dt = String(delivery_window.day_type || 'any');
83
+ const tr = String(delivery_window.time_range || 'any');
84
+ const fl = delivery_window.flexible !== false;
85
+ if (!VALID_DAY.has(dt) || !VALID_TIME.has(tr)) {
86
+ return void res.json({ error: '配送窗口参数无效' });
87
+ }
88
+ if (dt !== 'any' || tr !== 'any') {
89
+ deliveryWindowJson = JSON.stringify({ day_type: dt, time_range: tr, flexible: fl });
90
+ }
91
+ }
92
+ const product = db.prepare(`SELECT p.*, u.id as seller_uid FROM products p
93
+ JOIN users u ON p.seller_id = u.id WHERE p.id = ? AND p.status = 'active'`).get(product_id);
94
+ if (!product)
95
+ return void res.json({ error: '商品不存在或已下架' });
96
+ let variant = null;
97
+ if (Number(product.has_variants) === 1) {
98
+ if (!variant_id)
99
+ return void res.json({ error: '该商品需选择规格', error_code: 'VARIANT_REQUIRED' });
100
+ const v = db.prepare(`SELECT id, price_override, stock, options_json
101
+ FROM product_variants WHERE id = ? AND product_id = ? AND is_active = 1`)
102
+ .get(variant_id, product_id);
103
+ if (!v)
104
+ return void res.json({ error: '规格不存在或已下架' });
105
+ if (Number(v.stock) < reqQty)
106
+ return void res.json({ error: `该规格库存不足(剩 ${v.stock},需 ${reqQty})`, error_code: 'STOCK_INSUFFICIENT', stock: v.stock });
107
+ variant = v;
108
+ if (v.price_override != null)
109
+ product.price = Number(v.price_override);
110
+ }
111
+ else if (variant_id) {
112
+ return void res.json({ error: '该商品无规格选项,请勿传 variant_id' });
113
+ }
114
+ else {
115
+ if (product.stock < reqQty)
116
+ return void res.json({ error: `库存不足(剩 ${product.stock},需 ${reqQty})`, error_code: 'STOCK_INSUFFICIENT', stock: product.stock });
117
+ }
118
+ // Wave D-4: 限时促销 — 命中则覆盖 product.price 为 sale_price
119
+ const flashSale = getActiveFlashSale(product.id, variant ? variant.id : null);
120
+ if (flashSale) {
121
+ product.price = Number(flashSale.sale_price);
122
+ }
123
+ // Wave D-4 P0-1: 价格保护(expected_price 不一致 → 409)
124
+ if (expected_price != null) {
125
+ const expected = Number(expected_price);
126
+ if (Number.isFinite(expected) && Math.abs(expected - Number(product.price)) > 0.001) {
127
+ return void res.status(409).json({
128
+ error: 'price_changed',
129
+ error_code: 'PRICE_CHANGED',
130
+ message: '价格已变动(限时促销可能已结束或商品调价)',
131
+ new_price: Number(product.price),
132
+ old_price: expected,
133
+ });
134
+ }
135
+ }
136
+ // Wave A-3: 优惠券应用 — P1-1: flash sale 不可叠加 coupon
137
+ let couponId = null;
138
+ let couponDiscount = 0;
139
+ if (coupon_code && typeof coupon_code === 'string' && coupon_code.trim()) {
140
+ if (flashSale) {
141
+ return void res.json({ error: '限时促销不可与优惠券叠加,请去掉优惠码', error_code: 'FLASH_NO_COUPON' });
142
+ }
143
+ const result = applyCouponToOrder(coupon_code, product.seller_uid, product_id, Number(product.price) * reqQty);
144
+ if (!result.ok)
145
+ return void res.json({ error: result.error, error_code: 'COUPON_INVALID' });
146
+ couponId = result.coupon.id;
147
+ couponDiscount = result.discount || 0;
148
+ }
149
+ // 验证 session_token(如果提供)
150
+ if (session_token) {
151
+ const session = db.prepare(`
152
+ SELECT * FROM price_sessions WHERE token = ? AND product_id = ? AND user_id = ?
153
+ `).get(session_token, product_id, user.id);
154
+ if (!session)
155
+ return void res.json({ error: 'session_token 无效,请重新调用 verify-price' });
156
+ if (session.used_at)
157
+ return void res.json({ error: 'session_token 已使用,请重新调用 verify-price' });
158
+ if (new Date(session.expires_at) < new Date()) {
159
+ return void res.json({ error: 'session_token 已过期(10分钟有效),请重新调用 verify-price' });
160
+ }
161
+ if (session.price !== product.price) {
162
+ return void res.json({
163
+ error: 'price_changed',
164
+ message: `商品价格已变动:验证时 ${session.price} WAZ,当前 ${product.price} WAZ`,
165
+ new_price: product.price,
166
+ hint: '请重新调用 verify-price 获取新价格',
167
+ });
168
+ }
169
+ db.prepare(`UPDATE price_sessions SET used_at = datetime('now') WHERE token = ?`).run(session_token);
170
+ }
171
+ const basePrice = product.price;
172
+ // 多件:subtotal = basePrice * qty,减 coupon,再加保险(按 subtotal 计费)
173
+ const subtotal = Math.round(basePrice * reqQty * 100) / 100;
174
+ const priceAfterCoupon = Math.max(0, Math.round((subtotal - couponDiscount) * 100) / 100);
175
+ const insuranceRate = getProtocolParam('order_insurance_rate', 0.01);
176
+ const insurancePremium = buy_insurance ? Math.round(priceAfterCoupon * insuranceRate * 100) / 100 : 0;
177
+ const totalAmount = Math.max(0, Math.round((priceAfterCoupon + insurancePremium) * 100) / 100);
178
+ // B5 主动捐赠 — 按订单总额 × 比例算(额外扣款,进 charity_fund)
179
+ const donationAmount = donationPctNum > 0 ? Math.round(totalAmount * donationPctNum * 100) / 100 : 0;
180
+ const wallet = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(user.id);
181
+ if (!wallet)
182
+ return void res.status(500).json({ error: '钱包记录缺失', error_code: 'WALLET_MISSING' });
183
+ if (wallet.balance < totalAmount + donationAmount)
184
+ return void res.json({ error: `余额不足:需 ${(totalAmount + donationAmount).toFixed(2)} WAZ(含 ${donationAmount} WAZ 捐赠),当前 ${wallet.balance} WAZ` });
185
+ const now = new Date();
186
+ const orderId = generateId('ord');
187
+ let autoAccepted = false;
188
+ // P0: 整个下单流程原子化 — INSERT order + UPDATE wallet + UPDATE products + transition 任一步抛错全部回滚
189
+ try {
190
+ db.transaction(() => {
191
+ // 推土机分享快照:从 buyer.sponsor_path 解析 L1/L2/L3,应用 region 限制
192
+ const buyer = db.prepare("SELECT sponsor_id, sponsor_path, region FROM users WHERE id = ?").get(user.id);
193
+ // 孤儿用户首次绑 sponsor:buyer 无 sponsor + 客户端传 sponsor_hint
194
+ // 校验:① 非自己 ② 防环路 ③ hint 必须是 verified buyer
195
+ const sponsorHint = (typeof req.body.sponsor_hint === 'string' && req.body.sponsor_hint) ? String(req.body.sponsor_hint) : null;
196
+ if (!buyer.sponsor_id && sponsorHint && sponsorHint !== user.id) {
197
+ const hint = db.prepare("SELECT id, sponsor_path FROM users WHERE id = ? AND id NOT IN ('sys_protocol', ?)")
198
+ .get(sponsorHint, INTERNAL_AUDITOR_ID);
199
+ if (hint && isAllowedSponsor(hint.id)) {
200
+ const hintPath = hint.sponsor_path || '';
201
+ if (!hintPath.split('>').includes(user.id)) {
202
+ const newPath = hint.sponsor_path ? `${hint.sponsor_path}>${hint.id}` : hint.id;
203
+ db.prepare("UPDATE users SET sponsor_id = ?, sponsor_path = ?, updated_at = datetime('now') WHERE id = ?")
204
+ .run(hint.id, newPath, user.id);
205
+ buyer.sponsor_id = hint.id;
206
+ buyer.sponsor_path = newPath;
207
+ }
208
+ }
209
+ }
210
+ const maxLevels = (() => {
211
+ const r = db.prepare("SELECT max_levels FROM region_config WHERE region = ? AND active = 1").get(buyer?.region || 'global');
212
+ return r?.max_levels ?? 3;
213
+ })();
214
+ // 商品分享奖励链(per-product),与 PV 系统 sponsor_path 完全解耦
215
+ // 反推方向:谁分享了 product 给 buyer? → 该 sharer 是 L1
216
+ const productChain = getProductShareChain(product.id, user.id, 3);
217
+ const l1 = productChain[0];
218
+ const l2 = productChain[1];
219
+ const l3 = (maxLevels >= 3) ? productChain[2] : null;
220
+ const snapshotRate = Number(product.commission_rate ?? 0.10);
221
+ // H-2 fix:buyer_region 在下单时快照写入,settleCommission/depositToFund 读快照不读活值
222
+ const buyerRegionSnapshot = buyer?.region || 'global';
223
+ // P2P:若为 P2P 商品,下单时快照 content_hash(争议时凭买家所见 hash 判定)
224
+ const contentHashSnapshot = (Number(product.p2p_mode) === 1 && product.content_hash) ? String(product.content_hash) : null;
225
+ db.prepare(`INSERT INTO orders (
226
+ id, product_id, buyer_id, seller_id, quantity, unit_price, total_amount, escrow_amount,
227
+ status, shipping_address, notes, pay_deadline, accept_deadline, ship_deadline,
228
+ pickup_deadline, delivery_deadline, confirm_deadline,
229
+ l1_uid, l2_uid, l3_uid, snapshot_commission_rate, buyer_region, content_hash_at_order,
230
+ delivery_window, variant_id, variant_options_snapshot,
231
+ gift_recipient_name, gift_recipient_phone, gift_message, insurance_premium,
232
+ anonymous_recipient, recipient_code, donation_amount
233
+ ) VALUES (?,?,?,?,?,?,?,?,'created',?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(orderId, product.id, user.id, product.seller_uid, reqQty, basePrice, totalAmount, totalAmount, shipping_address, notes || null, addHours(now, 24), addHours(now, 48), addHours(now, 120), addHours(now, 168), addHours(now, 336), addHours(now, 408), l1, l2, l3, snapshotRate, buyerRegionSnapshot, contentHashSnapshot, deliveryWindowJson, variant ? variant.id : null, variant ? variant.options_json : null, giftRecipientName, giftRecipientPhone, giftMessage, insurancePremium, anonymousFlag, recipientCode, donationAmount);
234
+ // 协议层:写 genesis 事件 — order 创建(必然是 buyer 自己)
235
+ try {
236
+ appendOrderEvent(db, {
237
+ orderId,
238
+ eventType: 'open',
239
+ fromStatus: null,
240
+ toStatus: 'created',
241
+ actorId: user.id,
242
+ actorRole: 'buyer',
243
+ extra: {
244
+ product_id: product.id,
245
+ seller_id: product.seller_uid,
246
+ quantity: reqQty,
247
+ unit_price: basePrice,
248
+ total_amount: totalAmount,
249
+ variant_id: variant ? variant.id : null,
250
+ },
251
+ });
252
+ }
253
+ catch (e) {
254
+ console.warn('[order-chain] genesis event failed:', e.message);
255
+ }
256
+ db.prepare('UPDATE wallets SET balance = balance - ?, escrowed = escrowed + ? WHERE user_id = ?')
257
+ .run(totalAmount, totalAmount, user.id);
258
+ // B5:捐赠 — 从 balance 扣 + 进 charity_fund + 记一笔 donation txn(事务内原子)
259
+ if (donationAmount > 0) {
260
+ db.prepare('UPDATE wallets SET balance = balance - ? WHERE user_id = ?').run(donationAmount, user.id);
261
+ db.prepare(`UPDATE charity_fund SET balance = balance + ?, total_donated = total_donated + ?, updated_at = datetime('now') WHERE id = 'main'`)
262
+ .run(donationAmount, donationAmount);
263
+ db.prepare(`INSERT INTO charity_fund_txns (id, kind, from_user_id, to_user_id, amount, related_order_id, note)
264
+ VALUES (?, 'donation', ?, NULL, ?, ?, ?)`).run(generateId('cft'), user.id, donationAmount, orderId, `下单时捐赠 ${(donationPctNum * 100).toFixed(1)}%`);
265
+ // 同步 charity_reputation(捐款荣誉)
266
+ try {
267
+ ensureCharityRep(db, user.id);
268
+ }
269
+ catch { }
270
+ try {
271
+ db.prepare(`UPDATE charity_reputation SET donation_total = donation_total + ?, last_active = datetime('now') WHERE user_id = ?`)
272
+ .run(donationAmount, user.id);
273
+ }
274
+ catch { }
275
+ }
276
+ // Wave C-1: 有 variant → 同时 -qty variant.stock 和 product.stock(aggregate 保持一致)
277
+ // P1-3: 条件 UPDATE + changes() 防多 worker 竞态超卖
278
+ if (variant) {
279
+ const upd = db.prepare('UPDATE product_variants SET stock = stock - ?, updated_at = datetime(\'now\') WHERE id = ? AND stock >= ?').run(reqQty, variant.id, reqQty);
280
+ if (upd.changes !== 1)
281
+ throw new Error('VARIANT_STOCK_RACE');
282
+ }
283
+ const updP = db.prepare('UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?').run(reqQty, product.id, reqQty);
284
+ if (updP.changes !== 1)
285
+ throw new Error('PRODUCT_STOCK_RACE');
286
+ checkStockAndMaybeDelist(String(product.id));
287
+ // Wave D-4: flash sale sold_count 原子递增(max_qty 校验)
288
+ if (flashSale) {
289
+ const updF = db.prepare(`UPDATE flash_sales SET sold_count = sold_count + ?
290
+ WHERE id = ? AND (max_qty = 0 OR sold_count + ? <= max_qty) AND ends_at > datetime('now')`).run(reqQty, flashSale.id, reqQty);
291
+ if (updF.changes !== 1)
292
+ throw new Error('FLASH_SALE_EXHAUSTED');
293
+ }
294
+ // Wave A-3: 记录 coupon 使用 + 增加 uses_count
295
+ if (couponId) {
296
+ db.prepare('UPDATE orders SET coupon_id = ?, coupon_discount = ? WHERE id = ?')
297
+ .run(couponId, couponDiscount, orderId);
298
+ db.prepare('UPDATE coupons SET uses_count = uses_count + 1 WHERE id = ?').run(couponId);
299
+ }
300
+ transition(db, orderId, 'paid', user.id, [], '模拟支付完成');
301
+ notifyTransition(db, orderId, 'created', 'paid');
302
+ // 里程碑 3-C:双轨同支检测(监测+审计;不阻断)
303
+ try {
304
+ auditSponsorChainCross(orderId, user.id, String(product.seller_uid), buyer.sponsor_path);
305
+ }
306
+ catch (e) {
307
+ console.error('[M3-C audit]', e);
308
+ }
309
+ // 检查卖家是否有 auto_accept Skill
310
+ if (shouldAutoAccept(db, orderId)) {
311
+ const sysUser = db.prepare("SELECT id FROM users WHERE id = 'sys_protocol'").get();
312
+ if (sysUser) {
313
+ const ar = transition(db, orderId, 'accepted', sysUser.id, [], '⚡ auto_accept Skill 自动接单');
314
+ if (ar.success) {
315
+ notifyTransition(db, orderId, 'paid', 'accepted');
316
+ autoAccepted = true;
317
+ }
318
+ }
319
+ }
320
+ })();
321
+ }
322
+ catch (e) {
323
+ const msg = e.message;
324
+ console.error('[POST /api/orders tx]', msg);
325
+ if (msg === 'VARIANT_STOCK_RACE' || msg === 'PRODUCT_STOCK_RACE') {
326
+ return void res.status(409).json({ error: '库存已被抢光,请重试', error_code: 'STOCK_DEPLETED' });
327
+ }
328
+ if (msg === 'FLASH_SALE_EXHAUSTED') {
329
+ return void res.status(409).json({ error: '限时促销名额已售罄', error_code: 'FLASH_EXHAUSTED' });
330
+ }
331
+ return void res.status(500).json({ error: '下单失败,请重试', error_code: 'ORDER_TXN_FAILED' });
332
+ }
333
+ try {
334
+ broadcastSystemEvent('order_created', '📦', `订单创建 ${orderId} · ${totalAmount} WAZ`, orderId);
335
+ }
336
+ catch { }
337
+ res.json({ success: true, order_id: orderId, total_amount: totalAmount, auto_accepted: autoAccepted || undefined });
338
+ });
339
+ }
@@ -0,0 +1,180 @@
1
+ export function registerOrdersReadRoutes(app, deps) {
2
+ const { db, auth, getOrderStatus, getOrderChain, verifyOrderChain, getOrderDispute } = deps;
3
+ app.get('/api/orders', (req, res) => {
4
+ const user = auth(req, res);
5
+ if (!user)
6
+ return;
7
+ const orders = db.prepare(`
8
+ SELECT o.*, p.title as product_title, p.images,
9
+ ub.name as buyer_name, us.name as seller_name
10
+ FROM orders o
11
+ JOIN products p ON o.product_id = p.id
12
+ JOIN users ub ON o.buyer_id = ub.id
13
+ JOIN users us ON o.seller_id = us.id
14
+ WHERE o.buyer_id = ? OR o.seller_id = ? OR o.logistics_id = ?
15
+ ORDER BY o.created_at DESC LIMIT 50
16
+ `).all(user.id, user.id, user.id);
17
+ // B2 隐私购物:列表里也做相同 mask(防 seller/logistics 通过列表绕过详情 mask)
18
+ for (const o of orders) {
19
+ if (Number(o.anonymous_recipient) === 1 && o.buyer_id !== user.id) {
20
+ const code = o.recipient_code || 'PR-?????';
21
+ o.shipping_address = `🔒 ${code} · ${o.shipping_address}`;
22
+ delete o.recipient_code;
23
+ o.buyer_name = '🔒 ' + (typeof code === 'string' ? code : 'PR-?????');
24
+ }
25
+ }
26
+ res.json(orders);
27
+ });
28
+ // Wave D-2: 订单导出 CSV
29
+ app.get('/api/orders/export', (req, res) => {
30
+ const user = auth(req, res);
31
+ if (!user)
32
+ return;
33
+ const role = req.query.role === 'seller' ? 'seller' : 'buyer';
34
+ const field = role === 'seller' ? 'o.seller_id' : 'o.buyer_id';
35
+ const from = req.query.from ? String(req.query.from) : null;
36
+ const to = req.query.to ? String(req.query.to) : null;
37
+ const where = [`${field} = ?`];
38
+ const params = [user.id];
39
+ if (from) {
40
+ where.push(`o.created_at >= ?`);
41
+ params.push(from);
42
+ }
43
+ if (to) {
44
+ where.push(`o.created_at <= ?`);
45
+ params.push(to);
46
+ }
47
+ const EXPORT_LIMIT = 5000;
48
+ const rows = db.prepare(`
49
+ SELECT o.id, o.created_at, o.status, o.quantity, o.unit_price, o.total_amount,
50
+ o.coupon_discount, o.variant_options_snapshot, o.shipping_address,
51
+ p.title as product_title, p.category,
52
+ ub.handle as buyer_handle, ub.name as buyer_name,
53
+ us.handle as seller_handle, us.name as seller_name
54
+ FROM orders o
55
+ JOIN products p ON p.id = o.product_id
56
+ JOIN users ub ON ub.id = o.buyer_id
57
+ JOIN users us ON us.id = o.seller_id
58
+ WHERE ${where.join(' AND ')}
59
+ ORDER BY o.created_at DESC LIMIT ?
60
+ `).all(...params, EXPORT_LIMIT + 1);
61
+ // P1-4: 触达上限 → X-Truncated 头
62
+ const truncated = rows.length > EXPORT_LIMIT;
63
+ if (truncated)
64
+ rows.length = EXPORT_LIMIT;
65
+ const csvEscape = (val) => {
66
+ const s = val == null ? '' : String(val);
67
+ if (s.includes(',') || s.includes('"') || s.includes('\n') || s.includes('\r')) {
68
+ return `"${s.replace(/"/g, '""')}"`;
69
+ }
70
+ return s;
71
+ };
72
+ const headers = ['order_id', 'created_at', 'status', 'product', 'category', 'qty', 'unit_price', 'total', 'coupon_discount', 'variant', 'buyer', 'seller', 'address'];
73
+ const lines = [headers.join(',')];
74
+ for (const r of rows) {
75
+ let variantStr = '';
76
+ try {
77
+ const v = r.variant_options_snapshot ? JSON.parse(r.variant_options_snapshot) : null;
78
+ if (v)
79
+ variantStr = Object.entries(v).map(([k, val]) => `${k}:${val}`).join(';');
80
+ }
81
+ catch { }
82
+ lines.push([
83
+ r.id, r.created_at, r.status, r.product_title, r.category,
84
+ r.quantity, r.unit_price, r.total_amount, r.coupon_discount || 0,
85
+ variantStr, r.buyer_handle, r.seller_handle, r.shipping_address,
86
+ ].map(csvEscape).join(','));
87
+ }
88
+ const filename = `webaz-orders-${role}-${new Date().toISOString().slice(0, 10)}.csv`;
89
+ res.setHeader('Content-Type', 'text/csv; charset=utf-8');
90
+ res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
91
+ if (truncated) {
92
+ res.setHeader('X-Truncated', '1');
93
+ res.setHeader('X-Truncated-Limit', String(EXPORT_LIMIT));
94
+ res.setHeader('Access-Control-Expose-Headers', 'X-Truncated, X-Truncated-Limit');
95
+ }
96
+ // UTF-8 BOM 帮 Excel 识别
97
+ res.send('' + lines.join('\n'));
98
+ });
99
+ // 订单签名链 — 当事人 + arbitrator + admin 可查
100
+ app.get('/api/orders/:id/chain', (req, res) => {
101
+ const user = auth(req, res);
102
+ if (!user)
103
+ return;
104
+ const order = db.prepare('SELECT buyer_id, seller_id, logistics_id FROM orders WHERE id = ?').get(req.params.id);
105
+ if (!order)
106
+ return void res.status(404).json({ error: '订单不存在' });
107
+ const uid = user.id;
108
+ const isParty = uid === order.buyer_id || uid === order.seller_id || uid === order.logistics_id || user.role === 'arbitrator' || user.role === 'admin';
109
+ if (!isParty)
110
+ return void res.status(403).json({ error: '无权查看此订单链' });
111
+ const chain = getOrderChain(db, req.params.id);
112
+ const verification = verifyOrderChain(db, req.params.id);
113
+ res.json({ chain, verification });
114
+ });
115
+ app.get('/api/orders/:id', (req, res) => {
116
+ const user = auth(req, res);
117
+ if (!user)
118
+ return;
119
+ const statusInfo = getOrderStatus(db, req.params.id);
120
+ if (!statusInfo)
121
+ return void res.status(404).json({ error: '订单不存在' });
122
+ const order = statusInfo.order;
123
+ const isLogisticsPickup = user.role === 'logistics' &&
124
+ !order.logistics_id && order.status === 'shipped';
125
+ if (order.buyer_id !== user.id && order.seller_id !== user.id && order.logistics_id !== user.id && user.role !== 'arbitrator' && !isLogisticsPickup) {
126
+ return void res.status(403).json({ error: '无权查看此订单' });
127
+ }
128
+ // M8: 二手订单从 secondhand_items 查;商家订单从 products 查
129
+ const product = order.source === 'secondhand'
130
+ ? (() => {
131
+ const si = db.prepare('SELECT title, price, images FROM secondhand_items WHERE id = ?').get(order.product_id);
132
+ if (!si)
133
+ return null;
134
+ try {
135
+ return { title: si.title, price: si.price, images: JSON.parse(si.images || '[]') };
136
+ }
137
+ catch {
138
+ return { title: si.title, price: si.price, images: [] };
139
+ }
140
+ })()
141
+ : db.prepare('SELECT id, title, price, images, return_days FROM products WHERE id = ?').get(order.product_id);
142
+ const dispute = getOrderDispute(db, req.params.id);
143
+ // 为每条历史记录附上证据描述
144
+ const history = statusInfo.history.map(h => {
145
+ // P1 fix: 单条脏 evidence_ids 不应封死整个 order 详情
146
+ let ids = [];
147
+ try {
148
+ const p = JSON.parse(h.evidence_ids || '[]');
149
+ if (Array.isArray(p))
150
+ ids = p;
151
+ }
152
+ catch { }
153
+ const evidenceItems = ids.length
154
+ ? db.prepare(`SELECT description, type FROM evidence WHERE id IN (${ids.map(() => '?').join(',')})`).all(...ids)
155
+ : [];
156
+ return { ...h, evidence_items: evidenceItems };
157
+ });
158
+ // 物流跟踪摘要:从历史中提取所有物流操作的证据
159
+ const LOGISTICS_STEPS = ['shipped', 'picked_up', 'in_transit', 'delivered'];
160
+ const trackingInfo = history
161
+ .filter(h => LOGISTICS_STEPS.includes(h.to_status))
162
+ .map(h => ({
163
+ status: h.to_status,
164
+ actor: h.actor_name,
165
+ time: h.created_at,
166
+ evidence: h.evidence_items.map(e => e.description).filter(Boolean),
167
+ notes: h.notes,
168
+ }));
169
+ // B2 隐私购物:匿名订单 + 非买家本人 → shipping_address 前缀代号;不下放 recipient_code
170
+ if (Number(order.anonymous_recipient) === 1) {
171
+ const isBuyer = order.buyer_id === user.id;
172
+ if (!isBuyer) {
173
+ const code = order.recipient_code || 'PR-?????';
174
+ order.shipping_address = `🔒 ${code} · ${order.shipping_address}`;
175
+ delete order.recipient_code;
176
+ }
177
+ }
178
+ res.json({ ...statusInfo, history, product, dispute, trackingInfo });
179
+ });
180
+ }