@seasonkoh/webaz 0.1.27 → 0.1.29

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 (121) hide show
  1. package/README.md +4 -3
  2. package/README.zh-CN.md +7 -6
  3. package/dist/currency.js +16 -0
  4. package/dist/deposit-rails.js +52 -0
  5. package/dist/direct-pay-aml-monitor.js +67 -0
  6. package/dist/direct-pay-aml-review.js +40 -0
  7. package/dist/direct-pay-base-bond-entry.js +5 -0
  8. package/dist/direct-pay-bond-rail-clearance.js +94 -0
  9. package/dist/direct-pay-compliance-ingress.js +145 -0
  10. package/dist/direct-pay-controls.js +136 -0
  11. package/dist/direct-pay-create.js +162 -0
  12. package/dist/direct-pay-deferral-quota.js +43 -0
  13. package/dist/direct-pay-disclosures.js +44 -0
  14. package/dist/direct-pay-eligibility.js +46 -0
  15. package/dist/direct-pay-fee-ar.js +241 -0
  16. package/dist/direct-pay-launch-readiness.js +108 -0
  17. package/dist/direct-pay-launch-summary.js +68 -0
  18. package/dist/direct-pay-ledger.js +94 -0
  19. package/dist/direct-receive-account-qr.js +77 -0
  20. package/dist/direct-receive-accounts.js +82 -0
  21. package/dist/direct-receive-deferral.js +142 -0
  22. package/dist/direct-receive-deposits.js +260 -0
  23. package/dist/direct-receive-payment-instruction.js +26 -0
  24. package/dist/fx-rates.js +71 -0
  25. package/dist/layer0-foundation/L0-1-database/schema.js +531 -1
  26. package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +15 -5
  27. package/dist/layer0-foundation/L0-2-state-machine/transitions.js +41 -0
  28. package/dist/layer0-foundation/L0-5-manifest/manifest.js +1 -1
  29. package/dist/layer1-agent/L1-1-mcp-server/cli.js +64 -0
  30. package/dist/layer1-agent/L1-1-mcp-server/network-mode.js +80 -0
  31. package/dist/layer1-agent/L1-1-mcp-server/server.js +251 -69
  32. package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +4 -4
  33. package/dist/ledger.js +12 -3
  34. package/dist/mcp.js +23 -4
  35. package/dist/merchant-bond-domain.js +64 -0
  36. package/dist/merchant-bond-exposure.js +78 -0
  37. package/dist/merchant-bond-watcher.js +26 -0
  38. package/dist/payment-rails.js +29 -0
  39. package/dist/product-verification.js +93 -0
  40. package/dist/pwa/acp-feed.js +3 -2
  41. package/dist/pwa/contract-fingerprint.js +3 -0
  42. package/dist/pwa/direct-pay-guards.js +20 -0
  43. package/dist/pwa/direct-pay-order-redaction.js +24 -0
  44. package/dist/pwa/endpoint-actions.js +3 -0
  45. package/dist/pwa/public/app-account.js +977 -0
  46. package/dist/pwa/public/app-admin.js +608 -0
  47. package/dist/pwa/public/app-agents.js +63 -0
  48. package/dist/pwa/public/app-ai.js +2162 -0
  49. package/dist/pwa/public/app-chat-poll.js +29 -0
  50. package/dist/pwa/public/app-contribution.js +836 -0
  51. package/dist/pwa/public/app-create-kinds.js +17 -0
  52. package/dist/pwa/public/app-direct-pay-accounts.js +141 -0
  53. package/dist/pwa/public/app-direct-pay-buyer.js +72 -0
  54. package/dist/pwa/public/app-direct-pay-compliance.js +67 -0
  55. package/dist/pwa/public/app-direct-pay-deferral-admin.js +72 -0
  56. package/dist/pwa/public/app-direct-pay-deferral.js +61 -0
  57. package/dist/pwa/public/app-direct-pay-fee-center.js +33 -0
  58. package/dist/pwa/public/app-direct-pay-fee-ops.js +112 -0
  59. package/dist/pwa/public/app-direct-pay-product-verify.js +103 -0
  60. package/dist/pwa/public/app-direct-pay-readiness.js +38 -0
  61. package/dist/pwa/public/app-direct-pay-store-verify.js +100 -0
  62. package/dist/pwa/public/app-direct-pay.js +227 -0
  63. package/dist/pwa/public/app-discover.js +1296 -0
  64. package/dist/pwa/public/app-external-links.js +32 -0
  65. package/dist/pwa/public/app-listings.js +226 -0
  66. package/dist/pwa/public/app-prelaunch-waz.js +39 -0
  67. package/dist/pwa/public/app-price.js +55 -0
  68. package/dist/pwa/public/app-product-media.js +15 -0
  69. package/dist/pwa/public/app-profile.js +1692 -0
  70. package/dist/pwa/public/app-seller.js +199 -0
  71. package/dist/pwa/public/app-shop.js +1145 -0
  72. package/dist/pwa/public/app.js +13388 -22277
  73. package/dist/pwa/public/i18n.js +413 -196
  74. package/dist/pwa/public/index.html +27 -0
  75. package/dist/pwa/public/openapi.json +579 -1
  76. package/dist/pwa/routes/admin-analytics.js +4 -2
  77. package/dist/pwa/routes/admin-direct-receive-deposits.js +321 -0
  78. package/dist/pwa/routes/agent-grants.js +255 -0
  79. package/dist/pwa/routes/buyer-feeds.js +2 -1
  80. package/dist/pwa/routes/chat.js +6 -1
  81. package/dist/pwa/routes/dashboards.js +2 -1
  82. package/dist/pwa/routes/direct-pay-availability.js +196 -0
  83. package/dist/pwa/routes/direct-pay-disclosure-acks.js +74 -0
  84. package/dist/pwa/routes/direct-pay-timeouts.js +71 -0
  85. package/dist/pwa/routes/direct-receive-accounts.js +155 -0
  86. package/dist/pwa/routes/direct-receive-payment-instructions.js +45 -0
  87. package/dist/pwa/routes/fx.js +12 -0
  88. package/dist/pwa/routes/leaderboard.js +47 -9
  89. package/dist/pwa/routes/listings.js +2 -2
  90. package/dist/pwa/routes/logistics.js +4 -0
  91. package/dist/pwa/routes/manifests.js +38 -0
  92. package/dist/pwa/routes/me-data.js +5 -2
  93. package/dist/pwa/routes/orders-action.js +117 -9
  94. package/dist/pwa/routes/orders-create.js +4 -0
  95. package/dist/pwa/routes/orders-read.js +36 -0
  96. package/dist/pwa/routes/p2p-products.js +2 -2
  97. package/dist/pwa/routes/products-create.js +5 -3
  98. package/dist/pwa/routes/products-links.js +34 -0
  99. package/dist/pwa/routes/products-list.js +2 -1
  100. package/dist/pwa/routes/products-update.js +22 -2
  101. package/dist/pwa/routes/promoter.js +3 -0
  102. package/dist/pwa/routes/referral.js +4 -0
  103. package/dist/pwa/routes/returns.js +26 -1
  104. package/dist/pwa/routes/rewards-apply.js +10 -5
  105. package/dist/pwa/routes/rewards-clearing-mature.js +96 -0
  106. package/dist/pwa/routes/rewards-escrow-expire.js +6 -2
  107. package/dist/pwa/routes/shops.js +2 -1
  108. package/dist/pwa/routes/url-claim.js +2 -2
  109. package/dist/pwa/routes/users-public.js +8 -0
  110. package/dist/pwa/routes/wallet-read.js +3 -0
  111. package/dist/pwa/routes/webauthn.js +7 -2
  112. package/dist/pwa/server-schema.js +9 -0
  113. package/dist/pwa/server.js +261 -1706
  114. package/dist/runtime/agent-grant-scopes.js +128 -0
  115. package/dist/runtime/agent-grant-verifier.js +67 -0
  116. package/dist/runtime/agent-pairing.js +60 -0
  117. package/dist/runtime/apply-webaz-runtime-schema.js +15 -0
  118. package/dist/runtime/webaz-schema-helpers.js +1952 -0
  119. package/dist/store-verification.js +77 -0
  120. package/dist/version.js +1 -1
  121. package/package.json +80 -2
@@ -22,11 +22,15 @@ import crypto from 'crypto';
22
22
  import { initDatabase, generateId } from '../layer0-foundation/L0-1-database/schema.js';
23
23
  import { setSeamDb } from '../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
24
24
  import { initSystemUser, transition, getOrderStatus, checkTimeouts, settleFault } from '../layer0-foundation/L0-2-state-machine/engine.js';
25
+ import { genuineSalePredicate } from '../layer0-foundation/L0-2-state-machine/genuine-sale.js';
26
+ import { settleDirectPayFeeAtCompletion } from '../direct-pay-fee-ar.js'; // RFC-018 PR4 genuine-sale SSOT + Direct Pay Rail 1 平台费链下应收(完成时释放遗留模拟 stake + accrue)
25
27
  import { endpointToAction, endpointToReadAction } from './endpoint-actions.js';
26
28
  import { AGENT_RATE_PER_MIN_DEFAULTS, CROSS_USER_READ_DAILY_CAP, MASS_ACTION_TYPES, MASS_ACTION_DAILY_CAPS } from './limits.js';
27
29
  // #420 P1-2/P1-3/P1-4 — 反滥用阈值单一真相源(governance-adjustable protocol_params)+ 纯决策函数
28
30
  import { ANTI_ABUSE_PARAMS, readAntiAbuseThresholds, agentTrustLevel, agentSybilPenalty, agentStrikeSeverity, verifierOutlierBand } from './anti-abuse-thresholds.js';
31
+ import { DIRECT_PAY_CONTROL_PARAMS, DIRECT_PAY_AML_PARAMS } from '../direct-pay-controls.js'; // PR-4a 控制面 + PR-6D AML 监控 param 默认 seed(均 fail-closed / inert)
29
32
  import { initOrderChainSchema, appendOrderEvent, getOrderChain, verifyOrderChain } from '../layer0-foundation/L0-2-state-machine/order-chain.js';
33
+ import { initVerifierWhitelistSchema, initMcpToolCallsSchema, initNotePhotoIndexSchema, initUserWishlistSchema, initProductQaSchema, initCouponsSchema, initAnnouncementsSchema, initProductWaitlistSchema, initFlashSalesSchema, initPublicIdeasSchema, initAuctionRemindersSchema, initEmailSubscriptionsSchema, initFeedbackTicketsSchema, initFeedbackMessagesSchema, initDisputeCasesSchema, initDisputeCommentsSchema, initDisputeCommentRepliesSchema, initShareableCommentsSchema, initDisputeFairnessVotesSchema, initOrderRatingsSchema, initBuyerRatingsSchema, initUserAddressesSchema, initP2pShopsSchema, initShareableLikesSchema, initShareableBookmarksSchema, initShareableTagsSchema, initManifestRegistrySchema, initPeerDirectorySchema, initSignalingQueueSchema, initConversationsSchema, initMessagesSchema, initChatReportsSchema, initQuotaIncreaseApplicationsSchema, initVerifierApplicationsSchema, initArbitratorReviewSchema, initVerifierAppealsSchema, initUserModerationSchema, initAdminAuditLogSchema, initVerificationCodesSchema, initAgentCallLogSchema, initAgentReputationSchema, initAgentDeclarationsSchema, initAgentAttestationsSchema, initAgentStrikesSchema, initAgentRevocationsSchema, initProductAliasesSchema, initRegionChangeLogSchema, initCartItemsSchema, initFollowsSchema, initPushSubscriptionsSchema, initUserSessionsSchema, initUserBlocklistSchema, initImportLogsSchema, initErrorLogSchema, initSecondhandItemsSchema, initProductTrialCampaignsSchema, initProductTrialClaimsSchema, initReturnRequestsSchema, initReturnMessagesSchema, initProductVariantsSchema, initEditorPicksSchema, initKycRecordsSchema, initWebauthnSchema, initClaimVerificationBaseSchema, initClaimVerifierSuspensionsSchema, initProductClaimSchema, initReviewClaimSchema, initSecondhandClaimSchema, initAuctionClaimSchema, initWishClaimSchema, initShareableClickLogSchema, initCommissionAuditLogSchema, initRegistrationAuditLogSchema, initProductExternalLinksBaseSchema, initLinkChallengesSchema, initVerifyTasksSchema, initVerifySubmissionsSchema, initVerifierStatsSchema, initRegisterListSearchColumns, initPendingCommissionEscrowSchema } from './server-schema.js';
30
34
  // RFC-014 PR4 — 正常成交结算走整数 base-units + allocate + 绝对值落库。
31
35
  import { toUnits, toDecimal, mulRate, allocate } from '../money.js';
32
36
  import { applyWalletDelta, creditColumns } from '../ledger.js';
@@ -83,6 +87,7 @@ import { registerClaimVerifyRoutes, processClaimTaskQueue, isEligibleClaimVerifi
83
87
  import { registerFollowsRoutes } from './routes/follows.js';
84
88
  // Leaderboard (#1013 Phase 11) — 单 endpoint 8 kinds(products/creators/buyers/sellers/value/agents/arbitrators/verifiers)
85
89
  import { registerLeaderboardRoutes } from './routes/leaderboard.js';
90
+ import { registerFxRoutes } from './routes/fx.js'; // FX = display-only USDC→local rates
86
91
  // Shareables 互动 (#1013 Phase 12) — click/like/comments/bookmark 8 endpoints
87
92
  import { registerShareablesInteractionsRoutes } from './routes/shareables-interactions.js';
88
93
  // Shareables CRUD (#1013 Phase 13) — 11 endpoints (notes-photo + create + me + creator-stats + by-* + feed + detail + PATCH + DELETE)
@@ -153,6 +158,8 @@ import { registerArbitratorRoutes } from './routes/arbitrator.js';
153
158
  import { registerGovernanceOnboardingRoutes } from './routes/governance-onboarding.js';
154
159
  import { startAutoDeactivateCron, runAutoDeactivateSweep } from './routes/governance-auto-deactivate.js';
155
160
  import { startEscrowExpireCron } from './routes/rewards-escrow-expire.js';
161
+ import { startDirectPayTimeoutCron } from './routes/direct-pay-timeouts.js';
162
+ import { startClearingMatureCron } from './routes/rewards-clearing-mature.js';
156
163
  import { startAutoDowngradeCron } from './routes/rewards-auto-downgrade.js';
157
164
  import { registerRewardsApplyRoutes } from './routes/rewards-apply.js';
158
165
  // 卖家配额 + 数据中心 (#1013 Phase 45) — 4 user + 3 admin
@@ -241,6 +248,11 @@ import { registerOrdersReadRoutes } from './routes/orders-read.js';
241
248
  import { registerOrdersActionRoutes } from './routes/orders-action.js';
242
249
  // Orders 下单端点 (#1013 Phase 85) — 1 endpoint (338-line POST /api/orders)
243
250
  import { registerOrdersCreateRoutes } from './routes/orders-create.js';
251
+ import { registerDirectPayDisclosureAckRoutes } from './routes/direct-pay-disclosure-acks.js'; // PR-4d: Direct Pay 风险披露 ack 端点(薄 adapter)
252
+ import { registerDirectReceivePaymentInstructionRoutes } from './routes/direct-receive-payment-instructions.js';
253
+ import { registerDirectReceiveAccountsRoutes } from './routes/direct-receive-accounts.js'; // PR-4f-a instruction CRUD + Phase C1 multi-account+QR
254
+ import { registerDirectPayAvailabilityRoutes } from './routes/direct-pay-availability.js'; // PR-4a: Direct Pay 可用性只读(控制面 SSOT)
255
+ import { registerAdminDirectReceiveDepositsRoutes } from './routes/admin-direct-receive-deposits.js'; // PR-4b-3: ROOT 生产保证金 receipt 确认(fail-closed scaffold)
244
256
  // Disputes 读端点 (#1013 Phase 86) — 5 endpoints (list/similar/detail/evidence-list/parties)
245
257
  import { registerDisputesReadRoutes } from './routes/disputes-read.js';
246
258
  // Disputes 写端点 (#1013 Phase 87) — 5 endpoints (respond/arbitrate/add-evidence/evidence-blob/request-evidence)
@@ -388,6 +400,7 @@ initSkillSchema(db);
388
400
  initSkillMarketSchema(db);
389
401
  initReputationSchema(db);
390
402
  initOrderChainSchema(db);
403
+ initPendingCommissionEscrowSchema(db); // RFC-018 PR1: relocated from inline (was ~8250) so all escrow schema is built in this early batch — see helper. matures_at + reversed added (schema-only).
391
404
  initBuildFeedbackSchema(db); // RFC-004 build_feedback
392
405
  initBuildTasksSchema(db); // RFC-006 build_tasks(协调层)
393
406
  initBuildTaskAgentMetadataSchema(db); // PR9B — agent-ready task metadata satellite(schema only;FUTURE-TASK-BOARD-V1-DESIGN #326)
@@ -417,11 +430,11 @@ ensureEvidenceColumns(db);
417
430
  initAnchorRegistrySchema(db);
418
431
  // boot-order fix(2026-05-26):anchor migration 引用 users.handle / search_anchor,
419
432
  // 但对应 ALTER TABLE 在 735+/958+ 行才跑。旧 DB(v3 era)触发 prepare 失败 → 此处 catch
420
- // 后 warn 不阻塞 server,但日志噪音 → 预热那两列让 migration 真正能跑
421
- try {
422
- db.exec("ALTER TABLE users ADD COLUMN handle TEXT");
423
- }
424
- catch { }
433
+ // 后 warn 不阻塞 server,但日志噪音 → 预热那两列让 migration 真正能跑。
434
+ // handle 现由 initRegisterListSearchColumns 在此预热(与 MCP runtime schema 同源,见
435
+ // src/runtime/webaz-schema-helpers.ts);该 helper 同时建 permanent_code/region + 11
436
+ // products 结构化字段(纯非钱列,从下方各 inline 块单点收口到此处,CREATE-before-ALTER 不变)。
437
+ initRegisterListSearchColumns(db);
425
438
  try {
426
439
  db.exec("ALTER TABLE users ADD COLUMN search_anchor TEXT");
427
440
  }
@@ -459,27 +472,9 @@ catch (e) {
459
472
  console.warn('[anchor-registry] migration:', e.message);
460
473
  }
461
474
  // ─── 验证员白名单表 ───────────────────────────────────────────────
462
- db.exec(`
463
- CREATE TABLE IF NOT EXISTS verifier_whitelist (
464
- user_id TEXT PRIMARY KEY,
465
- added_at TEXT DEFAULT (datetime('now')),
466
- note TEXT
467
- )
468
- `);
475
+ initVerifierWhitelistSchema(db);
469
476
  // ─── MCP 工具调用埋点表(远程上报)─────────────────────────────────
470
- db.exec(`
471
- CREATE TABLE IF NOT EXISTS mcp_tool_calls (
472
- id INTEGER PRIMARY KEY AUTOINCREMENT,
473
- tool_name TEXT NOT NULL,
474
- user_id_hash TEXT,
475
- server_version TEXT,
476
- outcome TEXT NOT NULL,
477
- latency_ms INTEGER NOT NULL,
478
- ts TEXT NOT NULL DEFAULT (datetime('now'))
479
- )
480
- `);
481
- db.exec(`CREATE INDEX IF NOT EXISTS idx_mcp_tc_ts ON mcp_tool_calls(ts)`);
482
- db.exec(`CREATE INDEX IF NOT EXISTS idx_mcp_tc_tool ON mcp_tool_calls(tool_name, ts)`);
477
+ initMcpToolCallsSchema(db);
483
478
  // ─── 内部审核账号(固定 ID,密钥由 MASTER_SEED 派生,幂等)────────
484
479
  const INTERNAL_AUDITOR_ID = 'usr_iaudit_001';
485
480
  const INTERNAL_AUDITOR_KEY = 'key_iaudit_' + createHmac('sha256', MASTER_SEED).update('internal_auditor_v1').digest('hex').slice(0, 32);
@@ -540,7 +535,7 @@ catch { }
540
535
  for (const stmt of [
541
536
  'ALTER TABLE users ADD COLUMN sponsor_id TEXT',
542
537
  'ALTER TABLE users ADD COLUMN sponsor_path TEXT',
543
- "ALTER TABLE users ADD COLUMN region TEXT DEFAULT 'global'",
538
+ // users.region moved to initRegisterListSearchColumns (single source, shared w/ MCP) — see ~line 494.
544
539
  // Admin 分级:root 全权 / regional 按 admin_scope 区域受限
545
540
  "ALTER TABLE users ADD COLUMN admin_type TEXT", // root | regional
546
541
  "ALTER TABLE users ADD COLUMN admin_scope TEXT", // global | china | us | eu | india | singapore
@@ -598,28 +593,7 @@ for (const stmt of [
598
593
  // M8 二手板块:独立表,避免污染 products 商家货架
599
594
  // 关键差异:1 件即 1 件(无库存)、个人卖家无需 seller 角色、无质保、协议费 1%(vs 商家 2%)
600
595
  try {
601
- db.exec(`CREATE TABLE IF NOT EXISTS secondhand_items (
602
- id TEXT PRIMARY KEY,
603
- seller_id TEXT NOT NULL,
604
- title TEXT NOT NULL,
605
- description TEXT,
606
- category TEXT NOT NULL, -- phone/computer/appliance/furniture/clothing/book/toy/sports/other
607
- condition_grade TEXT NOT NULL, -- brand_new/like_new/lightly_used/well_used/heavily_used
608
- price REAL NOT NULL,
609
- negotiable INTEGER DEFAULT 0,
610
- images TEXT, -- JSON 数组:dataURL 字符串 (最多 9 张)
611
- region TEXT,
612
- fulfillment TEXT DEFAULT 'both', -- shipping / in_person / both
613
- status TEXT DEFAULT 'available', -- available / reserved / sold / closed
614
- view_count INTEGER DEFAULT 0,
615
- created_at TEXT DEFAULT (datetime('now')),
616
- updated_at TEXT DEFAULT (datetime('now')),
617
- sold_at TEXT,
618
- sold_order_id TEXT
619
- )`);
620
- db.exec(`CREATE INDEX IF NOT EXISTS idx_si_status_created ON secondhand_items(status, created_at DESC)`);
621
- db.exec(`CREATE INDEX IF NOT EXISTS idx_si_seller ON secondhand_items(seller_id, status)`);
622
- db.exec(`CREATE INDEX IF NOT EXISTS idx_si_cat ON secondhand_items(category, status)`);
596
+ initSecondhandItemsSchema(db);
623
597
  }
624
598
  catch (e) {
625
599
  console.error('[secondhand schema]', e);
@@ -795,29 +769,9 @@ catch { }
795
769
  }
796
770
  catch { }
797
771
  });
798
- // P13: 购物车
799
- db.exec(`
800
- CREATE TABLE IF NOT EXISTS cart_items (
801
- user_id TEXT NOT NULL,
802
- product_id TEXT NOT NULL,
803
- qty INTEGER NOT NULL DEFAULT 1,
804
- added_at TEXT DEFAULT (datetime('now')),
805
- PRIMARY KEY (user_id, product_id)
806
- )
807
- `);
808
- // P14: 关注关系(社交电商)
809
- db.exec(`
810
- CREATE TABLE IF NOT EXISTS follows (
811
- follower_id TEXT NOT NULL,
812
- followee_id TEXT NOT NULL,
813
- created_at TEXT DEFAULT (datetime('now')),
814
- PRIMARY KEY (follower_id, followee_id)
815
- )
816
- `);
817
- try {
818
- db.exec('CREATE INDEX IF NOT EXISTS idx_follows_followee ON follows(followee_id)');
819
- }
820
- catch { }
772
+ // P13: 购物车 / P14: 关注关系(社交电商)→ server-schema.ts
773
+ initCartItemsSchema(db);
774
+ initFollowsSchema(db);
821
775
  // P14: 用户 feed 可见性开关(默认公开)
822
776
  try {
823
777
  db.exec("ALTER TABLE users ADD COLUMN feed_visible INTEGER DEFAULT 1");
@@ -975,6 +929,7 @@ const DEFAULT_PARAMS = [
975
929
  // #420 P1-2/P1-3/P1-4:反滥用阈值(agent 信任公式 / strike 阶梯 / verifier outlier)→ 治理可调。
976
930
  // 默认值 === 抽取前硬编码字面量(单一真相源在 anti-abuse-thresholds.ts;测试强制校验一致)。
977
931
  ...ANTI_ABUSE_PARAMS,
932
+ ...DIRECT_PAY_CONTROL_PARAMS, ...DIRECT_PAY_AML_PARAMS,
978
933
  ];
979
934
  for (const p of DEFAULT_PARAMS) {
980
935
  try {
@@ -1188,23 +1143,7 @@ function disbursePlatformReward(userId, amount, source, ref) {
1188
1143
  // Wave E-5: PWA Push 订阅
1189
1144
  // 注:实际 push 投递需要 web-push 库(npm i web-push)+ VAPID 私钥签名;
1190
1145
  // 当前实现只做订阅层 + SW push 事件处理,留待 web-push 接入后即可发送
1191
- db.exec(`
1192
- CREATE TABLE IF NOT EXISTS push_subscriptions (
1193
- id TEXT PRIMARY KEY,
1194
- user_id TEXT NOT NULL,
1195
- endpoint TEXT NOT NULL,
1196
- p256dh TEXT NOT NULL,
1197
- auth TEXT NOT NULL,
1198
- user_agent TEXT,
1199
- enabled INTEGER DEFAULT 1,
1200
- created_at TEXT DEFAULT (datetime('now')),
1201
- UNIQUE(user_id, endpoint)
1202
- )
1203
- `);
1204
- try {
1205
- db.exec('CREATE INDEX IF NOT EXISTS idx_push_user ON push_subscriptions(user_id, enabled)');
1206
- }
1207
- catch { }
1146
+ initPushSubscriptionsSchema(db);
1208
1147
  // 2026-05-22 V2:verifier 新任务通知偏好(默认开,可关)
1209
1148
  try {
1210
1149
  db.exec('ALTER TABLE users ADD COLUMN notify_claim_tasks INTEGER DEFAULT 1');
@@ -1277,14 +1216,8 @@ catch { } // 完整店铺介绍(多段)
1277
1216
  // ─── 4 层身份模型 ─────────────────────────────────────────
1278
1217
  // id (内部 usr_xxx, 永不可改) + permanent_code (6 位 Crockford base32, 永不可改, 对外短码)
1279
1218
  // + handle (@username, 可改 7天1次/年3次) + name (昵称, 可重复可改)
1280
- try {
1281
- db.exec("ALTER TABLE users ADD COLUMN permanent_code TEXT");
1282
- }
1283
- catch { }
1284
- try {
1285
- db.exec("ALTER TABLE users ADD COLUMN handle TEXT");
1286
- }
1287
- catch { }
1219
+ // permanent_code / handle + 其唯一索引已上移到 initRegisterListSearchColumns(~line 494,
1220
+ // MCP runtime schema 同源);此处仅保留 handle 的附属列(不在 register/list/search 路径上)
1288
1221
  try {
1289
1222
  db.exec("ALTER TABLE users ADD COLUMN handle_last_created_at TEXT");
1290
1223
  }
@@ -1293,14 +1226,6 @@ try {
1293
1226
  db.exec("ALTER TABLE users ADD COLUMN handle_change_log TEXT");
1294
1227
  }
1295
1228
  catch { } // JSON: [{at, from}], 保留近 365 天
1296
- try {
1297
- db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_users_permanent_code ON users(permanent_code) WHERE permanent_code IS NOT NULL");
1298
- }
1299
- catch { }
1300
- try {
1301
- db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_users_handle ON users(handle) WHERE handle IS NOT NULL");
1302
- }
1303
- catch { }
1304
1229
  // P15 雷达扫描:粗粒度地理位置(0.1° ≈ 11km × 11km,QVOD 风格匿名聚合)
1305
1230
  try {
1306
1231
  db.exec("ALTER TABLE users ADD COLUMN geo_lat REAL");
@@ -1396,27 +1321,7 @@ const LARGE_WITHDRAW_THRESHOLD = 100;
1396
1321
  // 用途:防 api_key 泄露后无法吊销的根本问题。每个 api_key 关联一个 session 行;
1397
1322
  // 用户可在 "活跃会话" 页查看 IP/UA/最后活跃,单点吊销或一键全登出。
1398
1323
  // "一键全登出" = rotate users.api_key(所有旧 key 即刻 401,新 key 在 session 表里)。
1399
- db.exec(`
1400
- CREATE TABLE IF NOT EXISTS user_sessions (
1401
- id TEXT PRIMARY KEY,
1402
- user_id TEXT NOT NULL,
1403
- api_key TEXT NOT NULL,
1404
- ip TEXT,
1405
- user_agent TEXT,
1406
- fingerprint_hash TEXT,
1407
- created_at TEXT DEFAULT (datetime('now')),
1408
- last_seen_at TEXT DEFAULT (datetime('now')),
1409
- revoked_at TEXT
1410
- )
1411
- `);
1412
- try {
1413
- db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_user ON user_sessions(user_id, revoked_at)');
1414
- }
1415
- catch { }
1416
- try {
1417
- db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_key ON user_sessions(api_key)');
1418
- }
1419
- catch { }
1324
+ initUserSessionsSchema(db);
1420
1325
  // A4 智能下单:用户默认地址(搜索时自动过滤不可达商品 + 下单时预填)
1421
1326
  try {
1422
1327
  db.exec("ALTER TABLE users ADD COLUMN default_address_text TEXT");
@@ -1432,19 +1337,7 @@ try {
1432
1337
  }
1433
1338
  catch { }
1434
1339
  // A2 黑名单(精准匹配护栏):买家可拉黑卖家,搜索时自动过滤
1435
- db.exec(`
1436
- CREATE TABLE IF NOT EXISTS user_blocklist (
1437
- blocker_id TEXT NOT NULL,
1438
- blocked_id TEXT NOT NULL,
1439
- reason TEXT,
1440
- created_at TEXT DEFAULT (datetime('now')),
1441
- PRIMARY KEY (blocker_id, blocked_id)
1442
- )
1443
- `);
1444
- try {
1445
- db.exec("CREATE INDEX IF NOT EXISTS idx_blocklist_blocker ON user_blocklist(blocker_id)");
1446
- }
1447
- catch { }
1340
+ initUserBlocklistSchema(db);
1448
1341
  // P-Distrib β:分布式内容层(外链 shareables + P2P 原生 manifests + pin 经济)
1449
1342
  // shareables = 外链分享(YouTube/TikTok/小红书 等外部内容)— 仅索引 URL,零内容存储
1450
1343
  db.exec(`
@@ -1530,99 +1423,12 @@ try {
1530
1423
  db.exec("CREATE INDEX IF NOT EXISTS idx_share_order ON shareables(related_order_id) WHERE related_order_id IS NOT NULL");
1531
1424
  }
1532
1425
  catch { }
1533
- // 审计修 C-1:笔记图片 hash 索引表 O(1) 剽窃检测,替代全表扫描
1534
- // hash PRIMARY KEY 天然唯一约束;删笔记时同步删对应行
1535
- db.exec(`
1536
- CREATE TABLE IF NOT EXISTS note_photo_index (
1537
- hash TEXT PRIMARY KEY,
1538
- shareable_id TEXT NOT NULL
1539
- )
1540
- `);
1541
- try {
1542
- db.exec("CREATE INDEX IF NOT EXISTS idx_npi_shareable ON note_photo_index(shareable_id)");
1543
- }
1544
- catch { }
1545
- // Wave A-1: 个人心愿单(独立于慈善 wishes)
1546
- db.exec(`
1547
- CREATE TABLE IF NOT EXISTS user_wishlist (
1548
- user_id TEXT NOT NULL,
1549
- product_id TEXT NOT NULL,
1550
- note TEXT,
1551
- notify_price_drop INTEGER DEFAULT 1,
1552
- notify_back_in_stock INTEGER DEFAULT 1,
1553
- price_at_add REAL,
1554
- created_at TEXT DEFAULT (datetime('now')),
1555
- PRIMARY KEY(user_id, product_id)
1556
- )
1557
- `);
1558
- try {
1559
- db.exec('CREATE INDEX IF NOT EXISTS idx_wl_product ON user_wishlist(product_id)');
1560
- }
1561
- catch { }
1562
- // Wave A-2: 商品 Q&A(公开问答 — 自动 FAQ + 防虚假承诺)
1563
- db.exec(`
1564
- CREATE TABLE IF NOT EXISTS product_qa (
1565
- id TEXT PRIMARY KEY,
1566
- product_id TEXT NOT NULL,
1567
- asker_id TEXT NOT NULL,
1568
- seller_id TEXT NOT NULL,
1569
- question TEXT NOT NULL,
1570
- answer TEXT,
1571
- answered_at TEXT,
1572
- is_public INTEGER DEFAULT 1,
1573
- helpful_count INTEGER DEFAULT 0,
1574
- created_at TEXT DEFAULT (datetime('now'))
1575
- )
1576
- `);
1577
- try {
1578
- db.exec('CREATE INDEX IF NOT EXISTS idx_qa_product ON product_qa(product_id, created_at DESC)');
1579
- }
1580
- catch { }
1581
- try {
1582
- db.exec('CREATE INDEX IF NOT EXISTS idx_qa_seller ON product_qa(seller_id, answered_at)');
1583
- }
1584
- catch { }
1585
- try {
1586
- db.exec('CREATE INDEX IF NOT EXISTS idx_qa_asker ON product_qa(asker_id)');
1587
- }
1588
- catch { }
1589
- // 防重复 +1 的 votes 表
1590
- db.exec(`
1591
- CREATE TABLE IF NOT EXISTS product_qa_helpful_voters (
1592
- qa_id TEXT NOT NULL,
1593
- user_id TEXT NOT NULL,
1594
- voted_at TEXT DEFAULT (datetime('now')),
1595
- PRIMARY KEY (qa_id, user_id)
1596
- )
1597
- `);
1598
- // Wave A-3: 优惠券 / 限时折扣(卖家发券 · 全店满减 · 单品限时)
1599
- db.exec(`
1600
- CREATE TABLE IF NOT EXISTS coupons (
1601
- id TEXT PRIMARY KEY,
1602
- seller_id TEXT NOT NULL,
1603
- code TEXT NOT NULL,
1604
- scope TEXT NOT NULL, -- 'product' | 'shop' | 'all'
1605
- scope_id TEXT, -- product_id when scope='product'
1606
- discount_type TEXT NOT NULL, -- 'percentage' | 'fixed'
1607
- discount_value REAL NOT NULL,
1608
- min_order_amount REAL DEFAULT 0,
1609
- max_uses INTEGER DEFAULT 0, -- 0 = unlimited
1610
- uses_count INTEGER DEFAULT 0,
1611
- starts_at TEXT,
1612
- expires_at TEXT,
1613
- is_active INTEGER DEFAULT 1,
1614
- created_at TEXT DEFAULT (datetime('now')),
1615
- UNIQUE(seller_id, code)
1616
- )
1617
- `);
1618
- try {
1619
- db.exec('CREATE INDEX IF NOT EXISTS idx_coupons_seller ON coupons(seller_id, is_active)');
1620
- }
1621
- catch { }
1622
- try {
1623
- db.exec('CREATE INDEX IF NOT EXISTS idx_coupons_scope ON coupons(scope, scope_id) WHERE is_active = 1');
1624
- }
1625
- catch { }
1426
+ // 笔记图片 hash 索引 / Wave A 购物表(心愿单 · 商品Q&A · 优惠券)→ server-schema.ts
1427
+ // 纯幂等建表/建索引 DDL,原位调用保持 boot 顺序不变
1428
+ initNotePhotoIndexSchema(db);
1429
+ initUserWishlistSchema(db);
1430
+ initProductQaSchema(db);
1431
+ initCouponsSchema(db);
1626
1432
  // Orders 表加 coupon 字段(记录使用了哪张券、折扣多少)
1627
1433
  for (const stmt of [
1628
1434
  'ALTER TABLE orders ADD COLUMN coupon_id TEXT',
@@ -1633,147 +1439,20 @@ for (const stmt of [
1633
1439
  }
1634
1440
  catch { }
1635
1441
  }
1636
- // Wave A-4: 平台公告(admin 发布 角色 / 区域定向)
1637
- db.exec(`
1638
- CREATE TABLE IF NOT EXISTS announcements (
1639
- id TEXT PRIMARY KEY,
1640
- author_id TEXT NOT NULL,
1641
- title TEXT NOT NULL,
1642
- body TEXT NOT NULL,
1643
- target_roles TEXT, -- JSON array: ['buyer','seller'] or null=all
1644
- target_regions TEXT, -- JSON array: ['china','us'] or null=all
1645
- severity TEXT DEFAULT 'info', -- 'info' | 'warning' | 'critical'
1646
- is_active INTEGER DEFAULT 1,
1647
- starts_at TEXT,
1648
- expires_at TEXT,
1649
- created_at TEXT DEFAULT (datetime('now'))
1650
- )
1651
- `);
1652
- try {
1653
- db.exec('CREATE INDEX IF NOT EXISTS idx_ann_active ON announcements(is_active, created_at DESC)');
1654
- }
1655
- catch { }
1656
- // 用户阅读记录(PK 防重复 dismiss)
1657
- db.exec(`
1658
- CREATE TABLE IF NOT EXISTS announcement_reads (
1659
- user_id TEXT NOT NULL,
1660
- announcement_id TEXT NOT NULL,
1661
- read_at TEXT DEFAULT (datetime('now')),
1662
- PRIMARY KEY (user_id, announcement_id)
1663
- )
1664
- `);
1665
- // Wave B-2: 预售 / waitlist(缺货商品允许买家排队 → 回货时通知)
1666
- db.exec(`
1667
- CREATE TABLE IF NOT EXISTS product_waitlist (
1668
- user_id TEXT NOT NULL,
1669
- product_id TEXT NOT NULL,
1670
- desired_qty INTEGER DEFAULT 1,
1671
- note TEXT,
1672
- notified_at TEXT, -- 回货时填,表示已发通知
1673
- created_at TEXT DEFAULT (datetime('now')),
1674
- PRIMARY KEY (user_id, product_id)
1675
- )
1676
- `);
1677
- try {
1678
- db.exec('CREATE INDEX IF NOT EXISTS idx_waitlist_product ON product_waitlist(product_id) WHERE notified_at IS NULL');
1679
- }
1680
- catch { }
1681
- // Wave D-4: 限时促销 / Flash Sale
1682
- db.exec(`
1683
- CREATE TABLE IF NOT EXISTS flash_sales (
1684
- id TEXT PRIMARY KEY,
1685
- seller_id TEXT NOT NULL,
1686
- product_id TEXT NOT NULL,
1687
- variant_id TEXT, -- 可选,绑定具体规格
1688
- sale_price REAL NOT NULL,
1689
- original_price REAL NOT NULL, -- 创建时快照,用于显示「省 X」
1690
- max_qty INTEGER DEFAULT 0, -- 0 = 不限
1691
- sold_count INTEGER DEFAULT 0,
1692
- starts_at TEXT NOT NULL,
1693
- ends_at TEXT NOT NULL,
1694
- is_active INTEGER DEFAULT 1,
1695
- created_at TEXT DEFAULT (datetime('now'))
1696
- )
1697
- `);
1698
- try {
1699
- db.exec('CREATE INDEX IF NOT EXISTS idx_flash_product ON flash_sales(product_id, is_active)');
1700
- }
1701
- catch { }
1702
- try {
1703
- db.exec('CREATE INDEX IF NOT EXISTS idx_flash_seller ON flash_sales(seller_id, ends_at DESC)');
1704
- }
1705
- catch { }
1442
+ // Wave A-4 公告+阅读 / Wave B-2 预售waitlist / Wave D-4 限时促销 → server-schema.ts
1443
+ // 纯幂等建表/建索引 DDL,原位调用保持 boot 顺序不变
1444
+ initAnnouncementsSchema(db);
1445
+ initProductWaitlistSchema(db);
1446
+ initFlashSalesSchema(db);
1706
1447
  // 公共助手:拿商品(含 variant 选项)当前生效的 flash sale
1707
1448
  // #1013 Phase 23: 已迁出到 routes/flash-sales.ts,本地 wrapper 让 orders 流程签名不变
1708
1449
  const getActiveFlashSale = (productId, variantId) => getActiveFlashSaleRaw(db, productId, variantId);
1709
1450
  // 2026-05-24 #978: 测评免单 (Trial Review Refund)
1710
1451
  // 卖家发新品时可开启「测评免单」计划:买家以原价正常下单,发笔记达 reach 阈值后系统自动退款
1711
1452
  // reach_score = views * 0.1 + shares * 1 + conversions * 10
1712
- db.exec(`
1713
- CREATE TABLE IF NOT EXISTS product_trial_campaigns (
1714
- id TEXT PRIMARY KEY, -- ptc_xxxx
1715
- -- 1 product 1 row (复用同一行:关闭后再开 = UPDATE status='active',避免 UNIQUE 阻断 reopen)
1716
- product_id TEXT NOT NULL UNIQUE REFERENCES products(id),
1717
- seller_id TEXT NOT NULL REFERENCES users(id),
1718
- quota_total INTEGER NOT NULL, -- 总名额 1-200
1719
- quota_claimed INTEGER NOT NULL DEFAULT 0,
1720
- reach_threshold INTEGER NOT NULL, -- 综合 reach 阈值 (默认 50)
1721
- min_chars INTEGER NOT NULL DEFAULT 50, -- 笔记最少字数
1722
- min_days_live INTEGER NOT NULL DEFAULT 7, -- 笔记需 live 至少 N 天才评估
1723
- status TEXT NOT NULL DEFAULT 'active', -- active / paused / closed
1724
- created_at TEXT DEFAULT (datetime('now')),
1725
- closed_at TEXT
1726
- )
1727
- `);
1728
- try {
1729
- db.exec("CREATE INDEX IF NOT EXISTS idx_ptc_seller ON product_trial_campaigns(seller_id, status)");
1730
- }
1731
- catch { }
1732
- try {
1733
- db.exec("CREATE INDEX IF NOT EXISTS idx_ptc_product ON product_trial_campaigns(product_id, status)");
1734
- }
1735
- catch { }
1736
- db.exec(`
1737
- CREATE TABLE IF NOT EXISTS product_trial_claims (
1738
- id TEXT PRIMARY KEY, -- pcl_xxxx
1739
- campaign_id TEXT NOT NULL REFERENCES product_trial_campaigns(id),
1740
- product_id TEXT NOT NULL REFERENCES products(id),
1741
- seller_id TEXT NOT NULL REFERENCES users(id),
1742
- buyer_id TEXT NOT NULL REFERENCES users(id),
1743
- order_id TEXT NOT NULL REFERENCES orders(id),
1744
- note_id TEXT, -- shareables.id with type='note'
1745
- status TEXT NOT NULL DEFAULT 'pending_note', -- pending_note / pending_threshold / refunded / expired / cancelled
1746
- reach_score REAL DEFAULT 0,
1747
- metrics_json TEXT, -- 最新评估的 {views, shares, conversions} 快照
1748
- refund_amount REAL,
1749
- refunded_at TEXT,
1750
- expired_at TEXT,
1751
- last_eval_at TEXT,
1752
- claimed_at TEXT DEFAULT (datetime('now')),
1753
- note_linked_at TEXT,
1754
- UNIQUE(buyer_id, product_id) -- 一买家一商品仅 1 个名额
1755
- )
1756
- `);
1757
- try {
1758
- db.exec("CREATE INDEX IF NOT EXISTS idx_pcl_campaign ON product_trial_claims(campaign_id, status)");
1759
- }
1760
- catch { }
1761
- try {
1762
- db.exec("CREATE INDEX IF NOT EXISTS idx_pcl_buyer ON product_trial_claims(buyer_id, status)");
1763
- }
1764
- catch { }
1765
- try {
1766
- db.exec("CREATE INDEX IF NOT EXISTS idx_pcl_seller ON product_trial_claims(seller_id, status)");
1767
- }
1768
- catch { }
1769
- try {
1770
- db.exec("CREATE INDEX IF NOT EXISTS idx_pcl_eval ON product_trial_claims(status, last_eval_at)");
1771
- }
1772
- catch { }
1773
- try {
1774
- db.exec("CREATE INDEX IF NOT EXISTS idx_pcl_note ON product_trial_claims(note_id) WHERE note_id IS NOT NULL");
1775
- }
1776
- catch { }
1453
+ // 测评免单计划 + 认领 → server-schema.ts;claims 的 snap/audit ALTER 刻意留原位(紧跟下方)
1454
+ initProductTrialCampaignsSchema(db);
1455
+ initProductTrialClaimsSchema(db);
1777
1456
  // 审计 P0-1:claim 时快照 campaign 配置,cron 评估按快照而非当前活动(防卖家中途上调阈值白嫖)
1778
1457
  for (const col of [
1779
1458
  'ALTER TABLE product_trial_claims ADD COLUMN snap_reach_threshold INTEGER',
@@ -1790,29 +1469,8 @@ for (const col of [
1790
1469
  }
1791
1470
  catch { /* 已存在 */ }
1792
1471
  }
1793
- // 2026-05-25 邮箱订阅独立表(GDPR-ready)— ideas 解耦
1794
- // consent 显式存;unsubscribe_token 让用户主动退订;source 区分来源
1795
- db.exec(`
1796
- CREATE TABLE IF NOT EXISTS email_subscriptions (
1797
- id TEXT PRIMARY KEY,
1798
- email TEXT NOT NULL UNIQUE,
1799
- source TEXT NOT NULL DEFAULT 'welcome',
1800
- consent_at TEXT NOT NULL DEFAULT (datetime('now')),
1801
- unsubscribe_token TEXT NOT NULL UNIQUE,
1802
- unsubscribed_at TEXT,
1803
- ip_hash TEXT,
1804
- user_id TEXT,
1805
- created_at TEXT DEFAULT (datetime('now'))
1806
- )
1807
- `);
1808
- try {
1809
- db.exec("CREATE INDEX IF NOT EXISTS idx_es_status ON email_subscriptions(unsubscribed_at, created_at DESC)");
1810
- }
1811
- catch { }
1812
- try {
1813
- db.exec("CREATE INDEX IF NOT EXISTS idx_es_source ON email_subscriptions(source, created_at DESC)");
1814
- }
1815
- catch { }
1472
+ // 邮箱订阅独立表(GDPR-ready)→ server-schema.ts;后续 ALTER 列扩展刻意留原位(紧跟下方)
1473
+ initEmailSubscriptionsSchema(db);
1816
1474
  // 2026-05-26: 用户期望身份 + 备注(welcome 表单丰富化)
1817
1475
  try {
1818
1476
  db.exec("ALTER TABLE email_subscriptions ADD COLUMN role_preference TEXT");
@@ -1835,71 +1493,13 @@ try {
1835
1493
  db.exec("ALTER TABLE email_subscriptions ADD COLUMN handled_by TEXT");
1836
1494
  }
1837
1495
  catch { }
1838
- // 2026-05-24 首屏「我有建议」公开收集(匿名可投,登录态自动绑 user_id)
1839
- db.exec(`
1840
- CREATE TABLE IF NOT EXISTS public_ideas (
1841
- id TEXT PRIMARY KEY,
1842
- user_id TEXT, -- 可空(匿名提交)
1843
- contact TEXT, -- 可选 email / handle / 任何联系方式
1844
- content TEXT NOT NULL,
1845
- ip_hash TEXT,
1846
- ua_hash TEXT,
1847
- status TEXT NOT NULL DEFAULT 'new', -- new / triaged / resolved / spam
1848
- created_at TEXT DEFAULT (datetime('now'))
1849
- )
1850
- `);
1851
- try {
1852
- db.exec("CREATE INDEX IF NOT EXISTS idx_pi_status ON public_ideas(status, created_at DESC)");
1853
- }
1854
- catch { }
1855
- try {
1856
- db.exec("CREATE INDEX IF NOT EXISTS idx_pi_rate ON public_ideas(ip_hash, created_at)");
1857
- }
1858
- catch { }
1859
- // 2026-05-24 #959: 拍卖「⏰ 提醒我」
1860
- // 买家订阅拍卖,cron 在 deadline - lead_minutes 时发通知;1 个订阅 = 多行(每个 lead 时间一行)
1861
- // 默认订阅 = [60, 10](结束前 1h + 10min 各提醒一次)
1862
- db.exec(`
1863
- CREATE TABLE IF NOT EXISTS auction_reminders (
1864
- id TEXT PRIMARY KEY, -- arm_xxxx
1865
- auction_id TEXT NOT NULL REFERENCES auctions(id),
1866
- user_id TEXT NOT NULL REFERENCES users(id),
1867
- lead_minutes INTEGER NOT NULL, -- 提前多少分钟提醒
1868
- fire_at TEXT NOT NULL, -- deadline - lead_minutes(创建时算好)
1869
- sent_at TEXT,
1870
- created_at TEXT DEFAULT (datetime('now')),
1871
- UNIQUE(auction_id, user_id, lead_minutes)
1872
- )
1873
- `);
1874
- try {
1875
- db.exec("CREATE INDEX IF NOT EXISTS idx_arm_due ON auction_reminders(sent_at, fire_at) WHERE sent_at IS NULL");
1876
- }
1877
- catch { }
1878
- try {
1879
- db.exec("CREATE INDEX IF NOT EXISTS idx_arm_user ON auction_reminders(user_id, auction_id)");
1880
- }
1881
- catch { }
1882
- // Wave D-3: 用户反馈 / 客服工单(buyer-to-platform,独立于 disputes)
1883
- db.exec(`
1884
- CREATE TABLE IF NOT EXISTS feedback_tickets (
1885
- id TEXT PRIMARY KEY,
1886
- user_id TEXT NOT NULL,
1887
- category TEXT NOT NULL, -- 'bug' | 'abuse' | 'feature' | 'account' | 'other'
1888
- severity TEXT DEFAULT 'medium', -- 'low' | 'medium' | 'high'
1889
- subject TEXT NOT NULL,
1890
- body TEXT NOT NULL,
1891
- status TEXT NOT NULL DEFAULT 'open', -- open | in_progress | resolved | closed
1892
- admin_reply TEXT,
1893
- replied_by TEXT,
1894
- replied_at TEXT,
1895
- created_at TEXT DEFAULT (datetime('now')),
1896
- updated_at TEXT DEFAULT (datetime('now'))
1897
- )
1898
- `);
1899
- try {
1900
- db.exec('CREATE INDEX IF NOT EXISTS idx_feedback_user ON feedback_tickets(user_id, created_at DESC)');
1901
- }
1902
- catch { }
1496
+ // 首屏「我有建议」公开收集 / #959 拍卖「⏰ 提醒我」 → server-schema.ts
1497
+ // 纯幂等建表/建索引 DDL,原位调用保持 boot 顺序不变(email_subscriptions 仍留原位)
1498
+ initPublicIdeasSchema(db);
1499
+ initAuctionRemindersSchema(db);
1500
+ // Wave D-3: 用户反馈 / 客服工单(buyer-to-platform,独立于 disputes)→ server-schema.ts
1501
+ // 后续 ALTER 列扩展刻意留原位(紧跟下方)
1502
+ initFeedbackTicketsSchema(db);
1903
1503
  // G-4: AI 建议回复
1904
1504
  try {
1905
1505
  db.exec('ALTER TABLE feedback_tickets ADD COLUMN ai_suggested_reply TEXT');
@@ -1918,21 +1518,9 @@ try {
1918
1518
  db.exec('ALTER TABLE feedback_tickets ADD COLUMN admin_seen_at TEXT');
1919
1519
  }
1920
1520
  catch { }
1921
- // W7 客服 ticket-thread — 多轮消息(user ↔ admin
1922
- db.exec(`
1923
- CREATE TABLE IF NOT EXISTS feedback_messages (
1924
- id TEXT PRIMARY KEY, -- fmsg_xxx
1925
- ticket_id TEXT NOT NULL,
1926
- sender_id TEXT NOT NULL,
1927
- sender_role TEXT NOT NULL, -- 'user' | 'admin'
1928
- body TEXT NOT NULL,
1929
- created_at TEXT DEFAULT (datetime('now'))
1930
- )
1931
- `);
1932
- try {
1933
- db.exec('CREATE INDEX IF NOT EXISTS idx_fmsg_ticket ON feedback_messages(ticket_id, created_at)');
1934
- }
1935
- catch { }
1521
+ // W7 客服 ticket-thread — 多轮消息(user ↔ admin)→ server-schema.ts
1522
+ // 后续 ALTER 列扩展刻意留原位(紧跟下方)
1523
+ initFeedbackMessagesSchema(db);
1936
1524
  // 跨窗反诈一致性:所有 thread 消息表加 flagged + flag_reasons
1937
1525
  try {
1938
1526
  db.exec('ALTER TABLE feedback_messages ADD COLUMN flagged INTEGER DEFAULT 0');
@@ -1943,50 +1531,10 @@ try {
1943
1531
  }
1944
1532
  catch { }
1945
1533
  // ─── 公开仲裁判例 (P1) ─────────────────────────────────────
1946
- // disputes 是当事人/仲裁员私域;dispute_cases 是裁决后的公开脱敏版本
1947
- db.exec(`
1948
- CREATE TABLE IF NOT EXISTS dispute_cases (
1949
- id TEXT PRIMARY KEY, -- dcase_xxx
1950
- dispute_id TEXT, -- 原始 disputes.id (内部追溯)
1951
- order_id TEXT,
1952
- product_id TEXT, -- 关键索引:按商品查公开判例
1953
- seller_id TEXT,
1954
- buyer_id TEXT, -- 仅内部使用,不外露
1955
- category_tag TEXT, -- 物流 / 质量 / 描述不符 / 售后 / 拒收 / 其他
1956
- winner TEXT, -- buyer / seller / split / dismissed
1957
- resolution TEXT, -- 简短人读判决 (如 '全额退款')
1958
- amount_bucket TEXT, -- '0-100' / '100-500' / '500-2000' / '2000+' WAZ
1959
- buyer_argument TEXT, -- 脱敏后买家陈述
1960
- seller_argument TEXT, -- 脱敏后卖家陈述
1961
- ruling_text TEXT, -- 仲裁员判决书
1962
- arbitrator_id TEXT,
1963
- fairness_yes INTEGER DEFAULT 0,
1964
- fairness_no INTEGER DEFAULT 0,
1965
- comment_count INTEGER DEFAULT 0,
1966
- published_at TEXT DEFAULT (datetime('now')),
1967
- created_at TEXT DEFAULT (datetime('now'))
1968
- )
1969
- `);
1970
- try {
1971
- db.exec('CREATE INDEX IF NOT EXISTS idx_dcase_product ON dispute_cases(product_id, published_at DESC)');
1972
- }
1973
- catch { }
1974
- try {
1975
- db.exec('CREATE INDEX IF NOT EXISTS idx_dcase_seller ON dispute_cases(seller_id, published_at DESC)');
1976
- }
1977
- catch { }
1978
- db.exec(`
1979
- CREATE TABLE IF NOT EXISTS dispute_comments (
1980
- id TEXT PRIMARY KEY, -- dcom_xxx
1981
- case_id TEXT NOT NULL,
1982
- commenter_id TEXT NOT NULL,
1983
- body TEXT NOT NULL,
1984
- flagged INTEGER DEFAULT 0,
1985
- likes INTEGER DEFAULT 0,
1986
- created_at TEXT DEFAULT (datetime('now')),
1987
- UNIQUE(case_id, commenter_id) -- 一案一人一次(防刷)
1988
- )
1989
- `);
1534
+ // 公开判例(裁决后脱敏版本,disputes 是当事人/仲裁员私域)→ server-schema.ts
1535
+ initDisputeCasesSchema(db);
1536
+ // 公开判例评论 server-schema.ts;anonymous ALTER + idx_dcom_case 刻意留原位
1537
+ initDisputeCommentsSchema(db);
1990
1538
  try {
1991
1539
  db.exec('ALTER TABLE dispute_comments ADD COLUMN anonymous INTEGER DEFAULT 0');
1992
1540
  }
@@ -1995,27 +1543,8 @@ try {
1995
1543
  db.exec('CREATE INDEX IF NOT EXISTS idx_dcom_case ON dispute_comments(case_id, created_at DESC)');
1996
1544
  }
1997
1545
  catch { }
1998
- // W5 仲裁公开评论楼中楼 — 单层子回复;保留原 dispute_comments UNIQUE
1999
- db.exec(`
2000
- CREATE TABLE IF NOT EXISTS dispute_comment_replies (
2001
- id TEXT PRIMARY KEY, -- drep_xxx
2002
- parent_comment_id TEXT NOT NULL, -- 指向 dispute_comments.id
2003
- case_id TEXT NOT NULL,
2004
- replier_id TEXT NOT NULL,
2005
- body TEXT NOT NULL,
2006
- anonymous INTEGER DEFAULT 0,
2007
- likes INTEGER DEFAULT 0,
2008
- created_at TEXT DEFAULT (datetime('now'))
2009
- )
2010
- `);
2011
- try {
2012
- db.exec('CREATE INDEX IF NOT EXISTS idx_drep_parent ON dispute_comment_replies(parent_comment_id, created_at)');
2013
- }
2014
- catch { }
2015
- try {
2016
- db.exec('CREATE INDEX IF NOT EXISTS idx_drep_case ON dispute_comment_replies(case_id, created_at DESC)');
2017
- }
2018
- catch { }
1546
+ // W5 仲裁公开评论楼中楼 — 单层子回复 server-schema.ts;后续 ALTER 刻意留原位
1547
+ initDisputeCommentRepliesSchema(db);
2019
1548
  // 跨窗反诈一致性
2020
1549
  try {
2021
1550
  db.exec('ALTER TABLE dispute_comment_replies ADD COLUMN flagged INTEGER DEFAULT 0');
@@ -2030,68 +1559,21 @@ try {
2030
1559
  db.exec('ALTER TABLE dispute_comments ADD COLUMN flag_reasons TEXT');
2031
1560
  }
2032
1561
  catch { }
2033
- // W6 笔记评论 — 原生 parent_id 楼中楼(仅 1 层;非 root 不可再 reply)
2034
- db.exec(`
2035
- CREATE TABLE IF NOT EXISTS shareable_comments (
2036
- id TEXT PRIMARY KEY, -- scom_xxx
2037
- shareable_id TEXT NOT NULL, -- shareables.id
2038
- commenter_id TEXT NOT NULL,
2039
- parent_id TEXT, -- 子评论指向父评论;root = NULL
2040
- body TEXT NOT NULL,
2041
- flagged INTEGER DEFAULT 0,
2042
- likes INTEGER DEFAULT 0,
2043
- created_at TEXT DEFAULT (datetime('now'))
2044
- )
2045
- `);
2046
- try {
2047
- db.exec('CREATE INDEX IF NOT EXISTS idx_scom_shareable ON shareable_comments(shareable_id, parent_id, created_at DESC)');
2048
- }
2049
- catch { }
1562
+ // W6 笔记评论 — 原生 parent_id 楼中楼(仅 1 层)→ server-schema.ts;flag_reasons ALTER 刻意留原位
1563
+ initShareableCommentsSchema(db);
2050
1564
  try {
2051
1565
  db.exec('ALTER TABLE shareable_comments ADD COLUMN flag_reasons TEXT');
2052
1566
  }
2053
1567
  catch { }
2054
- db.exec(`
2055
- CREATE TABLE IF NOT EXISTS dispute_fairness_votes (
2056
- case_id TEXT NOT NULL,
2057
- voter_id TEXT NOT NULL,
2058
- vote TEXT NOT NULL, -- 'yes' / 'no'
2059
- created_at TEXT DEFAULT (datetime('now')),
2060
- PRIMARY KEY (case_id, voter_id)
2061
- )
2062
- `);
1568
+ // 公开判例公平性投票 → server-schema.ts;idx_feedback_open 刻意留原位(非本表索引,不相邻)
1569
+ initDisputeFairnessVotesSchema(db);
2063
1570
  try {
2064
1571
  db.exec('CREATE INDEX IF NOT EXISTS idx_feedback_open ON feedback_tickets(status, created_at DESC) WHERE status IN (\'open\', \'in_progress\')');
2065
1572
  }
2066
1573
  catch { }
2067
- // Wave C-3: 买家评价 / 评分 完成订单后给卖家 1-5 + 文字
2068
- db.exec(`
2069
- CREATE TABLE IF NOT EXISTS order_ratings (
2070
- order_id TEXT PRIMARY KEY,
2071
- buyer_id TEXT NOT NULL,
2072
- seller_id TEXT NOT NULL,
2073
- product_id TEXT NOT NULL,
2074
- stars INTEGER NOT NULL, -- 1-5
2075
- comment TEXT,
2076
- reply TEXT, -- seller 可回复
2077
- replied_at TEXT,
2078
- created_at TEXT DEFAULT (datetime('now'))
2079
- )
2080
- `);
2081
- try {
2082
- db.exec('CREATE INDEX IF NOT EXISTS idx_rating_seller ON order_ratings(seller_id, created_at DESC)');
2083
- }
2084
- catch { }
2085
- try {
2086
- db.exec('CREATE INDEX IF NOT EXISTS idx_rating_product ON order_ratings(product_id, created_at DESC)');
2087
- }
2088
- catch { }
2089
- // P2 hot-path:覆盖 recommend_count 子查询(COUNT DISTINCT buyer_id WHERE product_id=? AND stars>=4)
2090
- try {
2091
- db.exec('CREATE INDEX IF NOT EXISTS idx_rating_recommend ON order_ratings(product_id, stars, buyer_id)');
2092
- }
2093
- catch { }
2094
- // P2 hot-path:覆盖 sales_count 子查询(COUNT WHERE product_id=? AND status=completed)
1574
+ // Wave C-3: 买家评价 / 评分 server-schema.ts;后续结构化维度 ALTER + 跨表 orders 索引刻意留原位
1575
+ initOrderRatingsSchema(db);
1576
+ // P2 hot-path:覆盖 sales_count 子查询(COUNT WHERE product_id=? AND status=completed)—— orders 表索引,留原位
2095
1577
  try {
2096
1578
  db.exec('CREATE INDEX IF NOT EXISTS idx_orders_product_status ON orders(product_id, status)');
2097
1579
  }
@@ -2122,325 +1604,104 @@ try {
2122
1604
  db.exec('ALTER TABLE order_ratings ADD COLUMN buyer_followup_at TEXT');
2123
1605
  }
2124
1606
  catch { }
2125
- db.exec(`
2126
- CREATE TABLE IF NOT EXISTS buyer_ratings (
2127
- order_id TEXT PRIMARY KEY,
2128
- seller_id TEXT NOT NULL,
2129
- buyer_id TEXT NOT NULL,
2130
- stars INTEGER NOT NULL,
2131
- comment TEXT,
2132
- dim_payment_speed INTEGER,
2133
- dim_communication INTEGER,
2134
- dim_responsiveness INTEGER,
2135
- hidden_until TEXT,
2136
- created_at TEXT DEFAULT (datetime('now'))
2137
- )
2138
- `);
2139
- try {
2140
- db.exec('CREATE INDEX IF NOT EXISTS idx_buyer_ratings_buyer ON buyer_ratings(buyer_id, created_at DESC)');
2141
- }
2142
- catch { }
2143
- // Wave C-2: 多收货地址簿 — buyer 保存常用地址,下单时选择默认地址
2144
- db.exec(`
2145
- CREATE TABLE IF NOT EXISTS user_addresses (
2146
- id TEXT PRIMARY KEY,
2147
- user_id TEXT NOT NULL,
2148
- label TEXT NOT NULL, -- 标签(家 / 公司 / 父母家)
2149
- recipient TEXT NOT NULL,
2150
- phone TEXT,
2151
- region TEXT, -- 省/市/区
2152
- detail TEXT NOT NULL, -- 详细地址
2153
- is_default INTEGER DEFAULT 0,
2154
- created_at TEXT DEFAULT (datetime('now')),
2155
- updated_at TEXT DEFAULT (datetime('now'))
2156
- )
2157
- `);
2158
- try {
2159
- db.exec('CREATE INDEX IF NOT EXISTS idx_addr_user ON user_addresses(user_id, is_default DESC)');
2160
- }
2161
- catch { }
2162
- // Wave B-3: 退货请求 — 买家在 return_days 窗口内可发起,卖家可接受/拒绝;拒绝后可走 dispute
2163
- db.exec(`
2164
- CREATE TABLE IF NOT EXISTS return_requests (
2165
- id TEXT PRIMARY KEY,
2166
- order_id TEXT NOT NULL,
2167
- buyer_id TEXT NOT NULL,
2168
- seller_id TEXT NOT NULL,
2169
- product_id TEXT NOT NULL,
2170
- reason TEXT NOT NULL, -- 'quality' | 'wrong_item' | 'damaged' | 'no_longer_needed' | 'other'
2171
- reason_text TEXT,
2172
- refund_amount DECIMAL(18,2), -- 默认 = order.total_amount
2173
- status TEXT NOT NULL DEFAULT 'pending', -- pending | accepted | rejected | refunded | escalated | cancelled
2174
- seller_response TEXT,
2175
- escalated_dispute_id TEXT,
2176
- created_at TEXT DEFAULT (datetime('now')),
2177
- resolved_at TEXT
2178
- )
2179
- `);
2180
- try {
2181
- db.exec('CREATE INDEX IF NOT EXISTS idx_returns_seller_pending ON return_requests(seller_id, status) WHERE status = \'pending\'');
2182
- }
2183
- catch { }
2184
- try {
2185
- db.exec('CREATE INDEX IF NOT EXISTS idx_returns_buyer ON return_requests(buyer_id, created_at)');
2186
- }
2187
- catch { }
2188
- try {
2189
- db.exec('CREATE INDEX IF NOT EXISTS idx_returns_order ON return_requests(order_id)');
2190
- }
2191
- catch { }
1607
+ // 反向评价:卖家给买家评分(双盲)→ server-schema.ts
1608
+ initBuyerRatingsSchema(db);
1609
+ // Wave C-2: 多收货地址簿 → server-schema.ts
1610
+ initUserAddressesSchema(db);
1611
+ // Wave B-3: 退货请求 → server-schema.ts;pickup ALTER 刻意留原位(紧跟下方)
1612
+ initReturnRequestsSchema(db);
2192
1613
  // 2026-05-22 L3+B3:退货上门取件(MVP — 仅声明阶段)
2193
1614
  // 完整状态机(accepted_pickup_pending → picked_up → refunded)留 Phase 2
2194
1615
  try {
2195
- db.exec('ALTER TABLE return_requests ADD COLUMN pickup_requested INTEGER DEFAULT 0');
2196
- }
2197
- catch { }
2198
- try {
2199
- db.exec('ALTER TABLE return_requests ADD COLUMN pickup_address TEXT');
2200
- }
2201
- catch { }
2202
- // W2 售后协商时间线 — 多轮消息(buyer ↔ seller)
2203
- db.exec(`
2204
- CREATE TABLE IF NOT EXISTS return_messages (
2205
- id TEXT PRIMARY KEY, -- rmsg_xxx
2206
- return_id TEXT NOT NULL,
2207
- sender_id TEXT NOT NULL,
2208
- sender_role TEXT NOT NULL, -- 'buyer' | 'seller' | 'system'
2209
- body TEXT NOT NULL,
2210
- created_at TEXT DEFAULT (datetime('now'))
2211
- )
2212
- `);
2213
- try {
2214
- db.exec('CREATE INDEX IF NOT EXISTS idx_rmsg_return ON return_messages(return_id, created_at)');
2215
- }
2216
- catch { }
2217
- // 跨窗反诈一致性
2218
- try {
2219
- db.exec('ALTER TABLE return_messages ADD COLUMN flagged INTEGER DEFAULT 0');
2220
- }
2221
- catch { }
2222
- try {
2223
- db.exec('ALTER TABLE return_messages ADD COLUMN flag_reasons TEXT');
2224
- }
2225
- catch { }
2226
- // Wave B-1 Phase 1: 商品 variants(同款多 SKU — 颜色/尺寸/规格组合)
2227
- // schema + CRUD 端点;Phase 2 再集成订单/购物车
2228
- db.exec(`
2229
- CREATE TABLE IF NOT EXISTS product_variants (
2230
- id TEXT PRIMARY KEY,
2231
- product_id TEXT NOT NULL,
2232
- sku TEXT, -- 卖家内部 SKU 编号(可选)
2233
- options_json TEXT NOT NULL, -- {"颜色":"红","尺寸":"L"} 必填
2234
- price_override REAL, -- null = 用 product.price
2235
- stock INTEGER DEFAULT 0,
2236
- images_json TEXT, -- variant 专属图(可选,null = 用 product.images)
2237
- is_active INTEGER DEFAULT 1,
2238
- created_at TEXT DEFAULT (datetime('now')),
2239
- updated_at TEXT DEFAULT (datetime('now'))
2240
- )
2241
- `);
2242
- try {
2243
- db.exec('CREATE INDEX IF NOT EXISTS idx_pv_product ON product_variants(product_id, is_active)');
2244
- }
2245
- catch { }
2246
- // 给 products 加 has_variants 标记(避免每次查 join 检查)
2247
- try {
2248
- db.exec('ALTER TABLE products ADD COLUMN has_variants INTEGER DEFAULT 0');
2249
- }
2250
- catch { }
2251
- // P2-1: variants 唯一性 — options 的 canonical key (sorted keys) 用于防止同 product 内重复 SKU
2252
- try {
2253
- db.exec('ALTER TABLE product_variants ADD COLUMN options_key TEXT');
2254
- }
2255
- catch { }
2256
- // 回填历史行的 options_key(一次性,新行在 POST/PATCH 时写入)
2257
- try {
2258
- const rows = db.prepare('SELECT id, options_json FROM product_variants WHERE options_key IS NULL').all();
2259
- const upd = db.prepare('UPDATE product_variants SET options_key = ? WHERE id = ?');
2260
- for (const r of rows) {
2261
- try {
2262
- const obj = JSON.parse(r.options_json || '{}');
2263
- const key = Object.keys(obj).sort().map(k => `${k}=${String(obj[k])}`).join('|');
2264
- upd.run(key, r.id);
2265
- }
2266
- catch { }
2267
- }
2268
- }
2269
- catch { }
2270
- // 唯一性索引(同 product + 同 options 组合不可有两条 active 行)
2271
- try {
2272
- db.exec('CREATE UNIQUE INDEX IF NOT EXISTS uniq_pv_product_options ON product_variants(product_id, options_key) WHERE is_active = 1');
2273
- }
2274
- catch { }
2275
- // Sprint 4 — claim_loss_count (历史被验证不实次数 — 用于搜索降权 + 公开 badge)
2276
- for (const stmt of [
2277
- 'ALTER TABLE products ADD COLUMN claim_loss_count INTEGER DEFAULT 0',
2278
- 'ALTER TABLE secondhand_items ADD COLUMN claim_loss_count INTEGER DEFAULT 0',
2279
- 'ALTER TABLE auctions ADD COLUMN claim_loss_count INTEGER DEFAULT 0',
2280
- ]) {
2281
- try {
2282
- db.exec(stmt);
2283
- }
2284
- catch { }
2285
- }
2286
- // P2P 原生商店 — 卖家本地节点存详情,WebAZ 只锚定 hash + 关键字段
2287
- for (const stmt of [
2288
- 'ALTER TABLE products ADD COLUMN p2p_mode INTEGER DEFAULT 0',
2289
- 'ALTER TABLE products ADD COLUMN content_hash TEXT', // sha256 of canonical detail JSON
2290
- 'ALTER TABLE products ADD COLUMN peer_endpoint TEXT', // WS/HTTPS 拉取点
2291
- 'ALTER TABLE products ADD COLUMN content_signature TEXT', // HMAC(api_key, hash + signed_at)
2292
- 'ALTER TABLE products ADD COLUMN content_signed_at TEXT',
2293
- 'ALTER TABLE orders ADD COLUMN content_hash_at_order TEXT', // 下单时锚定的 hash 版本(争议证据)
2294
- ]) {
2295
- try {
2296
- db.exec(stmt);
2297
- }
2298
- catch { }
2299
- }
2300
- try {
2301
- db.exec('CREATE INDEX IF NOT EXISTS idx_products_p2p ON products(p2p_mode, status)');
2302
- }
2303
- catch { }
2304
- db.exec(`
2305
- CREATE TABLE IF NOT EXISTS p2p_shops (
2306
- id TEXT PRIMARY KEY,
2307
- owner_id TEXT NOT NULL,
2308
- name TEXT NOT NULL,
2309
- description TEXT,
2310
- thumbnail_uri TEXT,
2311
- peer_endpoint TEXT,
2312
- peer_pubkey TEXT,
2313
- status TEXT NOT NULL DEFAULT 'active',
2314
- created_at TEXT DEFAULT (datetime('now')),
2315
- updated_at TEXT DEFAULT (datetime('now'))
2316
- )
2317
- `);
2318
- try {
2319
- db.exec('CREATE INDEX IF NOT EXISTS idx_p2p_shops_owner ON p2p_shops(owner_id, status)');
2320
- }
2321
- catch { }
2322
- db.exec(`
2323
- CREATE TABLE IF NOT EXISTS shareable_likes (
2324
- id TEXT PRIMARY KEY,
2325
- shareable_id TEXT NOT NULL,
2326
- user_id TEXT NOT NULL,
2327
- created_at TEXT DEFAULT (datetime('now')),
2328
- UNIQUE(shareable_id, user_id)
2329
- )
2330
- `);
2331
- try {
2332
- db.exec('CREATE INDEX IF NOT EXISTS idx_shr_likes_shr ON shareable_likes(shareable_id)');
2333
- }
2334
- catch { }
2335
- try {
2336
- db.exec('CREATE INDEX IF NOT EXISTS idx_shr_likes_user ON shareable_likes(user_id, created_at DESC)');
2337
- }
2338
- catch { }
2339
- // 2026-05-22 audit P2:收藏功能(小红书风格"收藏" tab)
2340
- db.exec(`
2341
- CREATE TABLE IF NOT EXISTS shareable_bookmarks (
2342
- id TEXT PRIMARY KEY,
2343
- shareable_id TEXT NOT NULL,
2344
- user_id TEXT NOT NULL,
2345
- created_at TEXT DEFAULT (datetime('now')),
2346
- UNIQUE(shareable_id, user_id)
2347
- )
2348
- `);
2349
- try {
2350
- db.exec('CREATE INDEX IF NOT EXISTS idx_shr_bm_user ON shareable_bookmarks(user_id, created_at DESC)');
2351
- }
2352
- catch { }
2353
- try {
2354
- db.exec('CREATE INDEX IF NOT EXISTS idx_shr_bm_shr ON shareable_bookmarks(shareable_id)');
2355
- }
2356
- catch { }
2357
- // 2026-05-22 audit P1 backlog:# 话题/标签系统(小红书风格内容分发)
2358
- db.exec(`
2359
- CREATE TABLE IF NOT EXISTS shareable_tags (
2360
- id INTEGER PRIMARY KEY AUTOINCREMENT,
2361
- shareable_id TEXT NOT NULL,
2362
- tag TEXT NOT NULL, -- 已 lowercase + trim,最长 30 字符
2363
- created_at TEXT DEFAULT (datetime('now')),
2364
- UNIQUE(shareable_id, tag)
2365
- )
2366
- `);
1616
+ db.exec('ALTER TABLE return_requests ADD COLUMN pickup_requested INTEGER DEFAULT 0');
1617
+ }
1618
+ catch { }
2367
1619
  try {
2368
- db.exec('CREATE INDEX IF NOT EXISTS idx_shr_tags_tag ON shareable_tags(tag, created_at DESC)');
1620
+ db.exec('ALTER TABLE return_requests ADD COLUMN pickup_address TEXT');
2369
1621
  }
2370
1622
  catch { }
1623
+ // W2 售后协商时间线 — 多轮消息(buyer ↔ seller)→ server-schema.ts;flagged/flag_reasons ALTER 刻意留原位(紧跟下方)
1624
+ initReturnMessagesSchema(db);
1625
+ // 跨窗反诈一致性
2371
1626
  try {
2372
- db.exec('CREATE INDEX IF NOT EXISTS idx_shr_tags_shr ON shareable_tags(shareable_id)');
1627
+ db.exec('ALTER TABLE return_messages ADD COLUMN flagged INTEGER DEFAULT 0');
2373
1628
  }
2374
1629
  catch { }
2375
- // manifest_registry = 原生 P2P 内容索引(仅 hash + 签名 + 元数据;字节在用户设备)
2376
- db.exec(`
2377
- CREATE TABLE IF NOT EXISTS manifest_registry (
2378
- hash TEXT PRIMARY KEY,
2379
- owner_id TEXT NOT NULL,
2380
- content_type TEXT NOT NULL,
2381
- byte_size INTEGER NOT NULL,
2382
- title TEXT,
2383
- description TEXT,
2384
- thumbnail_data_uri TEXT,
2385
- signature TEXT NOT NULL,
2386
- signed_at TEXT NOT NULL,
2387
- related_product_id TEXT,
2388
- related_anchor TEXT,
2389
- status TEXT DEFAULT 'active',
2390
- takedown_reason TEXT,
2391
- takedown_at TEXT,
2392
- takedown_by TEXT,
2393
- created_at TEXT DEFAULT (datetime('now'))
2394
- )
2395
- `);
2396
1630
  try {
2397
- db.exec("CREATE INDEX IF NOT EXISTS idx_mfst_owner ON manifest_registry(owner_id, status)");
1631
+ db.exec('ALTER TABLE return_messages ADD COLUMN flag_reasons TEXT');
2398
1632
  }
2399
1633
  catch { }
1634
+ // Wave B-1 Phase 1: 商品 variants(同款多 SKU — 颜色/尺寸/规格组合)
1635
+ // schema + CRUD 端点;Phase 2 再集成订单/购物车
1636
+ // Wave B-1: 商品 variants → server-schema.ts;has_variants/options_key ALTER + 回填 + uniq 索引刻意留原位(紧跟下方)
1637
+ initProductVariantsSchema(db);
1638
+ // 给 products 加 has_variants 标记(避免每次查 join 检查)
2400
1639
  try {
2401
- db.exec("CREATE INDEX IF NOT EXISTS idx_mfst_product ON manifest_registry(related_product_id, status)");
1640
+ db.exec('ALTER TABLE products ADD COLUMN has_variants INTEGER DEFAULT 0');
2402
1641
  }
2403
1642
  catch { }
1643
+ // P2-1: variants 唯一性 — options 的 canonical key (sorted keys) 用于防止同 product 内重复 SKU
2404
1644
  try {
2405
- db.exec("CREATE INDEX IF NOT EXISTS idx_mfst_anchor ON manifest_registry(related_anchor, status)");
1645
+ db.exec('ALTER TABLE product_variants ADD COLUMN options_key TEXT');
2406
1646
  }
2407
1647
  catch { }
2408
- // peer_directory = 在线 peer 注册(哪些 user 持有哪些 hash 的 cache,heartbeat 5min 失效)
2409
- db.exec(`
2410
- CREATE TABLE IF NOT EXISTS peer_directory (
2411
- peer_id TEXT NOT NULL,
2412
- manifest_hash TEXT NOT NULL,
2413
- is_owner INTEGER DEFAULT 0,
2414
- pin_intent INTEGER DEFAULT 0,
2415
- last_heartbeat TEXT NOT NULL,
2416
- bytes_served_total INTEGER DEFAULT 0,
2417
- PRIMARY KEY (peer_id, manifest_hash)
2418
- )
2419
- `);
1648
+ // 回填历史行的 options_key(一次性,新行在 POST/PATCH 时写入)
2420
1649
  try {
2421
- db.exec("CREATE INDEX IF NOT EXISTS idx_peer_hash ON peer_directory(manifest_hash, last_heartbeat DESC)");
1650
+ const rows = db.prepare('SELECT id, options_json FROM product_variants WHERE options_key IS NULL').all();
1651
+ const upd = db.prepare('UPDATE product_variants SET options_key = ? WHERE id = ?');
1652
+ for (const r of rows) {
1653
+ try {
1654
+ const obj = JSON.parse(r.options_json || '{}');
1655
+ const key = Object.keys(obj).sort().map(k => `${k}=${String(obj[k])}`).join('|');
1656
+ upd.run(key, r.id);
1657
+ }
1658
+ catch { }
1659
+ }
2422
1660
  }
2423
1661
  catch { }
1662
+ // 唯一性索引(同 product + 同 options 组合不可有两条 active 行)
2424
1663
  try {
2425
- db.exec("CREATE INDEX IF NOT EXISTS idx_peer_heartbeat ON peer_directory(last_heartbeat)");
1664
+ db.exec('CREATE UNIQUE INDEX IF NOT EXISTS uniq_pv_product_options ON product_variants(product_id, options_key) WHERE is_active = 1');
2426
1665
  }
2427
1666
  catch { }
2428
- // signaling_queue = WebRTC SDP/ICE 中继(TTL 2min,cron 清理)
2429
- db.exec(`
2430
- CREATE TABLE IF NOT EXISTS signaling_queue (
2431
- id TEXT PRIMARY KEY,
2432
- to_peer_id TEXT NOT NULL,
2433
- from_peer_id TEXT NOT NULL,
2434
- signal_type TEXT NOT NULL,
2435
- signal_data TEXT NOT NULL,
2436
- created_at TEXT NOT NULL,
2437
- delivered_at TEXT
2438
- )
2439
- `);
1667
+ // Sprint 4 claim_loss_count (历史被验证不实次数 用于搜索降权 + 公开 badge)
1668
+ for (const stmt of [
1669
+ 'ALTER TABLE products ADD COLUMN claim_loss_count INTEGER DEFAULT 0',
1670
+ 'ALTER TABLE secondhand_items ADD COLUMN claim_loss_count INTEGER DEFAULT 0',
1671
+ 'ALTER TABLE auctions ADD COLUMN claim_loss_count INTEGER DEFAULT 0',
1672
+ ]) {
1673
+ try {
1674
+ db.exec(stmt);
1675
+ }
1676
+ catch { }
1677
+ }
1678
+ // P2P 原生商店 — 卖家本地节点存详情,WebAZ 只锚定 hash + 关键字段
1679
+ for (const stmt of [
1680
+ 'ALTER TABLE products ADD COLUMN p2p_mode INTEGER DEFAULT 0',
1681
+ 'ALTER TABLE products ADD COLUMN content_hash TEXT', // sha256 of canonical detail JSON
1682
+ 'ALTER TABLE products ADD COLUMN peer_endpoint TEXT', // WS/HTTPS 拉取点
1683
+ 'ALTER TABLE products ADD COLUMN content_signature TEXT', // HMAC(api_key, hash + signed_at)
1684
+ 'ALTER TABLE products ADD COLUMN content_signed_at TEXT',
1685
+ 'ALTER TABLE orders ADD COLUMN content_hash_at_order TEXT', // 下单时锚定的 hash 版本(争议证据)
1686
+ ]) {
1687
+ try {
1688
+ db.exec(stmt);
1689
+ }
1690
+ catch { }
1691
+ }
2440
1692
  try {
2441
- db.exec("CREATE INDEX IF NOT EXISTS idx_sig_to ON signaling_queue(to_peer_id, delivered_at)");
1693
+ db.exec('CREATE INDEX IF NOT EXISTS idx_products_p2p ON products(p2p_mode, status)');
2442
1694
  }
2443
1695
  catch { }
1696
+ // P2P 店铺 / 笔记点赞·收藏·标签 / manifest·peer·signaling(原生 P2P 内容层)→ server-schema.ts
1697
+ // 纯幂等建表/建索引 DDL,原位调用保持 boot 顺序不变
1698
+ initP2pShopsSchema(db);
1699
+ initShareableLikesSchema(db);
1700
+ initShareableBookmarksSchema(db);
1701
+ initShareableTagsSchema(db);
1702
+ initManifestRegistrySchema(db);
1703
+ initPeerDirectorySchema(db);
1704
+ initSignalingQueueSchema(db);
2444
1705
  // pin_receipts = 服务证明(pinner 给 recipient 传输 N bytes 时双签的回执)
2445
1706
  // recipient 之后下单同商品 → settlePinRewards 从 basin 拨 0.5% 订单额分给最近 5 个 pinners
2446
1707
  db.exec(`
@@ -3188,56 +2449,10 @@ try {
3188
2449
  db.exec('CREATE INDEX IF NOT EXISTS idx_aucbids_buyer ON auction_bids(buyer_id, status, submitted_at DESC)');
3189
2450
  }
3190
2451
  catch { }
3191
- // CHAT — 上下文绑定聊天(order / rfq / listing_qa
3192
- db.exec(`
3193
- CREATE TABLE IF NOT EXISTS conversations (
3194
- id TEXT PRIMARY KEY,
3195
- kind TEXT NOT NULL,
3196
- context_id TEXT NOT NULL,
3197
- user_a TEXT NOT NULL,
3198
- user_b TEXT NOT NULL,
3199
- last_message_at TEXT,
3200
- last_preview TEXT,
3201
- unread_a INTEGER NOT NULL DEFAULT 0,
3202
- unread_b INTEGER NOT NULL DEFAULT 0,
3203
- status TEXT NOT NULL DEFAULT 'active',
3204
- created_at TEXT DEFAULT (datetime('now')),
3205
- UNIQUE(kind, context_id, user_a, user_b)
3206
- )
3207
- `);
3208
- try {
3209
- db.exec('CREATE INDEX IF NOT EXISTS idx_conv_a ON conversations(user_a, last_message_at DESC)');
3210
- }
3211
- catch { }
3212
- try {
3213
- db.exec('CREATE INDEX IF NOT EXISTS idx_conv_b ON conversations(user_b, last_message_at DESC)');
3214
- }
3215
- catch { }
3216
- try {
3217
- db.exec('CREATE INDEX IF NOT EXISTS idx_conv_ctx ON conversations(kind, context_id)');
3218
- }
3219
- catch { }
3220
- db.exec(`
3221
- CREATE TABLE IF NOT EXISTS messages (
3222
- id TEXT PRIMARY KEY,
3223
- conversation_id TEXT NOT NULL,
3224
- sender_id TEXT NOT NULL,
3225
- body TEXT NOT NULL DEFAULT '',
3226
- attachments TEXT,
3227
- flagged INTEGER NOT NULL DEFAULT 0,
3228
- flag_reasons TEXT,
3229
- read_at TEXT,
3230
- created_at TEXT DEFAULT (datetime('now'))
3231
- )
3232
- `);
3233
- try {
3234
- db.exec('CREATE INDEX IF NOT EXISTS idx_msg_conv ON messages(conversation_id, created_at)');
3235
- }
3236
- catch { }
3237
- try {
3238
- db.exec('CREATE INDEX IF NOT EXISTS idx_msg_sender ON messages(sender_id, created_at DESC)');
3239
- }
3240
- catch { }
2452
+ // CHAT — 上下文绑定聊天(order / rfq / listing_qa)→ server-schema.ts
2453
+ initConversationsSchema(db);
2454
+ // 聊天消息 server-schema.ts;kind/meta ALTER 刻意留原位(紧跟下方)
2455
+ initMessagesSchema(db);
3241
2456
  // W1 私信结构化消息:kind = 'text' | 'offer' | 'tracking';meta = JSON payload
3242
2457
  try {
3243
2458
  db.exec("ALTER TABLE messages ADD COLUMN kind TEXT DEFAULT 'text'");
@@ -3247,24 +2462,8 @@ try {
3247
2462
  db.exec('ALTER TABLE messages ADD COLUMN meta TEXT');
3248
2463
  }
3249
2464
  catch { }
3250
- // 反诈举报表(chat report → 人工审核)
3251
- db.exec(`
3252
- CREATE TABLE IF NOT EXISTS chat_reports (
3253
- id TEXT PRIMARY KEY,
3254
- conversation_id TEXT NOT NULL,
3255
- message_id TEXT,
3256
- reporter_id TEXT NOT NULL,
3257
- reported_id TEXT NOT NULL,
3258
- reason TEXT NOT NULL,
3259
- note TEXT,
3260
- status TEXT NOT NULL DEFAULT 'pending',
3261
- created_at TEXT DEFAULT (datetime('now'))
3262
- )
3263
- `);
3264
- try {
3265
- db.exec('CREATE INDEX IF NOT EXISTS idx_chatrpt_status ON chat_reports(status, created_at)');
3266
- }
3267
- catch { }
2465
+ // 反诈举报表(chat report → 人工审核)→ server-schema.ts
2466
+ initChatReportsSchema(db);
3268
2467
  // 基金池入池流水(depositToFund 审计 + 4 周历史均值数据源)
3269
2468
  db.exec(`
3270
2469
  CREATE TABLE IF NOT EXISTS fund_deposits (
@@ -3629,68 +2828,13 @@ for (const stmt of [
3629
2828
  }
3630
2829
  catch { }
3631
2830
  }
3632
- db.exec(`
3633
- CREATE TABLE IF NOT EXISTS quota_increase_applications (
3634
- id TEXT PRIMARY KEY,
3635
- user_id TEXT NOT NULL,
3636
- current_quota INTEGER,
3637
- requested_quota INTEGER,
3638
- reason TEXT,
3639
- status TEXT DEFAULT 'pending',
3640
- applied_at TEXT DEFAULT (datetime('now')),
3641
- reviewed_at TEXT,
3642
- reviewed_by TEXT,
3643
- decision_note TEXT
3644
- )
3645
- `);
3646
- try {
3647
- db.exec('CREATE INDEX IF NOT EXISTS idx_quota_apps_status ON quota_increase_applications(status)');
3648
- }
3649
- catch { }
3650
- // Verifier 申请记录
3651
- db.exec(`
3652
- CREATE TABLE IF NOT EXISTS verifier_applications (
3653
- id TEXT PRIMARY KEY,
3654
- user_id TEXT NOT NULL,
3655
- status TEXT DEFAULT 'pending',
3656
- applied_at TEXT DEFAULT (datetime('now')),
3657
- reviewed_at TEXT,
3658
- reviewed_by TEXT,
3659
- decision_note TEXT,
3660
- snapshot TEXT
3661
- )
3662
- `);
3663
- try {
3664
- db.exec('CREATE INDEX IF NOT EXISTS idx_verifier_apps_status ON verifier_applications(status)');
3665
- }
3666
- catch { }
3667
- // Arbitrator 申请 + 白名单(外部仲裁员路径 — 与 verifier 平行)
3668
- // 内部仲裁员:role='arbitrator'(admin 通过 /admin/admins 创建,自动 is_system=1)
3669
- // 外部仲裁员:role='buyer' + arbitrator_whitelist 行(buyer 申请→admin 批准)
3670
- db.exec(`
3671
- CREATE TABLE IF NOT EXISTS arbitrator_applications (
3672
- id TEXT PRIMARY KEY,
3673
- user_id TEXT NOT NULL,
3674
- status TEXT DEFAULT 'pending',
3675
- applied_at TEXT DEFAULT (datetime('now')),
3676
- reviewed_at TEXT,
3677
- reviewed_by TEXT,
3678
- decision_note TEXT,
3679
- snapshot TEXT
3680
- );
3681
- CREATE TABLE IF NOT EXISTS arbitrator_whitelist (
3682
- user_id TEXT PRIMARY KEY,
3683
- added_at TEXT DEFAULT (datetime('now')),
3684
- note TEXT,
3685
- is_system INTEGER DEFAULT 0,
3686
- granted_by TEXT,
3687
- stake_amount INTEGER DEFAULT 0
3688
- )
3689
- `);
3690
- try {
3691
- db.exec('CREATE INDEX IF NOT EXISTS idx_arb_apps_status ON arbitrator_applications(status)');
3692
- }
3693
- catch { }
2831
+ // 配额提升申请 → server-schema.ts
2832
+ initQuotaIncreaseApplicationsSchema(db);
2833
+ // Verifier 申请记录 → server-schema.ts
2834
+ initVerifierApplicationsSchema(db);
2835
+ // Arbitrator 申请 + 白名单(外部仲裁员路径)→ server-schema.ts
2836
+ // legacy 内部仲裁员 → 白名单的 migration INSERT 刻意留原位(紧跟下方)
2837
+ initArbitratorReviewSchema(db);
3694
2838
  // Migration:legacy 内部仲裁员 (role='arbitrator') → 自动加入白名单(is_system=1)
3695
2839
  try {
3696
2840
  db.prepare(`
@@ -3701,26 +2845,8 @@ try {
3701
2845
  catch (e) {
3702
2846
  console.warn('[arb migration]', e.message);
3703
2847
  }
3704
- // Verifier 申诉记录
3705
- db.exec(`
3706
- CREATE TABLE IF NOT EXISTS verifier_appeals (
3707
- id TEXT PRIMARY KEY,
3708
- user_id TEXT NOT NULL,
3709
- task_id TEXT,
3710
- submission_id TEXT,
3711
- reason TEXT NOT NULL,
3712
- evidence_urls TEXT DEFAULT '[]',
3713
- status TEXT DEFAULT 'pending',
3714
- admin_note TEXT,
3715
- reviewed_by TEXT,
3716
- reviewed_at TEXT,
3717
- created_at TEXT DEFAULT (datetime('now'))
3718
- )
3719
- `);
3720
- try {
3721
- db.exec('CREATE INDEX IF NOT EXISTS idx_verifier_appeals_status ON verifier_appeals(status)');
3722
- }
3723
- catch { }
2848
+ // Verifier 申诉记录 → server-schema.ts
2849
+ initVerifierAppealsSchema(db);
3724
2850
  // 扩展 verifier_whitelist
3725
2851
  for (const stmt of [
3726
2852
  "ALTER TABLE verifier_whitelist ADD COLUMN tier TEXT DEFAULT 'active-2'", // 旧数据兼容:当满级
@@ -3743,32 +2869,10 @@ try {
3743
2869
  db.prepare("UPDATE verifier_whitelist SET is_system = 1, tier = 'active-2', daily_quota = 9999 WHERE user_id = ?").run(INTERNAL_AUDITOR_ID);
3744
2870
  }
3745
2871
  catch { }
3746
- // 用户暂停状态(admin 管理)
3747
- db.exec(`
3748
- CREATE TABLE IF NOT EXISTS user_moderation (
3749
- user_id TEXT PRIMARY KEY,
3750
- suspended INTEGER DEFAULT 0,
3751
- reason TEXT,
3752
- suspended_by TEXT,
3753
- suspended_at TEXT
3754
- )
3755
- `);
3756
- // admin 操作审计日志
3757
- db.exec(`
3758
- CREATE TABLE IF NOT EXISTS admin_audit_log (
3759
- id TEXT PRIMARY KEY,
3760
- admin_id TEXT NOT NULL,
3761
- action TEXT NOT NULL,
3762
- target_type TEXT,
3763
- target_id TEXT,
3764
- detail TEXT,
3765
- created_at TEXT DEFAULT (datetime('now'))
3766
- )
3767
- `);
3768
- try {
3769
- db.exec('CREATE INDEX IF NOT EXISTS idx_admin_audit_log_created ON admin_audit_log(created_at)');
3770
- }
3771
- catch { }
2872
+ // 用户暂停状态(admin 管理)→ server-schema.ts
2873
+ initUserModerationSchema(db);
2874
+ // admin 操作审计日志 server-schema.ts(initAdminCoordinationSchema FK 依赖本表,须先建)
2875
+ initAdminAuditLogSchema(db);
3772
2876
  // admin/agent coordination contribution — operator-claim + agent-mandate event logs + fact-source link
3773
2877
  // (schema only). Placed HERE because it FKs users + contribution_facts (both created above) AND
3774
2878
  // admin_audit_log (created just above). No ingestion runs at boot.
@@ -3795,41 +2899,16 @@ initAdminCoordinationSchema(db);
3795
2899
  .run(JSON.stringify(roles), u.id);
3796
2900
  console.log(`[WebAZ] ✓ ${u.name} 已升级为 admin (bootstrap)`);
3797
2901
  })();
3798
- // 验证码表(邮箱绑定 / 找回密钥 / 改密码 等共用)
3799
- db.exec(`
3800
- CREATE TABLE IF NOT EXISTS verification_codes (
3801
- id TEXT PRIMARY KEY,
3802
- user_id TEXT NOT NULL,
3803
- channel TEXT NOT NULL, -- 'email' / 'phone'
3804
- target TEXT NOT NULL, -- 邮箱地址 / 手机号
3805
- code TEXT NOT NULL, -- 6 位数字
3806
- purpose TEXT NOT NULL, -- 'bind_email' / 'recover_key' / ...
3807
- attempts INTEGER DEFAULT 0,
3808
- used_at TEXT,
3809
- expires_at TEXT NOT NULL,
3810
- created_at TEXT DEFAULT (datetime('now'))
3811
- )
3812
- `);
3813
- try {
3814
- db.exec('CREATE INDEX IF NOT EXISTS idx_verification_codes_lookup ON verification_codes(channel, target, purpose)');
3815
- }
3816
- catch { }
2902
+ // 验证码表(邮箱绑定 / 找回密钥 / 改密码 等共用)→ server-schema.ts
2903
+ initVerificationCodesSchema(db);
3817
2904
  const NEW_PRODUCT_COLS = [
3818
- 'ALTER TABLE products ADD COLUMN specs TEXT',
3819
- 'ALTER TABLE products ADD COLUMN brand TEXT',
3820
- 'ALTER TABLE products ADD COLUMN model TEXT',
2905
+ // specs/brand/model/source_price/ship_regions/handling_hours/estimated_days/
2906
+ // fragile/return_days/return_condition/warranty_days moved to
2907
+ // initRegisterListSearchColumns (single source, shared w/ MCP) — see ~line 494.
3821
2908
  'ALTER TABLE products ADD COLUMN source_url TEXT',
3822
- 'ALTER TABLE products ADD COLUMN source_price REAL',
3823
2909
  'ALTER TABLE products ADD COLUMN source_price_at TEXT',
3824
2910
  'ALTER TABLE products ADD COLUMN weight_kg REAL',
3825
- 'ALTER TABLE products ADD COLUMN ship_regions TEXT DEFAULT "全国"',
3826
2911
  'ALTER TABLE products ADD COLUMN excluded_regions TEXT',
3827
- 'ALTER TABLE products ADD COLUMN handling_hours INTEGER DEFAULT 24',
3828
- 'ALTER TABLE products ADD COLUMN estimated_days TEXT',
3829
- 'ALTER TABLE products ADD COLUMN fragile INTEGER DEFAULT 0',
3830
- 'ALTER TABLE products ADD COLUMN return_days INTEGER DEFAULT 7',
3831
- 'ALTER TABLE products ADD COLUMN return_condition TEXT',
3832
- 'ALTER TABLE products ADD COLUMN warranty_days INTEGER DEFAULT 0',
3833
2912
  'ALTER TABLE products ADD COLUMN commitment_hash TEXT',
3834
2913
  'ALTER TABLE products ADD COLUMN description_hash TEXT',
3835
2914
  'ALTER TABLE products ADD COLUMN price_hash TEXT',
@@ -3891,7 +2970,7 @@ catch { }
3891
2970
  const upd = db.prepare(`UPDATE products SET
3892
2971
  last_sold_at = (SELECT MAX(COALESCE(updated_at, created_at)) FROM orders WHERE product_id = ? AND status = 'completed'),
3893
2972
  first_sold_at = (SELECT MIN(COALESCE(updated_at, created_at)) FROM orders WHERE product_id = ? AND status = 'completed'),
3894
- completion_count = (SELECT COUNT(1) FROM orders WHERE product_id = ? AND status = 'completed'),
2973
+ completion_count = (SELECT COUNT(1) FROM orders WHERE product_id = ? AND ${genuineSalePredicate('orders')}),
3895
2974
  dispute_loss_count = (
3896
2975
  SELECT COUNT(1) FROM disputes d JOIN orders o ON o.id = d.order_id
3897
2976
  WHERE o.product_id = ? AND d.ruling_type IN ('refund_buyer','partial_refund')
@@ -3933,20 +3012,12 @@ catch { }
3933
3012
  })();
3934
3013
  // ─── 里程碑 3:反操纵层 schema ─────────────────────────────────
3935
3014
  try {
3936
- db.exec(`CREATE TABLE IF NOT EXISTS shareable_click_log (
3937
- id INTEGER PRIMARY KEY AUTOINCREMENT,
3938
- shareable_id TEXT NOT NULL,
3939
- ip_hash TEXT NOT NULL,
3940
- ua_hash TEXT NOT NULL,
3941
- ref_path TEXT,
3942
- created_at TEXT DEFAULT (datetime('now'))
3943
- )`);
3944
- db.exec(`CREATE INDEX IF NOT EXISTS idx_scl_share_ts ON shareable_click_log(shareable_id, created_at)`);
3945
- db.exec(`CREATE INDEX IF NOT EXISTS idx_scl_share_ipua ON shareable_click_log(shareable_id, ip_hash, ua_hash, created_at)`);
3015
+ initShareableClickLogSchema(db);
3946
3016
  }
3947
3017
  catch (e) {
3948
3018
  console.error('[M3 schema scl]', e);
3949
3019
  }
3020
+ // shareables ALTER 刻意留原位(scl init 之后、cal init 之前)
3950
3021
  try {
3951
3022
  db.exec('ALTER TABLE shareables ADD COLUMN unique_click_count INTEGER DEFAULT 0');
3952
3023
  }
@@ -3956,137 +3027,42 @@ try {
3956
3027
  }
3957
3028
  catch { }
3958
3029
  try {
3959
- db.exec(`CREATE TABLE IF NOT EXISTS commission_audit_log (
3960
- id INTEGER PRIMARY KEY AUTOINCREMENT,
3961
- order_id TEXT,
3962
- buyer_id TEXT NOT NULL,
3963
- seller_id TEXT NOT NULL,
3964
- flag TEXT NOT NULL, -- 'sponsor_chain_cross' / 'self_in_chain'
3965
- detail TEXT, -- JSON: { relation: 'buyer_ancestor_of_seller' | ..., path: '...' }
3966
- created_at TEXT DEFAULT (datetime('now'))
3967
- )`);
3968
- db.exec(`CREATE INDEX IF NOT EXISTS idx_cal_buyer ON commission_audit_log(buyer_id, created_at)`);
3030
+ initCommissionAuditLogSchema(db);
3969
3031
  }
3970
3032
  catch (e) {
3971
3033
  console.error('[M3 schema cal]', e);
3972
3034
  }
3973
3035
  try {
3974
- db.exec(`CREATE TABLE IF NOT EXISTS registration_audit_log (
3975
- id INTEGER PRIMARY KEY AUTOINCREMENT,
3976
- user_id TEXT NOT NULL,
3977
- ip_hash TEXT NOT NULL,
3978
- ua_hash TEXT NOT NULL,
3979
- sponsor_id TEXT,
3980
- created_at TEXT DEFAULT (datetime('now'))
3981
- )`);
3982
- db.exec(`CREATE INDEX IF NOT EXISTS idx_ral_ip_ts ON registration_audit_log(ip_hash, created_at)`);
3036
+ initRegistrationAuditLogSchema(db);
3983
3037
  }
3984
3038
  catch (e) {
3985
3039
  console.error('[M3 schema ral]', e);
3986
3040
  }
3987
- // ─── 里程碑 4:Agent Reputation schema ─────────────────────────
3041
+ // ─── 里程碑 4:Agent observability/reputation schema → server-schema.ts ─
3988
3042
  try {
3989
- db.exec(`CREATE TABLE IF NOT EXISTS agent_call_log (
3990
- id INTEGER PRIMARY KEY AUTOINCREMENT,
3991
- api_key TEXT,
3992
- user_id TEXT,
3993
- endpoint TEXT NOT NULL,
3994
- method TEXT,
3995
- status_code INTEGER,
3996
- created_at TEXT DEFAULT (datetime('now'))
3997
- )`);
3998
- db.exec(`CREATE INDEX IF NOT EXISTS idx_acl_apikey_ts ON agent_call_log(api_key, created_at)`);
3999
- db.exec(`CREATE INDEX IF NOT EXISTS idx_acl_user_ts ON agent_call_log(user_id, created_at)`);
3043
+ initAgentCallLogSchema(db);
4000
3044
  }
4001
3045
  catch (e) {
4002
3046
  console.error('[M4 schema acl]', e);
4003
3047
  }
4004
3048
  try {
4005
- db.exec(`CREATE TABLE IF NOT EXISTS agent_reputation (
4006
- api_key TEXT PRIMARY KEY,
4007
- user_id TEXT NOT NULL,
4008
- trust_score REAL DEFAULT 0,
4009
- level TEXT DEFAULT 'new',
4010
- signals TEXT, -- JSON
4011
- last_calculated_at TEXT,
4012
- created_at TEXT DEFAULT (datetime('now'))
4013
- )`);
4014
- db.exec(`CREATE INDEX IF NOT EXISTS idx_ar_user ON agent_reputation(user_id)`);
3049
+ initAgentReputationSchema(db);
4015
3050
  }
4016
3051
  catch (e) {
4017
3052
  console.error('[M4 schema ar]', e);
4018
3053
  }
4019
- // ─── 2026-05-23 Agent 治理(spec: docs/AGENT-GOVERNANCE.md)─────
4020
- try {
4021
- // agent_declarations:agent 自声明(trust > new 必须先登记)
4022
- db.exec(`CREATE TABLE IF NOT EXISTS agent_declarations (
4023
- api_key TEXT PRIMARY KEY,
4024
- user_id TEXT NOT NULL,
4025
- operator_name TEXT NOT NULL, -- 公司/开发者名
4026
- operator_contact TEXT NOT NULL, -- email/handle/DID
4027
- purpose TEXT NOT NULL, -- ≤200 字
4028
- declared_scope TEXT NOT NULL, -- JSON: {roles, actions, regions}
4029
- attestations TEXT, -- JSON: {gdpr, kids_safe, no_pii_export, ...}
4030
- repo_url TEXT,
4031
- homepage TEXT,
4032
- revoked_at TEXT, -- 撤销时间(用户主动 revoke)
4033
- revoked_reason TEXT,
4034
- created_at TEXT DEFAULT (datetime('now')),
4035
- updated_at TEXT DEFAULT (datetime('now'))
4036
- )`);
4037
- db.exec(`CREATE INDEX IF NOT EXISTS idx_agd_operator ON agent_declarations(operator_name)`);
4038
- db.exec(`CREATE INDEX IF NOT EXISTS idx_agd_revoked ON agent_declarations(revoked_at) WHERE revoked_at IS NOT NULL`);
4039
- // agent_attestations:bilateral consent(用户主动同意某 agent 的 scope)
4040
- db.exec(`CREATE TABLE IF NOT EXISTS agent_attestations (
4041
- id TEXT PRIMARY KEY,
4042
- api_key TEXT NOT NULL, -- agent 的 api_key
4043
- user_id TEXT NOT NULL, -- 同意此 agent 行动的用户
4044
- approved_scope TEXT NOT NULL, -- JSON:用户实际批准的子集
4045
- spend_cap_per_order REAL, -- 该用户给此 agent 的单笔下单上限(可空 = 沿用 declared_scope)
4046
- spend_cap_daily REAL, -- 24h 累计上限
4047
- granted_at TEXT DEFAULT (datetime('now')),
4048
- revoked_at TEXT,
4049
- UNIQUE(api_key, user_id)
4050
- )`);
4051
- db.exec(`CREATE INDEX IF NOT EXISTS idx_aat_user ON agent_attestations(user_id, revoked_at)`);
4052
- // agent_strikes:违规累积(3-strike state machine)
4053
- db.exec(`CREATE TABLE IF NOT EXISTS agent_strikes (
4054
- id INTEGER PRIMARY KEY AUTOINCREMENT,
4055
- api_key TEXT NOT NULL,
4056
- user_id TEXT NOT NULL,
4057
- severity TEXT NOT NULL, -- 'warning' | 'suspend_7d' | 'permanent'
4058
- reason_code TEXT NOT NULL, -- 'fake_shipment' | 'mass_spam' | 'overlimit_order' | 'fraud_claim' | ...
4059
- reason_detail TEXT,
4060
- reported_by TEXT, -- user_id(举报人 / system / admin)
4061
- related_ref TEXT, -- 关联 order/dispute/claim_task id
4062
- issued_at TEXT DEFAULT (datetime('now')),
4063
- expires_at TEXT, -- warning=24h; suspend_7d=7d; permanent=null
4064
- appeal_status TEXT DEFAULT 'none', -- 'none' | 'pending' | 'approved' | 'denied'
4065
- appeal_reason TEXT,
4066
- appeal_decided_by TEXT,
4067
- appeal_decided_at TEXT
4068
- )`);
4069
- db.exec(`CREATE INDEX IF NOT EXISTS idx_ast_apikey ON agent_strikes(api_key, issued_at DESC)`);
4070
- db.exec(`CREATE INDEX IF NOT EXISTS idx_ast_user ON agent_strikes(user_id, issued_at DESC)`);
4071
- // 注:SQLite 不允许 partial index 用非确定性函数 (datetime('now'));用 expires_at 普通索引代替
4072
- db.exec(`CREATE INDEX IF NOT EXISTS idx_ast_active ON agent_strikes(api_key, expires_at)`);
4073
- // 2026-05-23 P1 fix 5.3:skills 加 disabled_by_strike_at(被 strike 自动停用后可恢复)
3054
+ // ─── 2026-05-23 Agent 治理(spec: docs/AGENT-GOVERNANCE.md)→ server-schema.ts ─
3055
+ // 顺序须保持:declarations → attestations → strikes →(ALTER skills 留原位)→ revocations
3056
+ try {
3057
+ initAgentDeclarationsSchema(db);
3058
+ initAgentAttestationsSchema(db);
3059
+ initAgentStrikesSchema(db);
3060
+ // 2026-05-23 P1 fix 5.3:skills 加 disabled_by_strike_at(被 strike 自动停用后可恢复)—— 留 server.ts 原位
4074
3061
  try {
4075
3062
  db.exec(`ALTER TABLE skills ADD COLUMN disabled_by_strike_at TEXT`);
4076
3063
  }
4077
3064
  catch { }
4078
- // agent_revocations:operator-级撤销(封禁同 operator 名下所有 agent)
4079
- db.exec(`CREATE TABLE IF NOT EXISTS agent_revocations (
4080
- id INTEGER PRIMARY KEY AUTOINCREMENT,
4081
- target_kind TEXT NOT NULL, -- 'api_key' | 'operator_name'
4082
- target_value TEXT NOT NULL,
4083
- revoked_by TEXT NOT NULL, -- user_id(用户自己 OR root admin)
4084
- revoked_by_role TEXT, -- 'self' | 'admin'
4085
- reason TEXT,
4086
- revoked_at TEXT DEFAULT (datetime('now')),
4087
- UNIQUE(target_kind, target_value, revoked_by)
4088
- )`);
4089
- db.exec(`CREATE INDEX IF NOT EXISTS idx_arev_target ON agent_revocations(target_kind, target_value)`);
3065
+ initAgentRevocationsSchema(db);
4090
3066
  }
4091
3067
  catch (e) {
4092
3068
  console.error('[agent_governance schema]', e);
@@ -4095,73 +3071,22 @@ catch (e) {
4095
3071
  // 协议级精准匹配:卖家声明该 SKU 的多种 alias(外部 id / 标题 / 短链 / 淘口令 token / 标题片段)
4096
3072
  // 服务端用 findProductsByAlias 做"完全相等 + 包含"判定。alias 至少 6 字符,反通用词。
4097
3073
  try {
4098
- db.exec(`CREATE TABLE IF NOT EXISTS product_aliases (
4099
- id TEXT PRIMARY KEY,
4100
- product_id TEXT NOT NULL,
4101
- alias_type TEXT NOT NULL, -- 'external_id' | 'external_title' | 'short_url' | 'kouling_token' | 'title_substring'
4102
- alias_value TEXT NOT NULL,
4103
- min_match_chars INTEGER DEFAULT 6,
4104
- created_at TEXT DEFAULT (datetime('now')),
4105
- challenged_at TEXT, -- M7.4 verifier 挑战时间
4106
- status TEXT DEFAULT 'active', -- 'active' | 'revoked' | 'challenged'
4107
- UNIQUE(alias_type, alias_value, product_id)
4108
- )`);
4109
- db.exec(`CREATE INDEX IF NOT EXISTS idx_alias_value ON product_aliases(alias_value)`);
4110
- db.exec(`CREATE INDEX IF NOT EXISTS idx_alias_product ON product_aliases(product_id)`);
4111
- db.exec(`CREATE INDEX IF NOT EXISTS idx_alias_type ON product_aliases(alias_type)`);
3074
+ initProductAliasesSchema(db);
4112
3075
  }
4113
3076
  catch (e) {
4114
3077
  console.error('[M7.2 schema product_aliases]', e);
4115
3078
  }
4116
- // M-5:region 切换 audit log + 24h 限流
3079
+ // M-5:region 切换 audit log + 24h 限流 → server-schema.ts
4117
3080
  try {
4118
- db.exec(`CREATE TABLE IF NOT EXISTS region_change_log (
4119
- id TEXT PRIMARY KEY,
4120
- user_id TEXT NOT NULL,
4121
- from_region TEXT,
4122
- to_region TEXT NOT NULL,
4123
- ip TEXT,
4124
- created_at TEXT DEFAULT (datetime('now'))
4125
- )`);
4126
- db.exec(`CREATE INDEX IF NOT EXISTS idx_region_change_user_ts ON region_change_log(user_id, created_at DESC)`);
3081
+ initRegionChangeLogSchema(db);
4127
3082
  }
4128
3083
  catch (e) {
4129
3084
  console.error('[M-5 schema region_change_log]', e);
4130
3085
  }
4131
- // WebAuthn / Passkey — 大额提现 等敏感操作的二次确认(commit B
3086
+ // WebAuthn / Passkey — 大额提现 等敏感操作的二次确认(commit B)→ server-schema.ts
3087
+ // users.webauthn_required_for_withdraw ALTER 刻意留原位(init 后、同一 try 内)
4132
3088
  try {
4133
- db.exec(`CREATE TABLE IF NOT EXISTS webauthn_credentials (
4134
- id TEXT PRIMARY KEY, -- credential.id (base64url)
4135
- user_id TEXT NOT NULL,
4136
- public_key BLOB NOT NULL, -- COSE public key
4137
- counter INTEGER NOT NULL DEFAULT 0,
4138
- transports TEXT, -- JSON array
4139
- device_label TEXT, -- user-friendly label
4140
- created_at TEXT DEFAULT (datetime('now')),
4141
- last_used_at TEXT
4142
- )`);
4143
- db.exec(`CREATE INDEX IF NOT EXISTS idx_wac_user ON webauthn_credentials(user_id)`);
4144
- db.exec(`CREATE TABLE IF NOT EXISTS webauthn_challenges (
4145
- id TEXT PRIMARY KEY,
4146
- user_id TEXT NOT NULL,
4147
- challenge TEXT NOT NULL,
4148
- purpose TEXT NOT NULL, -- 'register' | 'withdraw' | 'change-password' | 'reveal-key' | 'region'
4149
- purpose_data TEXT, -- JSON:例如 {amount: 1000, to_address: '0x...'}
4150
- created_at TEXT DEFAULT (datetime('now')),
4151
- expires_at TEXT NOT NULL,
4152
- consumed_at TEXT
4153
- )`);
4154
- db.exec(`CREATE INDEX IF NOT EXISTS idx_wac_chall_user ON webauthn_challenges(user_id, expires_at)`);
4155
- // gate token:auth/finish 成功后颁发,绑定 user + purpose + 业务参数(防重放)
4156
- db.exec(`CREATE TABLE IF NOT EXISTS webauthn_gate_tokens (
4157
- id TEXT PRIMARY KEY, -- token
4158
- user_id TEXT NOT NULL,
4159
- purpose TEXT NOT NULL,
4160
- purpose_data TEXT, -- JSON
4161
- expires_at TEXT NOT NULL, -- now + 60s
4162
- consumed_at TEXT
4163
- )`);
4164
- db.exec(`CREATE INDEX IF NOT EXISTS idx_wac_gate_user ON webauthn_gate_tokens(user_id, expires_at)`);
3089
+ initWebauthnSchema(db);
4165
3090
  // 用户 opt-in 设置
4166
3091
  try {
4167
3092
  db.exec("ALTER TABLE users ADD COLUMN webauthn_required_for_withdraw INTEGER DEFAULT 0");
@@ -4183,41 +3108,8 @@ const WEBAUTHN_CHALLENGE_TTL_MS = 5 * 60 * 1000; // 5 分钟
4183
3108
  const WEBAUTHN_GATE_TTL_MS = 90 * 1000; // 90 秒
4184
3109
  // M7.3:claim 验证任务系统 — 买家对推荐理由发起验证,3 verifier 共识仲裁
4185
3110
  try {
4186
- db.exec(`CREATE TABLE IF NOT EXISTS claim_verification_tasks (
4187
- id TEXT PRIMARY KEY,
4188
- order_id TEXT NOT NULL,
4189
- buyer_id TEXT NOT NULL,
4190
- seller_id TEXT NOT NULL,
4191
- product_id TEXT NOT NULL,
4192
- claim_target TEXT NOT NULL, -- 'price' | 'commission' | 'protection' | 'return' | 'warranty' | 'handling' | 'other'
4193
- claim_text TEXT NOT NULL, -- 买家陈述(≤ 500 字)
4194
- evidence_uri TEXT, -- 买家证据(URL / hash)
4195
- stake_buyer REAL NOT NULL, -- 买家锁定的质押金
4196
- seller_evidence_uri TEXT, -- 卖家提交的证据
4197
- seller_evidence_at TEXT,
4198
- deadline_at TEXT NOT NULL, -- 默认 48h;卖家提交证据后 +24h
4199
- status TEXT NOT NULL DEFAULT 'open', -- 'open' | 'sealed' | 'resolved_pass' | 'resolved_fail' | 'resolved_no_fault' | 'timeout_pass' | 'timeout_fail'
4200
- resolved_at TEXT,
4201
- created_at TEXT DEFAULT (datetime('now')),
4202
- UNIQUE(order_id)
4203
- )`);
4204
- db.exec(`CREATE INDEX IF NOT EXISTS idx_cvt_status ON claim_verification_tasks(status)`);
4205
- db.exec(`CREATE INDEX IF NOT EXISTS idx_cvt_buyer ON claim_verification_tasks(buyer_id)`);
4206
- db.exec(`CREATE INDEX IF NOT EXISTS idx_cvt_seller ON claim_verification_tasks(seller_id)`);
4207
- db.exec(`CREATE INDEX IF NOT EXISTS idx_cvt_deadline ON claim_verification_tasks(deadline_at) WHERE status = 'open'`);
4208
- db.exec(`CREATE TABLE IF NOT EXISTS claim_verification_votes (
4209
- id TEXT PRIMARY KEY,
4210
- task_id TEXT NOT NULL,
4211
- verifier_id TEXT NOT NULL,
4212
- vote TEXT NOT NULL, -- 'pass' | 'fail' | 'no_fault'
4213
- evidence_uri TEXT,
4214
- note TEXT,
4215
- voted_at TEXT DEFAULT (datetime('now')),
4216
- UNIQUE(task_id, verifier_id)
4217
- )`);
4218
- db.exec(`CREATE INDEX IF NOT EXISTS idx_cvv_task ON claim_verification_votes(task_id)`);
4219
- db.exec(`CREATE INDEX IF NOT EXISTS idx_cvv_verifier ON claim_verification_votes(verifier_id)`);
4220
- // M7.3b 结算扩展
3111
+ initClaimVerificationBaseSchema(db);
3112
+ // M7.3b 结算扩展 — 刻意留 server.ts 原位(base init 之后,按原顺序)
4221
3113
  try {
4222
3114
  db.exec(`ALTER TABLE claim_verification_tasks ADD COLUMN majority_vote TEXT`);
4223
3115
  }
@@ -4226,187 +3118,18 @@ try {
4226
3118
  db.exec(`ALTER TABLE claim_verification_votes ADD COLUMN was_majority INTEGER`);
4227
3119
  }
4228
3120
  catch { }
4229
- // verifier 禁言 / 永封记录(outlier 累计触发)
4230
- db.exec(`CREATE TABLE IF NOT EXISTS claim_verifier_suspensions (
4231
- id TEXT PRIMARY KEY,
4232
- user_id TEXT NOT NULL,
4233
- type TEXT NOT NULL, -- 'suspended' | 'revoked'
4234
- until_at TEXT, -- NULL = permanent (revoked)
4235
- reason TEXT,
4236
- outlier_count INTEGER,
4237
- created_at TEXT DEFAULT (datetime('now'))
4238
- )`);
4239
- db.exec(`CREATE INDEX IF NOT EXISTS idx_cvs_user ON claim_verifier_suspensions(user_id, created_at DESC)`);
4240
- // Sprint 1: 商品声明验证(与 order claim 平行,但作用于 product 层)
4241
- // 任何登录用户(除 seller 本人)可对商品的某项声明发起验证
4242
- // 3 verifier 共识投票判定 upheld(声明不实,卖家失分)/ dismissed(声明属实,发起人失质押)
4243
- db.exec(`CREATE TABLE IF NOT EXISTS product_claim_tasks (
4244
- id TEXT PRIMARY KEY,
4245
- product_id TEXT NOT NULL,
4246
- claimant_id TEXT NOT NULL,
4247
- seller_id TEXT NOT NULL,
4248
- claim_target TEXT NOT NULL, -- 'title' | 'description' | 'condition' | 'return_days' | 'handling_hours' | 'warranty_days' | 'shipping_regions' | 'origin' | 'other'
4249
- claim_text TEXT NOT NULL, -- 发起人陈述 6-500 字
4250
- evidence_uri TEXT, -- 发起人证据 URL
4251
- stake_claimant REAL NOT NULL, -- 发起人锁定质押
4252
- seller_evidence_uri TEXT, -- 卖家反驳证据
4253
- seller_evidence_at TEXT,
4254
- deadline_at TEXT NOT NULL, -- 默认 72h;卖家提交证据后 +24h
4255
- status TEXT NOT NULL DEFAULT 'open', -- 'open' | 'sealed' | 'resolved_upheld' | 'resolved_dismissed' | 'expired'
4256
- ruling TEXT, -- 'upheld' | 'dismissed' | 'insufficient'
4257
- majority_vote TEXT,
4258
- resolved_at TEXT,
4259
- created_at TEXT DEFAULT (datetime('now'))
4260
- )`);
4261
- db.exec(`CREATE INDEX IF NOT EXISTS idx_pct_status ON product_claim_tasks(status)`);
4262
- db.exec(`CREATE INDEX IF NOT EXISTS idx_pct_product ON product_claim_tasks(product_id)`);
4263
- db.exec(`CREATE INDEX IF NOT EXISTS idx_pct_claimant ON product_claim_tasks(claimant_id)`);
4264
- db.exec(`CREATE INDEX IF NOT EXISTS idx_pct_seller ON product_claim_tasks(seller_id)`);
4265
- db.exec(`CREATE TABLE IF NOT EXISTS product_claim_votes (
4266
- id TEXT PRIMARY KEY,
4267
- claim_id TEXT NOT NULL,
4268
- verifier_id TEXT NOT NULL,
4269
- vote TEXT NOT NULL, -- 'upheld' | 'dismissed' | 'insufficient'
4270
- evidence_uri TEXT,
4271
- note TEXT,
4272
- was_majority INTEGER,
4273
- voted_at TEXT DEFAULT (datetime('now')),
4274
- UNIQUE(claim_id, verifier_id)
4275
- )`);
4276
- db.exec(`CREATE INDEX IF NOT EXISTS idx_pcv_claim ON product_claim_votes(claim_id)`);
4277
- db.exec(`CREATE INDEX IF NOT EXISTS idx_pcv_verifier ON product_claim_votes(verifier_id)`);
4278
- // Sprint 2-A: 测评真实性验证(针对 shareables / manifests)
4279
- // 任何登录用户可对某条评测发起验证(不是真实购买/付费推广/利益相关等)
4280
- db.exec(`CREATE TABLE IF NOT EXISTS review_claim_tasks (
4281
- id TEXT PRIMARY KEY,
4282
- review_type TEXT NOT NULL, -- 'shareable' | 'manifest'
4283
- review_id TEXT NOT NULL, -- shareable.id 或 manifest.hash
4284
- product_id TEXT, -- 关联商品(用于显示)
4285
- reviewer_id TEXT NOT NULL, -- 被诉评测作者
4286
- claimant_id TEXT NOT NULL,
4287
- claim_target TEXT NOT NULL, -- 'not_real_purchase' | 'paid_promo' | 'incentivized' | 'misleading' | 'fake' | 'other'
4288
- claim_text TEXT NOT NULL,
4289
- evidence_uri TEXT,
4290
- stake_claimant REAL NOT NULL,
4291
- deadline_at TEXT NOT NULL,
4292
- status TEXT NOT NULL DEFAULT 'open',
4293
- ruling TEXT,
4294
- majority_vote TEXT,
4295
- resolved_at TEXT,
4296
- created_at TEXT DEFAULT (datetime('now'))
4297
- )`);
4298
- db.exec(`CREATE INDEX IF NOT EXISTS idx_rct_status ON review_claim_tasks(status)`);
4299
- db.exec(`CREATE INDEX IF NOT EXISTS idx_rct_review ON review_claim_tasks(review_type, review_id)`);
4300
- db.exec(`CREATE INDEX IF NOT EXISTS idx_rct_reviewer ON review_claim_tasks(reviewer_id)`);
4301
- db.exec(`CREATE TABLE IF NOT EXISTS review_claim_votes (
4302
- id TEXT PRIMARY KEY,
4303
- claim_id TEXT NOT NULL,
4304
- verifier_id TEXT NOT NULL,
4305
- vote TEXT NOT NULL, -- 'upheld' | 'dismissed' | 'insufficient'
4306
- evidence_uri TEXT,
4307
- note TEXT,
4308
- was_majority INTEGER,
4309
- voted_at TEXT DEFAULT (datetime('now')),
4310
- UNIQUE(claim_id, verifier_id)
4311
- )`);
4312
- db.exec(`CREATE INDEX IF NOT EXISTS idx_rcv_claim ON review_claim_votes(claim_id)`);
4313
- db.exec(`CREATE INDEX IF NOT EXISTS idx_rcv_verifier ON review_claim_votes(verifier_id)`);
4314
- // Sprint 2-B: 二手成色验证(针对 secondhand_items)
4315
- db.exec(`CREATE TABLE IF NOT EXISTS secondhand_claim_tasks (
4316
- id TEXT PRIMARY KEY,
4317
- sh_item_id TEXT NOT NULL,
4318
- seller_id TEXT NOT NULL, -- 二手卖家
4319
- claimant_id TEXT NOT NULL,
4320
- claim_target TEXT NOT NULL, -- 'condition' | 'images' | 'description' | 'title' | 'price' | 'other'
4321
- claim_text TEXT NOT NULL,
4322
- evidence_uri TEXT,
4323
- stake_claimant REAL NOT NULL,
4324
- deadline_at TEXT NOT NULL,
4325
- status TEXT NOT NULL DEFAULT 'open',
4326
- ruling TEXT,
4327
- majority_vote TEXT,
4328
- resolved_at TEXT,
4329
- created_at TEXT DEFAULT (datetime('now'))
4330
- )`);
4331
- db.exec(`CREATE INDEX IF NOT EXISTS idx_sct_status ON secondhand_claim_tasks(status)`);
4332
- db.exec(`CREATE INDEX IF NOT EXISTS idx_sct_item ON secondhand_claim_tasks(sh_item_id)`);
4333
- db.exec(`CREATE INDEX IF NOT EXISTS idx_sct_seller ON secondhand_claim_tasks(seller_id)`);
4334
- db.exec(`CREATE TABLE IF NOT EXISTS secondhand_claim_votes (
4335
- id TEXT PRIMARY KEY,
4336
- claim_id TEXT NOT NULL,
4337
- verifier_id TEXT NOT NULL,
4338
- vote TEXT NOT NULL,
4339
- evidence_uri TEXT,
4340
- note TEXT,
4341
- was_majority INTEGER,
4342
- voted_at TEXT DEFAULT (datetime('now')),
4343
- UNIQUE(claim_id, verifier_id)
4344
- )`);
4345
- db.exec(`CREATE INDEX IF NOT EXISTS idx_scv_claim ON secondhand_claim_votes(claim_id)`);
4346
- // Sprint 3-A: 拍卖声明(auctions)
4347
- // 任何非 seller 用户可对 active auction 的合理性发起验证
4348
- db.exec(`CREATE TABLE IF NOT EXISTS auction_claim_tasks (
4349
- id TEXT PRIMARY KEY,
4350
- auction_id TEXT NOT NULL,
4351
- seller_id TEXT NOT NULL,
4352
- claimant_id TEXT NOT NULL,
4353
- claim_target TEXT NOT NULL, -- 'unreasonable_reserve' | 'shill_bidding' | 'collusion' | 'fake_listing' | 'other'
4354
- claim_text TEXT NOT NULL,
4355
- evidence_uri TEXT,
4356
- stake_claimant REAL NOT NULL,
4357
- deadline_at TEXT NOT NULL,
4358
- status TEXT NOT NULL DEFAULT 'open',
4359
- ruling TEXT,
4360
- majority_vote TEXT,
4361
- resolved_at TEXT,
4362
- created_at TEXT DEFAULT (datetime('now'))
4363
- )`);
4364
- db.exec(`CREATE INDEX IF NOT EXISTS idx_act_status ON auction_claim_tasks(status)`);
4365
- db.exec(`CREATE INDEX IF NOT EXISTS idx_act_auction ON auction_claim_tasks(auction_id)`);
4366
- db.exec(`CREATE TABLE IF NOT EXISTS auction_claim_votes (
4367
- id TEXT PRIMARY KEY,
4368
- claim_id TEXT NOT NULL,
4369
- verifier_id TEXT NOT NULL,
4370
- vote TEXT NOT NULL,
4371
- evidence_uri TEXT,
4372
- note TEXT,
4373
- was_majority INTEGER,
4374
- voted_at TEXT DEFAULT (datetime('now')),
4375
- UNIQUE(claim_id, verifier_id)
4376
- )`);
4377
- db.exec(`CREATE INDEX IF NOT EXISTS idx_acv_claim ON auction_claim_votes(claim_id)`);
4378
- // Sprint 3-B: 慈善许愿声明(wishes)
4379
- // 任何非 wisher 用户可对 wish 真实性发起验证
4380
- db.exec(`CREATE TABLE IF NOT EXISTS wish_claim_tasks (
4381
- id TEXT PRIMARY KEY,
4382
- wish_id TEXT NOT NULL,
4383
- wisher_id TEXT NOT NULL,
4384
- claimant_id TEXT NOT NULL,
4385
- claim_target TEXT NOT NULL, -- 'fake_identity' | 'fake_story' | 'already_fulfilled' | 'duplicate' | 'inappropriate' | 'other'
4386
- claim_text TEXT NOT NULL,
4387
- evidence_uri TEXT,
4388
- stake_claimant REAL NOT NULL,
4389
- deadline_at TEXT NOT NULL,
4390
- status TEXT NOT NULL DEFAULT 'open',
4391
- ruling TEXT,
4392
- majority_vote TEXT,
4393
- resolved_at TEXT,
4394
- created_at TEXT DEFAULT (datetime('now'))
4395
- )`);
4396
- db.exec(`CREATE INDEX IF NOT EXISTS idx_wct_status ON wish_claim_tasks(status)`);
4397
- db.exec(`CREATE INDEX IF NOT EXISTS idx_wct_wish ON wish_claim_tasks(wish_id)`);
4398
- db.exec(`CREATE TABLE IF NOT EXISTS wish_claim_votes (
4399
- id TEXT PRIMARY KEY,
4400
- claim_id TEXT NOT NULL,
4401
- verifier_id TEXT NOT NULL,
4402
- vote TEXT NOT NULL,
4403
- evidence_uri TEXT,
4404
- note TEXT,
4405
- was_majority INTEGER,
4406
- voted_at TEXT DEFAULT (datetime('now')),
4407
- UNIQUE(claim_id, verifier_id)
4408
- )`);
4409
- db.exec(`CREATE INDEX IF NOT EXISTS idx_wcv_claim ON wish_claim_votes(claim_id)`);
3121
+ // verifier 禁言 / 永封记录 → server-schema.ts
3122
+ initClaimVerifierSuspensionsSchema(db);
3123
+ // Sprint 1: 商品声明验证 → server-schema.ts
3124
+ initProductClaimSchema(db);
3125
+ // Sprint 2-A: 测评真实性验证 server-schema.ts
3126
+ initReviewClaimSchema(db);
3127
+ // Sprint 2-B: 二手成色验证 → server-schema.ts
3128
+ initSecondhandClaimSchema(db);
3129
+ // Sprint 3-A: 拍卖声明 → server-schema.ts
3130
+ initAuctionClaimSchema(db);
3131
+ // Sprint 3-B: 慈善许愿声明 server-schema.ts
3132
+ initWishClaimSchema(db);
4410
3133
  }
4411
3134
  catch (e) {
4412
3135
  console.error('[M7.3 schema claim_verification]', e);
@@ -4523,8 +3246,8 @@ function computeAgentTrust(apiKey) {
4523
3246
  if (!user)
4524
3247
  return null;
4525
3248
  const ageDays = Math.max(0, (Date.now() - new Date(user.created_at.replace(' ', 'T') + 'Z').getTime()) / 86400_000);
4526
- const completedBuyer = db.prepare(`SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND status = 'completed'`).get(user.id).n;
4527
- const completedSeller = db.prepare(`SELECT COUNT(*) as n FROM orders WHERE seller_id = ? AND status = 'completed'`).get(user.id).n;
3249
+ const completedBuyer = db.prepare(`SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND ${genuineSalePredicate('orders')}`).get(user.id).n;
3250
+ const completedSeller = db.prepare(`SELECT COUNT(*) as n FROM orders WHERE seller_id = ? AND ${genuineSalePredicate('orders')}`).get(user.id).n;
4528
3251
  const disputeLoss = db.prepare(`SELECT COUNT(*) as n FROM disputes WHERE defendant_id = ? AND ruling_type IN ('refund_buyer','partial_refund')`).get(user.id).n;
4529
3252
  // 商品分享带来的真实成交(创作者贡献)
4530
3253
  const shareConversions = db.prepare(`
@@ -4960,19 +3683,8 @@ db.exec(`
4960
3683
  used_at TEXT
4961
3684
  )
4962
3685
  `);
4963
- db.exec(`
4964
- CREATE TABLE IF NOT EXISTS product_external_links (
4965
- id TEXT PRIMARY KEY,
4966
- product_id TEXT NOT NULL,
4967
- url TEXT NOT NULL,
4968
- source TEXT DEFAULT 'manual',
4969
- verified INTEGER DEFAULT 0,
4970
- verify_note TEXT,
4971
- added_at TEXT DEFAULT (datetime('now')),
4972
- verified_at TEXT,
4973
- UNIQUE(product_id, url)
4974
- )
4975
- `);
3686
+ // 外部链接验证 base → server-schema.ts;ALTER/index/回填 IIFE 刻意留原位(紧跟下方)
3687
+ initProductExternalLinksBaseSchema(db);
4976
3688
  try {
4977
3689
  db.exec('ALTER TABLE product_external_links ADD COLUMN revoked INTEGER DEFAULT 0');
4978
3690
  }
@@ -5023,58 +3735,12 @@ catch { }
5023
3735
  console.error('[backfill failed]', e.message);
5024
3736
  }
5025
3737
  })();
5026
- // link_challenges 保留用于向后兼容,新流程用 verify_tasks
5027
- db.exec(`
5028
- CREATE TABLE IF NOT EXISTS link_challenges (
5029
- id TEXT PRIMARY KEY,
5030
- product_id TEXT NOT NULL,
5031
- url TEXT NOT NULL,
5032
- code TEXT NOT NULL,
5033
- status TEXT DEFAULT 'pending',
5034
- created_at TEXT DEFAULT (datetime('now')),
5035
- expires_at TEXT NOT NULL,
5036
- verified_at TEXT
5037
- )
5038
- `);
5039
- db.exec(`
5040
- CREATE TABLE IF NOT EXISTS verify_tasks (
5041
- id TEXT PRIMARY KEY,
5042
- type TEXT NOT NULL DEFAULT 'code_check',
5043
- product_id TEXT NOT NULL,
5044
- url TEXT NOT NULL,
5045
- code TEXT,
5046
- verifiers_needed INTEGER NOT NULL DEFAULT 3,
5047
- reward_per_verifier REAL NOT NULL DEFAULT 0.1,
5048
- fee_locked REAL NOT NULL DEFAULT 0,
5049
- status TEXT NOT NULL DEFAULT 'open',
5050
- result TEXT,
5051
- created_at TEXT DEFAULT (datetime('now')),
5052
- expires_at TEXT NOT NULL,
5053
- settled_at TEXT
5054
- )
5055
- `);
5056
- db.exec(`
5057
- CREATE TABLE IF NOT EXISTS verify_submissions (
5058
- id TEXT PRIMARY KEY,
5059
- task_id TEXT NOT NULL,
5060
- verifier_id TEXT NOT NULL,
5061
- submission TEXT,
5062
- verdict TEXT,
5063
- claimed_at TEXT DEFAULT (datetime('now')),
5064
- submitted_at TEXT,
5065
- UNIQUE(task_id, verifier_id)
5066
- )
5067
- `);
5068
- db.exec(`
5069
- CREATE TABLE IF NOT EXISTS verifier_stats (
5070
- user_id TEXT PRIMARY KEY,
5071
- verify_rights INTEGER NOT NULL DEFAULT 3,
5072
- tasks_done INTEGER NOT NULL DEFAULT 0,
5073
- tasks_correct INTEGER NOT NULL DEFAULT 0,
5074
- tasks_wrong INTEGER NOT NULL DEFAULT 0,
5075
- suspended_until TEXT
5076
- )
5077
- `);
3738
+ // link_challenges 保留用于向后兼容,新流程用 verify_tasks → server-schema.ts
3739
+ initLinkChallengesSchema(db);
3740
+ initVerifyTasksSchema(db);
3741
+ initVerifySubmissionsSchema(db);
3742
+ // verifier_stats 须在 PERMANENT_ACCOUNTS bootstrap 之前完成 → server-schema.ts
3743
+ initVerifierStatsSchema(db);
5078
3744
  (() => {
5079
3745
  for (const acc of PERMANENT_ACCOUNTS) {
5080
3746
  const apiKey = 'key_perm_' + createHmac('sha256', MASTER_SEED).update(acc.seed).digest('hex');
@@ -5807,6 +4473,11 @@ registerAgentReputationRoutes(app, {
5807
4473
  });
5808
4474
  // #1013 Phase 47: 6 公开用户主页 endpoints 已迁出到 routes/users-public.ts
5809
4475
  registerUsersPublicRoutes(app, { db, auth, noteAuthenticityBadges });
4476
+ registerDirectPayDisclosureAckRoutes(app, { db, auth, generateId, consumeGateToken }); // PR-4d
4477
+ registerDirectReceivePaymentInstructionRoutes(app, { db, auth, generateId });
4478
+ registerDirectReceiveAccountsRoutes(app, { db, auth, generateId, consumeGateToken }); // PR-4f-a + Phase C1
4479
+ registerDirectPayAvailabilityRoutes(app, { db, auth, getProtocolParam, generateId }); // PR-4a + 缓交 apply/status
4480
+ registerAdminDirectReceiveDepositsRoutes(app, { db, requireRootAdmin: (req, res) => requireRootAdmin(req, res), consumeGateToken, logAdminAction, getProtocolParam }); // PR-4b-3 + readiness
5810
4481
  // RFC-004 build_feedback — agent-native "use → build" 反馈管道
5811
4482
  registerBuildFeedbackRoutes(app, {
5812
4483
  db, auth,
@@ -6574,25 +5245,8 @@ registerAdminTokenomicsRoutes(app, {
6574
5245
  });
6575
5246
  // system-flags — Phase 107 已迁出
6576
5247
  // #1013 Phase 73: GET /api/reviews/recent 已迁出(claim 2 端点也由同模块注册,定义在下游)
6577
- // B-4: 编辑精选 / 每周推荐
6578
- db.exec(`
6579
- CREATE TABLE IF NOT EXISTS editor_picks (
6580
- id TEXT PRIMARY KEY,
6581
- kind TEXT NOT NULL, -- 'product' | 'seller'
6582
- target_id TEXT NOT NULL,
6583
- title TEXT, -- 编辑推荐语
6584
- note TEXT,
6585
- starts_at TEXT NOT NULL,
6586
- ends_at TEXT NOT NULL,
6587
- sort_order INTEGER DEFAULT 0,
6588
- created_by TEXT,
6589
- created_at TEXT DEFAULT (datetime('now'))
6590
- )
6591
- `);
6592
- try {
6593
- db.exec('CREATE INDEX IF NOT EXISTS idx_ep_active ON editor_picks(kind, ends_at, sort_order)');
6594
- }
6595
- catch { }
5248
+ // B-4: 编辑精选 / 每周推荐 → server-schema.ts
5249
+ initEditorPicksSchema(db);
6596
5250
  // editor-picks 公开 — Phase 107 已迁出
6597
5251
  // #1013 Phase 66: 3 admin/editor-picks endpoints 已迁出
6598
5252
  registerAdminEditorPicksRoutes(app, {
@@ -6654,25 +5308,8 @@ registerAdminOpsRoutes(app, {
6654
5308
  });
6655
5309
  // AI 2 endpoints — Phase 100 已迁出
6656
5310
  registerAiRoutes(app, { db, auth, anthropic });
6657
- // D-3: KYC light — 实名认证(轻度,不存原始证件号)
6658
- db.exec(`
6659
- CREATE TABLE IF NOT EXISTS kyc_records (
6660
- user_id TEXT PRIMARY KEY,
6661
- real_name TEXT NOT NULL,
6662
- id_type TEXT NOT NULL, -- 'passport' | 'national_id' | 'driver_license'
6663
- id_number_hash TEXT NOT NULL, -- sha256(id_number + MASTER_SEED)
6664
- id_number_last4 TEXT, -- 末 4 位明文(便于核对)
6665
- status TEXT NOT NULL DEFAULT 'pending', -- pending / approved / rejected
6666
- reject_reason TEXT,
6667
- reviewed_by TEXT,
6668
- reviewed_at TEXT,
6669
- submitted_at TEXT DEFAULT (datetime('now'))
6670
- )
6671
- `);
6672
- try {
6673
- db.exec('CREATE INDEX IF NOT EXISTS idx_kyc_status ON kyc_records(status, submitted_at)');
6674
- }
6675
- catch { }
5311
+ // D-3: KYC light — 实名认证(轻度,不存原始证件号)→ server-schema.ts
5312
+ initKycRecordsSchema(db);
6676
5313
  // KYC 2 endpoints — Phase 97 已迁出
6677
5314
  registerKycRoutes(app, { db, auth, MASTER_SEED });
6678
5315
  // #1013 Phase 68: 6 admin/kyc+risk endpoints 已迁出
@@ -7033,7 +5670,7 @@ function checkVerifierEligibility(userId) {
7033
5670
  const ageDays = Math.floor((Date.now() - new Date(user.created_at).getTime()) / 86400_000);
7034
5671
  items.push({ key: 'age', label: '账户年龄 ≥ 60 天', current: ageDays, required: 60, ok: ageDays >= 60 });
7035
5672
  items.push({ key: 'email', label: '邮箱已验证', current: user.email_verified ? '✓' : '✗', required: '✓', ok: !!user.email_verified });
7036
- const orders = db.prepare("SELECT COUNT(*) as n FROM orders WHERE (buyer_id = ? OR seller_id = ?) AND status = 'completed'").get(userId, userId).n;
5673
+ const orders = db.prepare(`SELECT COUNT(*) as n FROM orders WHERE (buyer_id = ? OR seller_id = ?) AND ${genuineSalePredicate('orders')}`).get(userId, userId).n;
7037
5674
  items.push({ key: 'orders', label: '完成订单 ≥ 20 笔', current: orders, required: 20, ok: orders >= 20 });
7038
5675
  const disputeLost = db.prepare(`
7039
5676
  SELECT COUNT(*) as n FROM disputes
@@ -7180,7 +5817,7 @@ function checkArbitratorEligibility(userId) {
7180
5817
  const ageDays = Math.floor((Date.now() - new Date(user.created_at).getTime()) / 86400_000);
7181
5818
  items.push({ key: 'age', label: '账户年龄 ≥ 90 天', current: ageDays, required: 90, ok: ageDays >= 90 });
7182
5819
  items.push({ key: 'email', label: '邮箱已验证', current: user.email_verified ? '✓' : '✗', required: '✓', ok: !!user.email_verified });
7183
- const orders = db.prepare("SELECT COUNT(*) as n FROM orders WHERE (buyer_id = ? OR seller_id = ?) AND status = 'completed'").get(userId, userId).n;
5820
+ const orders = db.prepare(`SELECT COUNT(*) as n FROM orders WHERE (buyer_id = ? OR seller_id = ?) AND ${genuineSalePredicate('orders')}`).get(userId, userId).n;
7184
5821
  items.push({ key: 'orders', label: '完成订单 ≥ 50 笔', current: orders, required: 50, ok: orders >= 50 });
7185
5822
  const disputeLost = db.prepare(`
7186
5823
  SELECT COUNT(*) as n FROM disputes
@@ -7400,7 +6037,7 @@ registerOrdersReadRoutes(app, { db, auth, getOrderStatus, getOrderChain, verifyO
7400
6037
  registerOrdersActionRoutes(app, {
7401
6038
  db, auth, isTrustedRole, generateId, transition, notifyTransition,
7402
6039
  settleOrder, settleFault, detectFraud, createDispute, checkTimeouts, recordViolationReputation,
7403
- broadcastSystemEvent,
6040
+ broadcastSystemEvent, consumeGateToken,
7404
6041
  });
7405
6042
  // #1013 Phase 85: POST /api/orders 巨型事务已迁出
7406
6043
  registerOrdersCreateRoutes(app, {
@@ -7515,8 +6152,8 @@ function awardBidAndCreateOrder(rfq, winner) {
7515
6152
  db.prepare(`
7516
6153
  INSERT INTO products (id, seller_id, title, description, price, stock, status, images,
7517
6154
  ship_regions, handling_hours, commission_rate, category_id, stake_amount,
7518
- listing_id, fulfillment_type, eta_hours, freshness_ts, cold_start_remaining)
7519
- VALUES (?,?,?,?,?,?,'active','[]',?,?,?,?,0,?,?,?,datetime('now'),?)
6155
+ listing_id, fulfillment_type, eta_hours, freshness_ts, cold_start_remaining, currency)
6156
+ VALUES (?,?,?,?,?,?,'active','[]',?,?,?,?,0,?,?,?,datetime('now'),?,'WAZ')
7520
6157
  `).run(productId, sellerId, String(rfq.title), `[RFQ ${rfqId}] ` + (rfq.notes ? String(rfq.notes).slice(0, 200) : '协议撮合订单'), price, qty, String(rfq.region_required || '全国'), 24, 0, 'cat_default', lstId, fulfillmentType, etaHours, 30);
7521
6158
  }
7522
6159
  }
@@ -7970,8 +6607,8 @@ function settleAuctionInner(aucId) {
7970
6607
  productId = generateId('p');
7971
6608
  db.prepare(`
7972
6609
  INSERT INTO products (id, seller_id, title, description, price, stock, status, images,
7973
- ship_regions, handling_hours, commission_rate, category_id, stake_amount, listing_id, freshness_ts, cold_start_remaining)
7974
- VALUES (?,?,?,?,?,?,'active','[]',?,?,?,?,0,?,datetime('now'),?)
6610
+ ship_regions, handling_hours, commission_rate, category_id, stake_amount, listing_id, freshness_ts, cold_start_remaining, currency)
6611
+ VALUES (?,?,?,?,?,?,'active','[]',?,?,?,?,0,?,datetime('now'),?,'WAZ')
7975
6612
  `).run(productId, sellerId, String(auc.title), `[AUC ${aucId}] ` + (auc.notes ? String(auc.notes).slice(0, 200) : '拍卖成交'), price, qty, '全国', 24, 0.10, 'cat_default', auc.listing_id ? String(auc.listing_id) : null, 30);
7976
6613
  }
7977
6614
  // 5) 截止期 — 拍卖订单走标准 17 天链(拍卖结束后再谈履约时效)
@@ -8053,14 +6690,8 @@ setInterval(() => {
8053
6690
  // #1013 Phase 4: 8 endpoints + 4 helpers 已迁出到 routes/chat.ts
8054
6691
  // ============================================================
8055
6692
  registerChatRoutes(app, { db, auth, generateId, rateLimitOk });
8056
- // 初始化导入次数追踪表
8057
- db.exec(`
8058
- CREATE TABLE IF NOT EXISTS import_logs (
8059
- id TEXT PRIMARY KEY,
8060
- user_id TEXT NOT NULL,
8061
- created_at TEXT DEFAULT (datetime('now'))
8062
- )
8063
- `);
6693
+ // 初始化导入次数追踪表 → server-schema.ts
6694
+ initImportLogsSchema(db);
8064
6695
  const FREE_IMPORT_LIMIT = 10;
8065
6696
  // #1013 Phase 114: import-product 已迁出
8066
6697
  registerImportProductRoutes(app, {
@@ -8105,7 +6736,7 @@ const TASK_DEFS = {
8105
6736
  };
8106
6737
  // 计算用户当前任务进度(不写库,纯读)
8107
6738
  function computeTaskProgress(userId) {
8108
- const completed = db.prepare(`SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND status = 'completed'`).get(userId).n;
6739
+ const completed = db.prepare(`SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND ${genuineSalePredicate('orders')}`).get(userId).n;
8109
6740
  const ratingsGiven = db.prepare(`SELECT COUNT(*) as n FROM order_ratings WHERE buyer_id = ?`).get(userId).n;
8110
6741
  const ratingsReceived = db.prepare(`SELECT COUNT(*) as n FROM order_ratings WHERE seller_id = ?`).get(userId).n;
8111
6742
  const follows = db.prepare(`SELECT COUNT(*) as n FROM follows WHERE follower_id = ?`).get(userId).n;
@@ -8297,6 +6928,7 @@ registerShareablesInteractionsRoutes(app, {
8297
6928
  parseMentions, notifyMentions,
8298
6929
  });
8299
6930
  registerLeaderboardRoutes(app, { db, internalAuditorId: INTERNAL_AUDITOR_ID, rateLimitOk });
6931
+ registerFxRoutes(app, { rateLimitOk });
8300
6932
  // like-status / bookmark-status / bookmarked-shareables 已迁出 (#1013 Phase 12)
8301
6933
  // ─── Manifest Registry API(原生 P2P 内容索引,零字节存储)─────
8302
6934
  // ============================================================
@@ -8574,28 +7206,19 @@ function settleCommission(orderId, effectiveBase) {
8574
7206
  { level: 2, beneficiary: l2Uid },
8575
7207
  { level: 3, beneficiary: l3Uid },
8576
7208
  ];
8577
- // #7 Commission source_type attribution 找到带来 buyer shareable,确定 channel 类型
8578
- // 反推: uid 作为 sharer product 分享给"下家"的 shareable 类型
8579
- function resolveSourceType(uid) {
8580
- if (!uid)
8581
- return 'sponsor';
8582
- const attr = db.prepare(`
8583
- SELECT shareable_id FROM product_share_attribution
8584
- WHERE product_id = ? AND sharer_id = ? AND shareable_id IS NOT NULL
8585
- ORDER BY created_at DESC LIMIT 1
8586
- `).get(order.product_id, uid);
8587
- if (!attr)
8588
- return 'sponsor';
8589
- const sh = db.prepare(`SELECT type FROM shareables WHERE id = ?`).get(attr.shareable_id);
8590
- if (!sh)
8591
- return 'sponsor';
8592
- return sh.type === 'note' ? 'note' : 'link';
8593
- }
7209
+ // #7 Commission source_type resolution moved to module-level commissionSourceType() RFC-018
7210
+ // Option A writes commission_records at MATURATION (not here), so source_type is stamped there.
8594
7211
  // 2026-06-04:所有兜底统一入 commission_reserve(三级公池,独立科目,只进不出)。
8595
7212
  // commission 不再回流 global_fund(PV 资金)—— 三套科目解耦,redirected 始终 0。
8596
7213
  let toCommissionReserve = 0; // → commission_reserve(仅作日志/返回信息用)
8597
7214
  // 三级金额一次性 allocate(精确求和 ≡ poolU);各级再按 gate 路由到钱包/公池/escrow。
8598
7215
  const levelAmtU = allocate(poolU, [LEVEL_RATES[1], LEVEL_RATES[2], LEVEL_RATES[3]]);
7216
+ // RFC-018 clearing window: opted-in commission accrues to pending_commission_escrow and matures
7217
+ // after the product's return window + buffer. Anchor = now (settleCommission runs at order→completed).
7218
+ const nowMsClearing = Date.now();
7219
+ const returnDaysClearing = Number(db.prepare("SELECT return_days FROM products WHERE id = ?").get(order.product_id)?.return_days ?? 7);
7220
+ const clearingBufferDays = Number(db.prepare("SELECT value FROM protocol_params WHERE key = 'settlement.clearing_buffer_days'").get()?.value ?? 2);
7221
+ const maturesAtClearing = nowMsClearing + Math.max(0, returnDaysClearing + clearingBufferDays) * 86400 * 1000;
8599
7222
  for (const { level, beneficiary } of recipients) {
8600
7223
  const amountU = levelAmtU[level - 1];
8601
7224
  const amount = toDecimal(amountU);
@@ -8641,15 +7264,17 @@ function settleCommission(orderId, effectiveBase) {
8641
7264
  catch (e) { /* UNIQUE 冲突 — settleCommission 重入幂等 */ }
8642
7265
  continue;
8643
7266
  }
8644
- // ⑤ 正常分账
7267
+ // ⑤ RFC-018 clearing (Option A): opted-in commission ACCRUES to pending_commission_escrow with a
7268
+ // matures_at — it is NOT credited to the wallet now and writes NO commission_records yet. The
7269
+ // maturation cron (rewards-clearing-mature) re-validates the order is still genuinely closed, then
7270
+ // writes commission_records + credits the wallet. A return in-window reverses the pending row
7271
+ // (proportionally) — never paid, never clawed back. expires_at is a NOT-NULL filler = matures_at;
7272
+ // the escrow-expire cron ignores rows where matures_at IS NOT NULL (i.e. clearing rows).
8645
7273
  try {
8646
- const srcType = resolveSourceType(beneficiary);
8647
- db.prepare(`INSERT INTO commission_records (id, order_id, beneficiary_id, source_buyer_id, level, amount, rate, region, source, source_type)
8648
- VALUES (?,?,?,?,?,?,?,?,?,?)`)
8649
- .run(generateId('comm'), orderId, beneficiary, order.buyer_id, level, amount, rate, region, routeSource, srcType);
8650
- applyWalletDelta(db, beneficiary, { balance: amountU, earned: amountU });
7274
+ db.prepare(`INSERT INTO pending_commission_escrow (recipient_user_id, order_id, amount, attribution_path, status, created_at, expires_at, matures_at) VALUES (?,?,?,?,'pending',?,?,?)`)
7275
+ .run(beneficiary, orderId, amount, `L${level}`, nowMsClearing, maturesAtClearing, maturesAtClearing);
8651
7276
  }
8652
- catch (e) { /* UNIQUE 冲突 */ }
7277
+ catch (e) { /* UNIQUE (recipient,order,path) — settleCommission re-entry idempotent */ }
8653
7278
  }
8654
7279
  db.prepare("UPDATE orders SET settled_commission_at = datetime('now') WHERE id = ?").run(orderId);
8655
7280
  // redirected 恒为 0:commission 兜底全部入 commission_reserve,不再回流 global_fund(PV 资金)。
@@ -8765,6 +7390,10 @@ function settleOrder(orderId) {
8765
7390
  // 内部 try/catch 用于非关键 hook(settlePinRewards / metrics 更新)失败不回滚资金主流程
8766
7391
  db.transaction(() => {
8767
7392
  const order = db.prepare('SELECT * FROM orders WHERE id = ?').get(orderId);
7393
+ if (order && order.payment_rail === 'direct_p2p') {
7394
+ settleDirectPayFeeAtCompletion(db, order, generateId('dpfr'));
7395
+ return;
7396
+ } // Rail1 直付:跳过 escrow 结算;平台费=链下应收(释放遗留模拟 stake + accrue,fail-closed 同原子边界)
8768
7397
  const total = order.total_amount;
8769
7398
  const isSecondhand = order.source === 'secondhand';
8770
7399
  const isInPerson = order.fulfillment_mode === 'in_person';
@@ -9212,7 +7841,7 @@ registerEvidenceRoutes(app, { db, auth, detectFraud });
9212
7841
  // #1013 Phase 1: 7 endpoint handlers 已迁出到 src/pwa/routes/webauthn.ts
9213
7842
  // helpers (consumeGateToken / requireHumanPresence) 仍在本文件,被 withdraw/arbitrate/vote 等引用
9214
7843
  registerWebauthnRoutes(app, {
9215
- db, auth, generateId,
7844
+ db, auth, generateId, rateLimitOk,
9216
7845
  rpId: WEBAUTHN_RP_ID,
9217
7846
  rpName: WEBAUTHN_RP_NAME,
9218
7847
  origin: WEBAUTHN_ORIGIN,
@@ -9964,22 +8593,7 @@ function startDepositWatcher() {
9964
8593
  // 轻量级自建错误上报 — 避免外部 Sentry 依赖
9965
8594
  // 后端:进程级 uncaughtException + unhandledRejection
9966
8595
  // 前端:POST /api/error-report(window.onerror → 入此表)
9967
- db.exec(`
9968
- CREATE TABLE IF NOT EXISTS error_log (
9969
- id INTEGER PRIMARY KEY AUTOINCREMENT,
9970
- source TEXT NOT NULL, -- 'server-uncaught' | 'server-rejection' | 'client'
9971
- message TEXT NOT NULL,
9972
- stack TEXT,
9973
- url TEXT, -- 客户端 location.href
9974
- user_agent TEXT, -- 客户端 UA
9975
- user_id TEXT, -- 已登录用户(可空)
9976
- created_at TEXT DEFAULT (datetime('now'))
9977
- )
9978
- `);
9979
- try {
9980
- db.exec('CREATE INDEX IF NOT EXISTS idx_error_log_created ON error_log(created_at)');
9981
- }
9982
- catch { }
8596
+ initErrorLogSchema(db);
9983
8597
  // ─── 治理岗位上岗(W3.5-B,2026-06-02)──────────────────────────
9984
8598
  // docs/GOVERNANCE-ONBOARDING.md — arbitrator + verifier 申请 / 上岗 / 卸任 / 申诉
9985
8599
  // 1 表:governance_applications(append-only,记录 apply/activate/resign/auto_deactivate/appeal)
@@ -10106,72 +8720,10 @@ try {
10106
8720
  db.exec('CREATE INDEX IF NOT EXISTS idx_rewards_apps_action ON rewards_applications(user_id, action, created_at DESC)');
10107
8721
  }
10108
8722
  catch { }
10109
- // 5. pending_commission_escrow:opt-out promoter 待激活领取队列(§3.5b)
10110
- // PR-1c-b: order_id is NULLable so attribution_path='pv_pair' rows (which accrue
10111
- // across many orders) can record amount without inventing a fake order_id.
10112
- // L1/L2/L3 rows still carry a real order_id, enforced by the gate code path
10113
- // in settleCommission (not by NOT NULL constraint).
10114
- db.exec(`
10115
- CREATE TABLE IF NOT EXISTS pending_commission_escrow (
10116
- id INTEGER PRIMARY KEY AUTOINCREMENT,
10117
- recipient_user_id TEXT NOT NULL,
10118
- order_id TEXT, -- NULL for pv_pair (PR-1c-b)
10119
- amount REAL NOT NULL, -- WAZ amount
10120
- attribution_path TEXT NOT NULL, -- 'L1' | 'L2' | 'L3' | 'pv_pair' | etc.
10121
- status TEXT NOT NULL DEFAULT 'pending', -- 'pending' | 'settled' | 'expired'
10122
- created_at INTEGER NOT NULL,
10123
- expires_at INTEGER NOT NULL,
10124
- settled_at INTEGER,
10125
- expired_to_charity_at INTEGER,
10126
- FOREIGN KEY (recipient_user_id) REFERENCES users(id),
10127
- FOREIGN KEY (order_id) REFERENCES orders(id)
10128
- )
10129
- `);
10130
- (function migrateEscrowOrderIdNullable() {
10131
- const cols = db.prepare("PRAGMA table_info(pending_commission_escrow)").all();
10132
- const orderIdCol = cols.find(c => c.name === 'order_id');
10133
- if (!orderIdCol || orderIdCol.notnull === 0)
10134
- return; // already nullable (or table missing entirely)
10135
- console.log('[pc-escrow-migrate] order_id is NOT NULL — recreating table to allow NULL for pv_pair');
10136
- db.exec('PRAGMA foreign_keys = OFF');
10137
- db.transaction(() => {
10138
- db.exec(`
10139
- CREATE TABLE pending_commission_escrow_new (
10140
- id INTEGER PRIMARY KEY AUTOINCREMENT,
10141
- recipient_user_id TEXT NOT NULL,
10142
- order_id TEXT,
10143
- amount REAL NOT NULL,
10144
- attribution_path TEXT NOT NULL,
10145
- status TEXT NOT NULL DEFAULT 'pending',
10146
- created_at INTEGER NOT NULL,
10147
- expires_at INTEGER NOT NULL,
10148
- settled_at INTEGER,
10149
- expired_to_charity_at INTEGER,
10150
- FOREIGN KEY (recipient_user_id) REFERENCES users(id),
10151
- FOREIGN KEY (order_id) REFERENCES orders(id)
10152
- )
10153
- `);
10154
- db.exec('INSERT INTO pending_commission_escrow_new SELECT * FROM pending_commission_escrow');
10155
- db.exec('DROP TABLE pending_commission_escrow');
10156
- db.exec('ALTER TABLE pending_commission_escrow_new RENAME TO pending_commission_escrow');
10157
- })();
10158
- db.exec('PRAGMA foreign_keys = ON');
10159
- })();
10160
- try {
10161
- db.exec('CREATE INDEX IF NOT EXISTS idx_escrow_recipient ON pending_commission_escrow(recipient_user_id, status, expires_at)');
10162
- }
10163
- catch { }
10164
- try {
10165
- db.exec('CREATE INDEX IF NOT EXISTS idx_escrow_expiry ON pending_commission_escrow(status, expires_at)');
10166
- }
10167
- catch { }
10168
- // PR-1c-a: UNIQUE (recipient, order, path) defends against double-insert if settleCommission ever retries
10169
- // Note: NULL order_id (PR-1c-b pv_pair) is distinct in SQLite UNIQUE — idempotency for pv_pair relies
10170
- // on binary_score_records.settled_at instead (source-side dedup).
10171
- try {
10172
- db.exec('CREATE UNIQUE INDEX IF NOT EXISTS uniq_escrow_recipient_order_path ON pending_commission_escrow(recipient_user_id, order_id, attribution_path)');
10173
- }
10174
- catch { }
8723
+ // pending_commission_escrow schema (table + migration + indexes) lives in
8724
+ // initPendingCommissionEscrowSchema (early helper batch above) RFC-018 PR1 relocated it there so
8725
+ // all of its DDL is built once, never half-inline (fresh-DB silent-fail铁律). The backfill below
8726
+ // only READS the table; it still runs after the build (early batch precedes this line).
10175
8727
  // Codex #69 P1:pv_escrow_reserve(#1106 隔离负债账)回填历史 pending pv_pair escrow —— 按 delta 对账,不全量转。
10176
8728
  // 该列在 global_fund 上新增(line ~2319);#1106 之后新建的 pv_pair escrow 结算时【已】pv_escrow_reserve += wazAmount,
10177
8729
  // 但加列【之前】产生的 pending pv_pair 从没进过 reserve。升级窗口里两者混存。
@@ -10221,6 +8773,7 @@ const RFC002_PARAMS = [
10221
8773
  { key: 'rewards_opt_in.min_completed_orders', value: '1', type: 'number', description: 'RFC-002 §3.2:申请 rewards opt-in 的最小已完成订单数', category: 'rewards', min: 0, max: 100, metaRuleLocked: false },
10222
8774
  { key: 'rewards_opt_in.require_passkey', value: '1', type: 'number', description: 'RFC-002 §3.3:申请 / 关闭是否需 Passkey(1=必须,0=允许 password)— META-RULE LOCKED,降低需 60d meta-rule track', category: 'rewards', min: 0, max: 1, metaRuleLocked: true },
10223
8775
  { key: 'rewards_opt_in.escrow_days', value: '30', type: 'number', description: 'RFC-002 §3.5b:pending commission escrow 过期天数(过期后流入 charity_fund)', category: 'rewards', min: 7, max: 180, metaRuleLocked: false },
8776
+ { key: 'settlement.clearing_buffer_days', value: '2', type: 'number', description: 'RFC-018:佣金清算期 = 商品 return_days + 此 buffer(成熟才入钱包;窗内退货则冲销,永不 clawback)', category: 'rewards', min: 0, max: 30, metaRuleLocked: false },
10224
8777
  { key: 'rewards_opt_in.consent_delay_seconds', value: '8', type: 'number', description: 'RFC-002 §3.3:server-side 8s 反诱导延迟 — META-RULE LOCKED,降低需 60d meta-rule track', category: 'rewards', min: 0, max: 60, metaRuleLocked: true },
10225
8778
  { key: 'rewards_opt_in.reconfirm_grace_days', value: '14', type: 'number', description: 'RFC-002 §3.10:major consent 变更后用户重新确认 grace 期(过期 auto_downgrade)', category: 'rewards', min: 3, max: 90, metaRuleLocked: false },
10226
8779
  ];
@@ -10309,6 +8862,8 @@ app.listen(PORT, () => {
10309
8862
  startAutoDeactivateCron({ db, generateId, getProtocolParam });
10310
8863
  // #1090 RFC-002 PR-1c-a: escrow expire cron (every 1h)
10311
8864
  startEscrowExpireCron({ db, redirectToCommissionReserve });
8865
+ startDirectPayTimeoutCron({ db });
8866
+ startClearingMatureCron({ db }); // RFC-018: pay/hold matured clearing commission (logic in the cron module)
10312
8867
  // #1090 RFC-002 PR-3 slice 2: auto_downgrade cron (every 24h)
10313
8868
  // Triggered when a new major consent text is published; opted-in users
10314
8869
  // who don't reconfirm within reconfirm_grace_days (14d default) get