@seasonkoh/webaz 0.1.24 → 0.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/layer0-foundation/L0-1-database/db-backends/pg-backend.js +51 -0
- package/dist/layer0-foundation/L0-1-database/db-backends/sql-dialect-datetime.js +437 -0
- package/dist/layer0-foundation/L0-1-database/db-backends/sql-placeholders.js +98 -0
- package/dist/layer0-foundation/L0-1-database/db.js +65 -0
- package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +13 -11
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +1 -1
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +13 -11
- package/dist/layer1-agent/L1-1-mcp-server/server.js +288 -208
- package/dist/layer1-agent/L1-2-external-anchor/anchor-engine.js +14 -12
- package/dist/layer2-business/L2-6-notifications/notification-engine.js +8 -5
- package/dist/layer2-business/L2-7-snf/snf-engine.js +16 -14
- package/dist/layer2-business/L2-8-feedback/build-feedback-engine.js +18 -10
- package/dist/layer2-business/L2-9-contribution/build-reputation-engine.js +37 -23
- package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +182 -0
- package/dist/layer2-business/L2-9-contribution/build-task-participation.js +47 -0
- package/dist/layer2-business/L2-9-contribution/build-task-read.js +222 -0
- package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +11 -3
- package/dist/layer2-business/L2-9-contribution/canonical-contribution-target.js +16 -0
- package/dist/layer2-business/L2-9-contribution/contribution-display-envelope.js +40 -0
- package/dist/layer2-business/L2-9-contribution/contribution-score-contract.js +36 -0
- package/dist/layer2-business/L2-9-contribution/contribution-score-evidence.js +61 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/canonical.js +60 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/github-credential.schema.js +140 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/github-fetch-adapter.js +437 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/self-consistency.js +38 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/verifier.js +231 -0
- package/dist/layer2-business/L2-9-contribution/github-credential-ingestion-engine.js +145 -0
- package/dist/layer2-business/L2-9-contribution/github-credential-store.js +115 -0
- package/dist/layer2-business/L2-9-contribution/identity-binding-engine.js +134 -0
- package/dist/layer2-business/L2-9-contribution/identity-binding-store.js +101 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-engine.js +126 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-store.js +30 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-discovery.js +55 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-engine.js +109 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-fact-precondition.js +22 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-proof-verifier.js +97 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-read.js +59 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-ai-store.js +99 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-draft.js +191 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +129 -0
- package/dist/layer2-business/L2-notes/note-photo-storage.js +4 -2
- package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +17 -15
- package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +11 -8
- package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +9 -8
- package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +11 -8
- package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +22 -16
- package/dist/pwa/acp-feed.js +13 -1
- package/dist/pwa/admin-bearer-auth.js +21 -0
- package/dist/pwa/contract-fingerprint.js +2 -0
- package/dist/pwa/email-delivery.js +127 -0
- package/dist/pwa/endpoint-actions.js +5 -1
- package/dist/pwa/goal-index.js +8 -8
- package/dist/pwa/human-presence.js +62 -0
- package/dist/pwa/public/app.js +1485 -283
- package/dist/pwa/public/i18n.js +297 -59
- package/dist/pwa/public/index.html +1 -0
- package/dist/pwa/public/openapi.json +5 -5
- package/dist/pwa/public/whitepaper/en/index.html +153 -0
- package/dist/pwa/public/whitepaper/zh-CN/index.html +153 -0
- package/dist/pwa/rate-limit.js +22 -0
- package/dist/pwa/routes/account-deletion.js +15 -13
- package/dist/pwa/routes/addresses.js +10 -9
- package/dist/pwa/routes/admin-admins.js +13 -14
- package/dist/pwa/routes/admin-analytics.js +109 -69
- package/dist/pwa/routes/admin-atomic.js +10 -4
- package/dist/pwa/routes/admin-catalog.js +13 -11
- package/dist/pwa/routes/admin-editor-picks.js +15 -10
- package/dist/pwa/routes/admin-events.js +5 -3
- package/dist/pwa/routes/admin-health.js +2 -1
- package/dist/pwa/routes/admin-moderation.js +50 -29
- package/dist/pwa/routes/admin-ops.js +35 -23
- package/dist/pwa/routes/admin-protocol-params.js +16 -19
- package/dist/pwa/routes/admin-reports.js +23 -21
- package/dist/pwa/routes/admin-tokenomics.js +26 -25
- package/dist/pwa/routes/admin-users-lifecycle.js +37 -40
- package/dist/pwa/routes/admin-users-query.js +65 -53
- package/dist/pwa/routes/admin-verifier-flow.js +82 -41
- package/dist/pwa/routes/admin-verifier-whitelist.js +55 -27
- package/dist/pwa/routes/admin-wallet-ops.js +32 -7
- package/dist/pwa/routes/agent-buy.js +46 -22
- package/dist/pwa/routes/agent-governance.js +52 -56
- package/dist/pwa/routes/ai.js +7 -5
- package/dist/pwa/routes/analytics.js +43 -41
- package/dist/pwa/routes/anchors.js +19 -20
- package/dist/pwa/routes/announcements.js +13 -13
- package/dist/pwa/routes/arbitrator.js +97 -31
- package/dist/pwa/routes/auction.js +157 -116
- package/dist/pwa/routes/auth-login.js +6 -4
- package/dist/pwa/routes/auth-read.js +21 -10
- package/dist/pwa/routes/auth-register.js +111 -26
- package/dist/pwa/routes/auth-sessions.js +12 -11
- package/dist/pwa/routes/blocklist.js +16 -15
- package/dist/pwa/routes/build-feedback.js +10 -9
- package/dist/pwa/routes/build-reputation.js +6 -2
- package/dist/pwa/routes/build-tasks.js +45 -13
- package/dist/pwa/routes/buyer-feeds.js +27 -25
- package/dist/pwa/routes/cart.js +16 -15
- package/dist/pwa/routes/charity.js +212 -150
- package/dist/pwa/routes/chat.js +42 -43
- package/dist/pwa/routes/checkin-tasks.js +10 -9
- package/dist/pwa/routes/checkout-helpers.js +12 -10
- package/dist/pwa/routes/claim-initiators.js +34 -14
- package/dist/pwa/routes/claim-verify.js +86 -53
- package/dist/pwa/routes/claim-voting.js +43 -18
- package/dist/pwa/routes/contribution-identity.js +164 -0
- package/dist/pwa/routes/contribution-score.js +19 -0
- package/dist/pwa/routes/coupons.js +19 -16
- package/dist/pwa/routes/dashboards.js +18 -16
- package/dist/pwa/routes/dispute-cases.js +25 -24
- package/dist/pwa/routes/disputes-read.js +45 -51
- package/dist/pwa/routes/disputes-write.js +124 -61
- package/dist/pwa/routes/evidence.js +9 -9
- package/dist/pwa/routes/external-anchors.js +13 -12
- package/dist/pwa/routes/feedback.js +29 -33
- package/dist/pwa/routes/flash-sales.js +18 -16
- package/dist/pwa/routes/follows.js +25 -24
- package/dist/pwa/routes/governance-auto-deactivate.js +21 -9
- package/dist/pwa/routes/governance-onboarding.js +70 -59
- package/dist/pwa/routes/group-buys.js +22 -22
- package/dist/pwa/routes/growth.js +34 -31
- package/dist/pwa/routes/import-product.js +12 -10
- package/dist/pwa/routes/kyc.js +9 -8
- package/dist/pwa/routes/leaderboard.js +20 -18
- package/dist/pwa/routes/listings.js +23 -22
- package/dist/pwa/routes/logistics.js +10 -8
- package/dist/pwa/routes/manifests.js +27 -27
- package/dist/pwa/routes/me-data.js +23 -21
- package/dist/pwa/routes/notifications.js +7 -6
- package/dist/pwa/routes/offers.js +30 -12
- package/dist/pwa/routes/orders-action.js +51 -29
- package/dist/pwa/routes/orders-create.js +75 -20
- package/dist/pwa/routes/orders-read.js +21 -20
- package/dist/pwa/routes/p2p-products.js +30 -18
- package/dist/pwa/routes/payments-governance.js +61 -56
- package/dist/pwa/routes/peers.js +9 -8
- package/dist/pwa/routes/pin-receipts.js +13 -13
- package/dist/pwa/routes/products-aliases.js +12 -10
- package/dist/pwa/routes/products-claims.js +36 -17
- package/dist/pwa/routes/products-create.js +53 -38
- package/dist/pwa/routes/products-crud.js +17 -16
- package/dist/pwa/routes/products-links.js +49 -26
- package/dist/pwa/routes/products-list.js +6 -4
- package/dist/pwa/routes/products-meta.js +40 -39
- package/dist/pwa/routes/products-update.js +19 -5
- package/dist/pwa/routes/profile-credentials.js +20 -19
- package/dist/pwa/routes/profile-identity.js +14 -13
- package/dist/pwa/routes/profile-location.js +7 -6
- package/dist/pwa/routes/profile-placement.js +20 -19
- package/dist/pwa/routes/profile-prefs.js +11 -11
- package/dist/pwa/routes/promoter.js +58 -66
- package/dist/pwa/routes/public-build-tasks.js +19 -0
- package/dist/pwa/routes/public-utils.js +108 -46
- package/dist/pwa/routes/push.js +16 -15
- package/dist/pwa/routes/ratings.js +92 -32
- package/dist/pwa/routes/recover-key.js +66 -26
- package/dist/pwa/routes/referral.js +37 -52
- package/dist/pwa/routes/reputation.js +3 -2
- package/dist/pwa/routes/returns.js +76 -73
- package/dist/pwa/routes/reviews.js +41 -18
- package/dist/pwa/routes/rewards-apply.js +16 -15
- package/dist/pwa/routes/rewards-auto-downgrade.js +9 -7
- package/dist/pwa/routes/rewards-escrow-expire.js +7 -5
- package/dist/pwa/routes/rfqs.js +163 -85
- package/dist/pwa/routes/search.js +16 -14
- package/dist/pwa/routes/secondhand.js +25 -22
- package/dist/pwa/routes/seller-quota.js +24 -26
- package/dist/pwa/routes/share-redirects.js +60 -55
- package/dist/pwa/routes/shareables-interactions.js +34 -35
- package/dist/pwa/routes/shareables.js +55 -51
- package/dist/pwa/routes/shop-referral.js +58 -0
- package/dist/pwa/routes/shops.js +25 -20
- package/dist/pwa/routes/signaling.js +10 -9
- package/dist/pwa/routes/skill-market.js +16 -16
- package/dist/pwa/routes/skills.js +15 -14
- package/dist/pwa/routes/snf.js +14 -13
- package/dist/pwa/routes/tags.js +10 -9
- package/dist/pwa/routes/task-proposals.js +121 -0
- package/dist/pwa/routes/trial.js +72 -52
- package/dist/pwa/routes/trusted-kpi.js +20 -18
- package/dist/pwa/routes/url-claim.js +67 -28
- package/dist/pwa/routes/users-public.js +62 -70
- package/dist/pwa/routes/variants.js +12 -13
- package/dist/pwa/routes/verifier-user.js +61 -21
- package/dist/pwa/routes/verify-tasks.js +49 -25
- package/dist/pwa/routes/waitlist.js +16 -15
- package/dist/pwa/routes/wallet-read.js +75 -37
- package/dist/pwa/routes/wallet-write.js +12 -9
- package/dist/pwa/routes/webauthn.js +25 -26
- package/dist/pwa/routes/webhooks.js +26 -26
- package/dist/pwa/routes/welcome.js +45 -50
- package/dist/pwa/routes/wishlist-qa.js +29 -32
- package/dist/pwa/server.js +304 -90
- package/dist/version.js +1 -1
- package/package.json +76 -3
|
@@ -1,32 +1,34 @@
|
|
|
1
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerProductsMetaRoutes(app, deps) {
|
|
2
3
|
const { db, auth, generateId, rateLimitOk, flagNewAccountShareable, refreshProductSharerCount } = deps;
|
|
4
|
+
void db; // RFC-016: 本文件已全量走异步 seam;db 仍在 deps 由调用方注入,此处不直接使用
|
|
3
5
|
const PH_RATE = 60; // 每 IP/分钟 60 次
|
|
4
6
|
const PH_MIN_SAMPLE = 5;
|
|
5
|
-
app.get('/api/products/:id/price-history', (req, res) => {
|
|
7
|
+
app.get('/api/products/:id/price-history', async (req, res) => {
|
|
6
8
|
const ip = req.ip || 'unknown';
|
|
7
9
|
if (!rateLimitOk(`ph:${ip}`, PH_RATE, 60_000))
|
|
8
10
|
return void res.status(429).json({ error: 'rate-limited' });
|
|
9
|
-
const p =
|
|
11
|
+
const p = await dbOne("SELECT id, price as current_price, category, category_id FROM products WHERE id = ?", [req.params.id]);
|
|
10
12
|
if (!p)
|
|
11
13
|
return void res.status(404).json({ error: 'not_found' });
|
|
12
|
-
const lifetimeCount =
|
|
14
|
+
const lifetimeCount = (await dbOne(`SELECT COUNT(1) as n FROM orders WHERE product_id = ? AND status = 'completed'`, [req.params.id])).n;
|
|
13
15
|
if (lifetimeCount < PH_MIN_SAMPLE) {
|
|
14
16
|
return void res.json({ product_id: req.params.id, current_price: p.current_price, insufficient_data: true, lifetime_sales: lifetimeCount, min_sample: PH_MIN_SAMPLE });
|
|
15
17
|
}
|
|
16
|
-
function aggregateWindow(days) {
|
|
18
|
+
async function aggregateWindow(days) {
|
|
17
19
|
const where = days != null
|
|
18
20
|
? `product_id = ? AND status = 'completed' AND created_at > datetime('now', '-' || ? || ' days')`
|
|
19
21
|
: `product_id = ? AND status = 'completed'`;
|
|
20
22
|
const args = days != null ? [req.params.id, days] : [req.params.id];
|
|
21
|
-
const row =
|
|
23
|
+
const row = (await dbOne(`
|
|
22
24
|
SELECT COUNT(1) as sales, COALESCE(SUM(total_amount), 0) as volume,
|
|
23
25
|
COALESCE(AVG(unit_price), 0) as avg
|
|
24
26
|
FROM orders WHERE ${where}
|
|
25
|
-
|
|
27
|
+
`, args));
|
|
26
28
|
if (row.sales === 0)
|
|
27
29
|
return { sales: 0, volume: 0, avg: 0, median: 0, p25: 0, p75: 0 };
|
|
28
30
|
// SQLite 无 percentile 函数;P1 fix #1: LIMIT 5000 防爆
|
|
29
|
-
const prices =
|
|
31
|
+
const prices = (await dbAll(`SELECT unit_price FROM orders WHERE ${where} ORDER BY unit_price ASC LIMIT 5000`, args)).map(r => Number(r.unit_price));
|
|
30
32
|
// P1 fix #2: 真百分位(linear interp)
|
|
31
33
|
const pct = (frac) => {
|
|
32
34
|
if (!prices.length)
|
|
@@ -48,15 +50,15 @@ export function registerProductsMetaRoutes(app, deps) {
|
|
|
48
50
|
p75: pct(0.75),
|
|
49
51
|
};
|
|
50
52
|
}
|
|
51
|
-
const d30 = aggregateWindow(30);
|
|
52
|
-
const d90 = aggregateWindow(90);
|
|
53
|
-
const lifetime = aggregateWindow(null);
|
|
53
|
+
const d30 = await aggregateWindow(30);
|
|
54
|
+
const d90 = await aggregateWindow(90);
|
|
55
|
+
const lifetime = await aggregateWindow(null);
|
|
54
56
|
// 价位分布(90 天内,最多 20 个 bucket)
|
|
55
|
-
const buckets =
|
|
57
|
+
const buckets = await dbAll(`
|
|
56
58
|
SELECT unit_price as price, COUNT(1) as count, COALESCE(SUM(quantity), COUNT(1)) as qty
|
|
57
59
|
FROM orders WHERE product_id = ? AND status = 'completed' AND created_at > datetime('now', '-90 days')
|
|
58
60
|
GROUP BY unit_price ORDER BY unit_price ASC LIMIT 20
|
|
59
|
-
|
|
61
|
+
`, [req.params.id]);
|
|
60
62
|
const totalBucketSales = buckets.reduce((s, b) => s + b.count, 0) || 1;
|
|
61
63
|
const priceBuckets = buckets.map(b => ({
|
|
62
64
|
price: Number(b.price),
|
|
@@ -65,11 +67,11 @@ export function registerProductsMetaRoutes(app, deps) {
|
|
|
65
67
|
pct: Math.round((b.count / totalBucketSales) * 10000) / 100,
|
|
66
68
|
}));
|
|
67
69
|
// 30 日日均价 sparkline — P1 fix #4: 单日 sales=1 不返回 avg(防反查买家身份)
|
|
68
|
-
const daily =
|
|
70
|
+
const daily = await dbAll(`
|
|
69
71
|
SELECT substr(created_at, 1, 10) as date, COUNT(1) as sales, AVG(unit_price) as avg
|
|
70
72
|
FROM orders WHERE product_id = ? AND status = 'completed' AND created_at > datetime('now', '-30 days')
|
|
71
73
|
GROUP BY date ORDER BY date ASC
|
|
72
|
-
|
|
74
|
+
`, [req.params.id]);
|
|
73
75
|
const dailyAvg = daily.map(d => ({
|
|
74
76
|
date: d.date,
|
|
75
77
|
sales: d.sales,
|
|
@@ -78,10 +80,10 @@ export function registerProductsMetaRoutes(app, deps) {
|
|
|
78
80
|
// 同类目近 30 天均价对照(cat_default 不算 meaningful)
|
|
79
81
|
let categoryAvg30d = null;
|
|
80
82
|
if (p.category_id && p.category_id !== 'cat_default') {
|
|
81
|
-
const row =
|
|
83
|
+
const row = (await dbOne(`
|
|
82
84
|
SELECT AVG(o.unit_price) as avg FROM orders o JOIN products p ON p.id = o.product_id
|
|
83
85
|
WHERE p.category_id = ? AND o.status = 'completed' AND o.created_at > datetime('now', '-30 days')
|
|
84
|
-
|
|
86
|
+
`, [p.category_id]));
|
|
85
87
|
categoryAvg30d = row.avg != null ? Math.round(Number(row.avg) * 100) / 100 : null;
|
|
86
88
|
}
|
|
87
89
|
// 异常预警
|
|
@@ -104,23 +106,23 @@ export function registerProductsMetaRoutes(app, deps) {
|
|
|
104
106
|
});
|
|
105
107
|
});
|
|
106
108
|
// 公开预览:未登录可调,返回最小公开信息(分享 banner 用)
|
|
107
|
-
app.get('/api/products/:id/preview', (req, res) => {
|
|
108
|
-
const row =
|
|
109
|
+
app.get('/api/products/:id/preview', async (req, res) => {
|
|
110
|
+
const row = await dbOne(`
|
|
109
111
|
SELECT p.id, p.title, p.price, p.category, u.name as seller_name
|
|
110
112
|
FROM products p
|
|
111
113
|
JOIN users u ON p.seller_id = u.id
|
|
112
114
|
WHERE p.id = ? AND p.status = 'active'
|
|
113
|
-
|
|
115
|
+
`, [req.params.id]);
|
|
114
116
|
if (!row)
|
|
115
117
|
return void res.status(404).json({ error: 'not_found' });
|
|
116
118
|
res.json(row);
|
|
117
119
|
});
|
|
118
120
|
// 分享许可:是否对该商品有 completed 订单
|
|
119
|
-
app.get('/api/products/:id/can-share', (req, res) => {
|
|
121
|
+
app.get('/api/products/:id/can-share', async (req, res) => {
|
|
120
122
|
const user = auth(req, res);
|
|
121
123
|
if (!user)
|
|
122
124
|
return;
|
|
123
|
-
const completed =
|
|
125
|
+
const completed = (await dbOne("SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND product_id = ? AND status = 'completed'", [user.id, req.params.id])).n;
|
|
124
126
|
res.json({
|
|
125
127
|
can_share: completed > 0,
|
|
126
128
|
completed_orders: completed,
|
|
@@ -128,22 +130,22 @@ export function registerProductsMetaRoutes(app, deps) {
|
|
|
128
130
|
});
|
|
129
131
|
});
|
|
130
132
|
// 获取或创建商品 shareable(被 sharePromoLink 用,走 /s/<id> 短链)
|
|
131
|
-
app.post('/api/products/:id/get-or-create-share', (req, res) => {
|
|
133
|
+
app.post('/api/products/:id/get-or-create-share', async (req, res) => {
|
|
132
134
|
const user = auth(req, res);
|
|
133
135
|
if (!user)
|
|
134
136
|
return;
|
|
135
137
|
const productId = req.params.id;
|
|
136
138
|
// RFC-002 §3.5 valuation-layer gate — share_link generation requires opt-in
|
|
137
|
-
const optIn =
|
|
139
|
+
const optIn = (await dbOne("SELECT rewards_opted_in FROM users WHERE id = ?", [user.id]))?.rewards_opted_in ?? 0;
|
|
138
140
|
if (optIn !== 1) {
|
|
139
|
-
const getParam = (key, def) => {
|
|
140
|
-
const r =
|
|
141
|
+
const getParam = async (key, def) => {
|
|
142
|
+
const r = await dbOne("SELECT value FROM protocol_params WHERE key = ?", [key]);
|
|
141
143
|
return r ? Number(r.value) : def;
|
|
142
144
|
};
|
|
143
|
-
const minOrders = getParam('rewards_opt_in.min_completed_orders', 1);
|
|
144
|
-
const requirePasskey = getParam('rewards_opt_in.require_passkey', 1);
|
|
145
|
-
const totalCompleted =
|
|
146
|
-
const passkeyCount =
|
|
145
|
+
const minOrders = await getParam('rewards_opt_in.min_completed_orders', 1);
|
|
146
|
+
const requirePasskey = await getParam('rewards_opt_in.require_passkey', 1);
|
|
147
|
+
const totalCompleted = (await dbOne("SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND status = 'completed'", [user.id])).n;
|
|
148
|
+
const passkeyCount = (await dbOne("SELECT COUNT(*) as n FROM webauthn_credentials WHERE user_id = ?", [user.id])).n;
|
|
147
149
|
const missing = [];
|
|
148
150
|
if (totalCompleted < minOrders)
|
|
149
151
|
missing.push(`completed_orders ${totalCompleted}/${minOrders}`);
|
|
@@ -153,31 +155,30 @@ export function registerProductsMetaRoutes(app, deps) {
|
|
|
153
155
|
missing.push('application_not_submitted');
|
|
154
156
|
return void res.status(403).json({
|
|
155
157
|
error: 'rewards_opt_in_required',
|
|
156
|
-
message_zh: '生成分享链接属于估值层操作 —
|
|
157
|
-
message_en: 'Share-link generation is a valuation-layer action — requires
|
|
158
|
+
message_zh: '生成分享链接属于估值层操作 — 需先开通分享分润 / share-commission opt-in(RFC-002)',
|
|
159
|
+
message_en: 'Share-link generation is a valuation-layer (rewards / share-link) action, NOT a contribution gate — requires rewards / share-commission opt-in (RFC-002)',
|
|
158
160
|
missing_requirements: missing,
|
|
159
161
|
next_steps: [
|
|
160
|
-
'Open PWA #me → tap "
|
|
162
|
+
'Open PWA #me → tap "申请分享分润 / Enable share-commission opt-in"',
|
|
161
163
|
'Read the 8-second disclosure (cannot skip)',
|
|
162
164
|
'Submit application — pre-checks run server-side',
|
|
163
165
|
],
|
|
164
166
|
});
|
|
165
167
|
}
|
|
166
|
-
const completed =
|
|
168
|
+
const completed = (await dbOne("SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND product_id = ? AND status = 'completed'", [user.id, productId])).n;
|
|
167
169
|
if (completed === 0)
|
|
168
170
|
return void res.json({ error: '需先完成该商品的购买才能分享', completed_orders: 0 });
|
|
169
171
|
// 优先复用现有 active shareable
|
|
170
|
-
const existing =
|
|
172
|
+
const existing = await dbOne(`SELECT id, owner_code FROM shareables WHERE owner_id = ? AND related_product_id = ? AND status = 'active' LIMIT 1`, [user.id, productId]);
|
|
171
173
|
if (existing) {
|
|
172
174
|
return void res.json({ ok: true, shareable_id: existing.id, owner_code: existing.owner_code, short_url: `/s/${existing.id}`, reused: true });
|
|
173
175
|
}
|
|
174
176
|
// 创建新 shareable(纯商品分享:无外链,无 native,仅绑 product_id)
|
|
175
177
|
const id = generateId('shr');
|
|
176
|
-
const ownerCode =
|
|
177
|
-
const product =
|
|
178
|
-
|
|
179
|
-
VALUES (?,?,?,?,?,?)
|
|
180
|
-
.run(id, user.id, 'product_promo', product?.title || null, productId, ownerCode);
|
|
178
|
+
const ownerCode = (await dbOne("SELECT permanent_code FROM users WHERE id = ?", [user.id]))?.permanent_code || null;
|
|
179
|
+
const product = await dbOne("SELECT title FROM products WHERE id = ?", [productId]);
|
|
180
|
+
await dbRun(`INSERT INTO shareables (id, owner_id, type, title, related_product_id, owner_code)
|
|
181
|
+
VALUES (?,?,?,?,?,?)`, [id, user.id, 'product_promo', product?.title || null, productId, ownerCode]);
|
|
181
182
|
flagNewAccountShareable(id, user.id);
|
|
182
183
|
refreshProductSharerCount(productId);
|
|
183
184
|
res.json({ ok: true, shareable_id: id, owner_code: ownerCode, short_url: `/s/${id}`, reused: false });
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { dbOne, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerProductsUpdateRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
-
|
|
3
|
+
// db 已走 RFC-016 异步 seam(dbOne/dbRun),不再直接用 deps.db
|
|
4
|
+
const { auth, makeCommitmentHash, makeDescriptionHash, makePriceHash, notifyWaitlist, notifyWishlistPriceDrop, checkStockAndMaybeDelist } = deps;
|
|
5
|
+
app.put('/api/products/:id', async (req, res) => {
|
|
4
6
|
const user = auth(req, res);
|
|
5
7
|
if (!user)
|
|
6
8
|
return;
|
|
7
|
-
const product =
|
|
9
|
+
const product = await dbOne('SELECT * FROM products WHERE id = ? AND seller_id = ?', [req.params.id, user.id]);
|
|
8
10
|
if (!product)
|
|
9
11
|
return void res.status(404).json({ error: '商品不存在或无权限' });
|
|
10
12
|
const { title, description, price, stock, specs, brand, model, handling_hours, ship_regions, estimated_days, fragile, return_days, return_condition, warranty_days, low_stock_threshold, auto_delist_on_zero, origin_claims, i18n_titles, i18n_descs, } = req.body;
|
|
@@ -81,7 +83,7 @@ export function registerProductsUpdateRoutes(app, deps) {
|
|
|
81
83
|
const descsResult = validateI18n(i18n_descs);
|
|
82
84
|
const newI18nTitles = titlesResult === undefined ? product.i18n_titles : titlesResult;
|
|
83
85
|
const newI18nDescs = descsResult === undefined ? product.i18n_descs : descsResult;
|
|
84
|
-
|
|
86
|
+
await dbRun(`UPDATE products SET
|
|
85
87
|
title=?, description=?, price=?, stock=?,
|
|
86
88
|
specs=?, brand=?, model=?, handling_hours=?, ship_regions=?,
|
|
87
89
|
estimated_days=?, fragile=?, return_days=?, return_condition=?, warranty_days=?,
|
|
@@ -91,7 +93,19 @@ export function registerProductsUpdateRoutes(app, deps) {
|
|
|
91
93
|
i18n_titles=?, i18n_descs=?,
|
|
92
94
|
commitment_hash=?, description_hash=?, price_hash=?, hashed_at=?,
|
|
93
95
|
updated_at=datetime('now')
|
|
94
|
-
WHERE id
|
|
96
|
+
WHERE id=?`, [
|
|
97
|
+
newTitle, newDesc, newPrice, newStock,
|
|
98
|
+
specsJson, brand ?? product.brand, model ?? product.model,
|
|
99
|
+
newHandling, newShipRegions, newEstDays, newFragile,
|
|
100
|
+
newReturnDays, newReturnCond, newWarranty,
|
|
101
|
+
newLowThreshold, newAutoDelist, resetAlert,
|
|
102
|
+
newOriginClaims,
|
|
103
|
+
newI18nTitles, newI18nDescs,
|
|
104
|
+
makeCommitmentHash(pFields),
|
|
105
|
+
makeDescriptionHash({ title: newTitle, description: newDesc, specs: specsJson }),
|
|
106
|
+
makePriceHash(newPrice, now), now,
|
|
107
|
+
req.params.id
|
|
108
|
+
]);
|
|
95
109
|
// Wave B-2: stock 从 0 → 正数时通知 waitlist 用户
|
|
96
110
|
if (oldStock === 0 && Number(newStock) > 0) {
|
|
97
111
|
try {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { dbOne, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
2
3
|
export function registerProfileCredentialsRoutes(app, deps) {
|
|
3
|
-
|
|
4
|
+
// db 已全量走 RFC-016 异步 seam(dbOne/dbRun),不再直接用 deps.db
|
|
5
|
+
const { auth, verifyPassword, hashPassword, issueCode, findActiveCode, MAX_CODE_ATTEMPTS } = deps;
|
|
4
6
|
// 设置 / 修改密码
|
|
5
|
-
app.post('/api/profile/set-password', (req, res) => {
|
|
7
|
+
app.post('/api/profile/set-password', async (req, res) => {
|
|
6
8
|
const user = auth(req, res);
|
|
7
9
|
if (!user)
|
|
8
10
|
return;
|
|
@@ -19,8 +21,7 @@ export function registerProfileCredentialsRoutes(app, deps) {
|
|
|
19
21
|
}
|
|
20
22
|
}
|
|
21
23
|
const hash = hashPassword(String(new_password));
|
|
22
|
-
|
|
23
|
-
.run(hash, user.id);
|
|
24
|
+
await dbRun("UPDATE users SET password_hash = ?, failed_attempts = 0, locked_until = NULL, updated_at = datetime('now') WHERE id = ?", [hash, user.id]);
|
|
24
25
|
res.json({ success: true });
|
|
25
26
|
});
|
|
26
27
|
// 验证密码(显示 API Key 前的二次确认)
|
|
@@ -39,7 +40,7 @@ export function registerProfileCredentialsRoutes(app, deps) {
|
|
|
39
40
|
res.json({ ok: true });
|
|
40
41
|
});
|
|
41
42
|
// 移除密码(恢复只用 API Key 模式)
|
|
42
|
-
app.post('/api/profile/remove-password', (req, res) => {
|
|
43
|
+
app.post('/api/profile/remove-password', async (req, res) => {
|
|
43
44
|
const user = auth(req, res);
|
|
44
45
|
if (!user)
|
|
45
46
|
return;
|
|
@@ -49,12 +50,11 @@ export function registerProfileCredentialsRoutes(app, deps) {
|
|
|
49
50
|
if (!current_password || !verifyPassword(String(current_password), user.password_hash)) {
|
|
50
51
|
return void res.json({ error: '密码错误' });
|
|
51
52
|
}
|
|
52
|
-
|
|
53
|
-
.run(user.id);
|
|
53
|
+
await dbRun("UPDATE users SET password_hash = NULL, updated_at = datetime('now') WHERE id = ?", [user.id]);
|
|
54
54
|
res.json({ success: true });
|
|
55
55
|
});
|
|
56
56
|
// 绑定邮箱 — 步骤 1:发码
|
|
57
|
-
app.post('/api/profile/bind-email', (req, res) => {
|
|
57
|
+
app.post('/api/profile/bind-email', async (req, res) => {
|
|
58
58
|
const user = auth(req, res);
|
|
59
59
|
if (!user)
|
|
60
60
|
return;
|
|
@@ -62,19 +62,22 @@ export function registerProfileCredentialsRoutes(app, deps) {
|
|
|
62
62
|
if (!email?.trim() || !EMAIL_RE.test(email.trim()))
|
|
63
63
|
return void res.json({ error: '邮箱格式无效' });
|
|
64
64
|
const target = email.trim().toLowerCase();
|
|
65
|
-
const dup =
|
|
65
|
+
const dup = await dbOne("SELECT 1 FROM users WHERE email = ? AND id != ? LIMIT 1", [target, user.id]);
|
|
66
66
|
if (dup)
|
|
67
67
|
return void res.json({ error: '该邮箱已被其他账户绑定' });
|
|
68
|
-
const
|
|
68
|
+
const issued = await issueCode(user.id, 'email', target, 'bind_email');
|
|
69
|
+
if (!issued.ok) {
|
|
70
|
+
return void res.status(issued.status).json({ error: issued.error, error_code: issued.error_code });
|
|
71
|
+
}
|
|
69
72
|
res.json({
|
|
70
73
|
success: true,
|
|
71
74
|
target_hint: target.replace(/^(.).*(@.*)$/, '$1***$2'),
|
|
72
|
-
expires_at,
|
|
73
|
-
...(
|
|
75
|
+
expires_at: issued.expires_at,
|
|
76
|
+
...(issued.provider === 'dev_console' ? { dev_code: issued.code } : {}),
|
|
74
77
|
});
|
|
75
78
|
});
|
|
76
79
|
// 绑定邮箱 — 步骤 2:确认验证码
|
|
77
|
-
app.post('/api/profile/confirm-email', (req, res) => {
|
|
80
|
+
app.post('/api/profile/confirm-email', async (req, res) => {
|
|
78
81
|
const user = auth(req, res);
|
|
79
82
|
if (!user)
|
|
80
83
|
return;
|
|
@@ -90,16 +93,14 @@ export function registerProfileCredentialsRoutes(app, deps) {
|
|
|
90
93
|
if (String(row.code) !== code.trim()) {
|
|
91
94
|
const attempts = row.attempts + 1;
|
|
92
95
|
if (attempts >= MAX_CODE_ATTEMPTS) {
|
|
93
|
-
|
|
94
|
-
.run(attempts, row.id);
|
|
96
|
+
await dbRun("UPDATE verification_codes SET attempts = ?, used_at = datetime('now') WHERE id = ?", [attempts, row.id]);
|
|
95
97
|
return void res.json({ error: '错误次数过多,验证码已作废,请重新获取' });
|
|
96
98
|
}
|
|
97
|
-
|
|
99
|
+
await dbRun("UPDATE verification_codes SET attempts = ? WHERE id = ?", [attempts, row.id]);
|
|
98
100
|
return void res.json({ error: `验证码错误(剩余 ${MAX_CODE_ATTEMPTS - attempts} 次)` });
|
|
99
101
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
.run(target, user.id);
|
|
102
|
+
await dbRun("UPDATE verification_codes SET used_at = datetime('now') WHERE id = ?", [row.id]);
|
|
103
|
+
await dbRun("UPDATE users SET email = ?, email_verified = 1, updated_at = datetime('now') WHERE id = ?", [target, user.id]);
|
|
103
104
|
res.json({ success: true, email: target });
|
|
104
105
|
});
|
|
105
106
|
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { dbOne, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
const ROLE_LOCKED_ROLES = ['admin', 'verifier'];
|
|
2
3
|
const VALID_REGIONS = new Set(['china', 'us', 'eu', 'india', 'singapore', 'global_north', 'global']);
|
|
3
4
|
const HANDLE_BASE_COOLDOWN_MONTHS = 12;
|
|
4
5
|
export function registerProfileIdentityRoutes(app, deps) {
|
|
6
|
+
// db 仍保留:用于 add-role 的 db.transaction(re-read+write 防丢更新,better-sqlite3 事务须同步)。
|
|
7
|
+
// 其余站点已走 RFC-016 异步 seam(dbOne/dbRun)。
|
|
5
8
|
const { db, generateId, auth, safeRoles } = deps;
|
|
6
9
|
app.post('/api/profile/add-role', (req, res) => {
|
|
7
10
|
const user = auth(req, res);
|
|
@@ -48,7 +51,7 @@ export function registerProfileIdentityRoutes(app, deps) {
|
|
|
48
51
|
return void res.json({ error: '已拥有该角色' });
|
|
49
52
|
res.json({ success: true, roles: finalRoles });
|
|
50
53
|
});
|
|
51
|
-
app.post('/api/profile/switch-role', (req, res) => {
|
|
54
|
+
app.post('/api/profile/switch-role', async (req, res) => {
|
|
52
55
|
const user = auth(req, res);
|
|
53
56
|
if (!user)
|
|
54
57
|
return;
|
|
@@ -63,10 +66,10 @@ export function registerProfileIdentityRoutes(app, deps) {
|
|
|
63
66
|
hint: '权责分离原则;如需买卖请用其他账号。'
|
|
64
67
|
});
|
|
65
68
|
}
|
|
66
|
-
|
|
69
|
+
await dbRun("UPDATE users SET role = ?, updated_at = datetime('now') WHERE id = ?", [role, user.id]);
|
|
67
70
|
res.json({ success: true, role, roles });
|
|
68
71
|
});
|
|
69
|
-
app.post('/api/profile/region', (req, res) => {
|
|
72
|
+
app.post('/api/profile/region', async (req, res) => {
|
|
70
73
|
const user = auth(req, res);
|
|
71
74
|
if (!user)
|
|
72
75
|
return;
|
|
@@ -79,7 +82,7 @@ export function registerProfileIdentityRoutes(app, deps) {
|
|
|
79
82
|
return void res.json({ success: true, region, unchanged: true });
|
|
80
83
|
}
|
|
81
84
|
// 30 天冷却(防规避 MLM 合规 + 历史 commission 已按当时 region 快照结算)
|
|
82
|
-
const lastChange =
|
|
85
|
+
const lastChange = await dbOne(`SELECT created_at FROM region_change_log WHERE user_id = ? ORDER BY created_at DESC LIMIT 1`, [user.id]);
|
|
83
86
|
if (lastChange) {
|
|
84
87
|
const sinceMs = Date.now() - new Date(lastChange.created_at + 'Z').getTime();
|
|
85
88
|
const COOLDOWN_MS = 30 * 24 * 3600 * 1000;
|
|
@@ -91,13 +94,12 @@ export function registerProfileIdentityRoutes(app, deps) {
|
|
|
91
94
|
});
|
|
92
95
|
}
|
|
93
96
|
}
|
|
94
|
-
|
|
97
|
+
await dbRun("UPDATE users SET region = ?, updated_at = datetime('now') WHERE id = ?", [region, user.id]);
|
|
95
98
|
const ip = req.ip || '';
|
|
96
|
-
|
|
97
|
-
.run(generateId('rcl'), user.id, fromRegion, region, ip || null);
|
|
99
|
+
await dbRun(`INSERT INTO region_change_log (id, user_id, from_region, to_region, ip) VALUES (?,?,?,?,?)`, [generateId('rcl'), user.id, fromRegion, region, ip || null]);
|
|
98
100
|
res.json({ success: true, region });
|
|
99
101
|
});
|
|
100
|
-
app.post('/api/profile/change-name', (req, res) => {
|
|
102
|
+
app.post('/api/profile/change-name', async (req, res) => {
|
|
101
103
|
const user = auth(req, res);
|
|
102
104
|
if (!user)
|
|
103
105
|
return;
|
|
@@ -110,11 +112,11 @@ export function registerProfileIdentityRoutes(app, deps) {
|
|
|
110
112
|
if (trimmed === user.name)
|
|
111
113
|
return void res.json({ error: '新昵称与当前相同' });
|
|
112
114
|
// 昵称可重复(唯一标识由 handle / permanent_code 承担)
|
|
113
|
-
|
|
115
|
+
await dbRun("UPDATE users SET name = ?, updated_at = datetime('now') WHERE id = ?", [trimmed, user.id]);
|
|
114
116
|
res.json({ success: true, name: trimmed });
|
|
115
117
|
});
|
|
116
118
|
// 改 handle:累进式冷却 — 第 N 次改需距上次 N × 12 月
|
|
117
|
-
app.post('/api/profile/change-handle', (req, res) => {
|
|
119
|
+
app.post('/api/profile/change-handle', async (req, res) => {
|
|
118
120
|
const user = auth(req, res);
|
|
119
121
|
if (!user)
|
|
120
122
|
return;
|
|
@@ -155,13 +157,12 @@ export function registerProfileIdentityRoutes(app, deps) {
|
|
|
155
157
|
}
|
|
156
158
|
}
|
|
157
159
|
// 唯一性
|
|
158
|
-
const dup =
|
|
160
|
+
const dup = await dbOne("SELECT 1 FROM users WHERE handle = ? AND id != ?", [raw, user.id]);
|
|
159
161
|
if (dup)
|
|
160
162
|
return void res.json({ error: '该用户名已被占用' });
|
|
161
163
|
log.push({ at: new Date().toISOString().slice(0, 19).replace('T', ' '), from: String(user.handle || '') });
|
|
162
164
|
// 全量保留 — 累进式冷却需要完整历史
|
|
163
|
-
|
|
164
|
-
.run(raw, JSON.stringify(log), user.id);
|
|
165
|
+
await dbRun(`UPDATE users SET handle = ?, handle_last_created_at = datetime('now'), handle_change_log = ?, updated_at = datetime('now') WHERE id = ?`, [raw, JSON.stringify(log), user.id]);
|
|
165
166
|
const nextRequiredMonths = (N + 1) * HANDLE_BASE_COOLDOWN_MONTHS;
|
|
166
167
|
res.json({
|
|
167
168
|
success: true,
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
import { dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
function quantizeCoord(value, precision_deg) {
|
|
2
3
|
// 例:precision=0.1 → factor=10;precision=0.05 → factor=20
|
|
3
4
|
const factor = 1 / precision_deg;
|
|
4
5
|
return Math.round(value * factor) / factor;
|
|
5
6
|
}
|
|
6
7
|
export function registerProfileLocationRoutes(app, deps) {
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
// db 已全量走 RFC-016 异步 seam(dbRun),不再直接用 deps.db
|
|
9
|
+
const { auth, getNearbyCellPrecision } = deps;
|
|
10
|
+
app.post('/api/profile/set-location', async (req, res) => {
|
|
9
11
|
const user = auth(req, res);
|
|
10
12
|
if (!user)
|
|
11
13
|
return;
|
|
@@ -21,15 +23,14 @@ export function registerProfileLocationRoutes(app, deps) {
|
|
|
21
23
|
// 服务端截断(防客户端传精确坐标)
|
|
22
24
|
const lat = quantizeCoord(rawLat, precision_deg);
|
|
23
25
|
const lng = quantizeCoord(rawLng, precision_deg);
|
|
24
|
-
|
|
25
|
-
.run(lat, lng, user.id);
|
|
26
|
+
await dbRun("UPDATE users SET geo_lat = ?, geo_lng = ?, geo_updated_at = datetime('now'), updated_at = datetime('now') WHERE id = ?", [lat, lng, user.id]);
|
|
26
27
|
res.json({ ok: true, lat, lng, precision_deg, approx_km });
|
|
27
28
|
});
|
|
28
|
-
app.post('/api/profile/clear-location', (req, res) => {
|
|
29
|
+
app.post('/api/profile/clear-location', async (req, res) => {
|
|
29
30
|
const user = auth(req, res);
|
|
30
31
|
if (!user)
|
|
31
32
|
return;
|
|
32
|
-
|
|
33
|
+
await dbRun("UPDATE users SET geo_lat = NULL, geo_lng = NULL, geo_updated_at = NULL WHERE id = ?", [user.id]);
|
|
33
34
|
res.json({ ok: true });
|
|
34
35
|
});
|
|
35
36
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { dbOne, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerProfilePlacementRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
-
|
|
3
|
+
// db 已全量走 RFC-016 异步 seam(dbOne/dbRun),不再直接用 deps.db
|
|
4
|
+
const { auth, internalAuditorId, resolveInviteCodeRef, pickPreferredSide, joinPowerLeg } = deps;
|
|
5
|
+
app.get('/api/profile/placement-status', async (req, res) => {
|
|
4
6
|
const user = auth(req, res);
|
|
5
7
|
if (!user)
|
|
6
8
|
return;
|
|
7
|
-
const u =
|
|
9
|
+
const u = await dbOne("SELECT placement_id, placement_side, left_child_id, right_child_id, placement_pref FROM users WHERE id = ?", [user.id]);
|
|
8
10
|
const hasPlacement = !!u?.placement_id;
|
|
9
11
|
const hasDownline = !!u?.left_child_id || !!u?.right_child_id;
|
|
10
12
|
res.json({
|
|
@@ -16,36 +18,35 @@ export function registerProfilePlacementRoutes(app, deps) {
|
|
|
16
18
|
placement_side: u?.placement_side,
|
|
17
19
|
});
|
|
18
20
|
});
|
|
19
|
-
app.post('/api/profile/bind-placement', (req, res) => {
|
|
21
|
+
app.post('/api/profile/bind-placement', async (req, res) => {
|
|
20
22
|
const user = auth(req, res);
|
|
21
23
|
if (!user)
|
|
22
24
|
return;
|
|
23
|
-
const { inviter_id
|
|
25
|
+
const { inviter_id } = req.body;
|
|
24
26
|
if (!inviter_id || typeof inviter_id !== 'string')
|
|
25
27
|
return void res.json({ error: '请提供 inviter_id' });
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
// invite-code ONLY: 6-7 位 permanent_code。usr_xxx / @handle / 裸 handle 不再接受 —
|
|
29
|
+
// 这是积分树关系绑定入口,必须与收窄后的邀请面一致。
|
|
30
|
+
const ref = resolveInviteCodeRef(inviter_id);
|
|
31
|
+
if (!ref)
|
|
32
|
+
return void res.json({ error: 'inviter 邀请码无效(仅 6-7 位永久码)' });
|
|
33
|
+
const resolvedInviterId = ref.userId;
|
|
30
34
|
if (resolvedInviterId === user.id)
|
|
31
35
|
return void res.json({ error: '不能挂靠到自己' });
|
|
32
|
-
const u =
|
|
36
|
+
const u = await dbOne("SELECT placement_id, left_child_id, right_child_id FROM users WHERE id = ?", [user.id]);
|
|
33
37
|
if (u?.placement_id)
|
|
34
38
|
return void res.json({ error: '你已在双轨树中(永久第一触点,不可改)' });
|
|
35
39
|
if (u?.left_child_id || u?.right_child_id)
|
|
36
40
|
return void res.json({ error: '你已有下线,不可补绑(防破坏树结构)' });
|
|
37
|
-
const inviter =
|
|
38
|
-
.get(resolvedInviterId, internalAuditorId);
|
|
41
|
+
const inviter = await dbOne("SELECT id, placement_path FROM users WHERE id = ? AND id NOT IN ('sys_protocol', ?)", [resolvedInviterId, internalAuditorId]);
|
|
39
42
|
if (!inviter)
|
|
40
43
|
return void res.json({ error: 'inviter 不存在' });
|
|
41
44
|
// 环路检查:inviter 的 placement_path 不能含自己
|
|
42
45
|
if ((inviter.placement_path || '').split('>').includes(user.id)) {
|
|
43
46
|
return void res.json({ error: '检测到环路(你已是 inviter 的上线)' });
|
|
44
47
|
}
|
|
45
|
-
//
|
|
46
|
-
const chosenSide = (
|
|
47
|
-
? side
|
|
48
|
-
: pickPreferredSide(inviter.id);
|
|
48
|
+
// pre-public 去左右码:忽略用户/邀请码指定的左右侧,放置侧别永远由系统自动决定
|
|
49
|
+
const chosenSide = pickPreferredSide(inviter.id);
|
|
49
50
|
try {
|
|
50
51
|
const placed = joinPowerLeg(inviter.id, chosenSide, user.id);
|
|
51
52
|
res.json({ success: true, inviter_id: inviter.id, side: chosenSide, depth: placed.depth });
|
|
@@ -54,7 +55,7 @@ export function registerProfilePlacementRoutes(app, deps) {
|
|
|
54
55
|
res.json({ error: `挂靠失败: ${e.message}` });
|
|
55
56
|
}
|
|
56
57
|
});
|
|
57
|
-
app.post('/api/profile/placement-pref', (req, res) => {
|
|
58
|
+
app.post('/api/profile/placement-pref', async (req, res) => {
|
|
58
59
|
const user = auth(req, res);
|
|
59
60
|
if (!user)
|
|
60
61
|
return;
|
|
@@ -64,7 +65,7 @@ export function registerProfilePlacementRoutes(app, deps) {
|
|
|
64
65
|
}
|
|
65
66
|
// legacy left/right 已不再支持长期强偏,无声折算为 team_count(agent 兼容期保护)
|
|
66
67
|
const stored = (pref === 'left' || pref === 'right') ? 'team_count' : pref;
|
|
67
|
-
|
|
68
|
-
res.json({ success: true, placement_pref: stored, coerced: stored !== pref ? `${pref} → ${stored}
|
|
68
|
+
await dbRun("UPDATE users SET placement_pref = ?, updated_at = datetime('now') WHERE id = ?", [stored, user.id]);
|
|
69
|
+
res.json({ success: true, placement_pref: stored, coerced: stored !== pref ? `${pref} → ${stored}(已统一为长期默认偏好)` : undefined });
|
|
69
70
|
});
|
|
70
71
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { dbOne, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerProfilePrefsRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
+
// db 已全量走 RFC-016 异步 seam(dbOne/dbRun),不再直接用 deps.db
|
|
4
|
+
const { auth } = deps;
|
|
3
5
|
// 默认地址(结构化 + 兼容旧 text/region)
|
|
4
|
-
app.post('/api/profile/default-address', (req, res) => {
|
|
6
|
+
app.post('/api/profile/default-address', async (req, res) => {
|
|
5
7
|
const user = auth(req, res);
|
|
6
8
|
if (!user)
|
|
7
9
|
return;
|
|
@@ -10,8 +12,7 @@ export function registerProfilePrefsRoutes(app, deps) {
|
|
|
10
12
|
if (body.text !== undefined || body.region !== undefined) {
|
|
11
13
|
const text = (body.text || '').toString().trim().slice(0, 200);
|
|
12
14
|
const region = (body.region || '').toString().trim().slice(0, 40);
|
|
13
|
-
|
|
14
|
-
.run(text || null, region || null, user.id);
|
|
15
|
+
await dbRun("UPDATE users SET default_address_text = ?, default_address_region = ?, updated_at = datetime('now') WHERE id = ?", [text || null, region || null, user.id]);
|
|
15
16
|
return void res.json({ ok: true, text: text || null, region: region || null });
|
|
16
17
|
}
|
|
17
18
|
// 结构化模式
|
|
@@ -42,21 +43,20 @@ export function registerProfilePrefsRoutes(app, deps) {
|
|
|
42
43
|
}
|
|
43
44
|
const structured = { line1, line2, country, state, city, recipient_name: recipient, phone1, phone2, postal_code: postal };
|
|
44
45
|
const text = [recipient, `${country} ${state} ${city}`.trim(), line1, line2, postal, phone1].filter(Boolean).join(' / ').slice(0, 200);
|
|
45
|
-
|
|
46
|
-
.run(text || null, state || null, JSON.stringify(structured), user.id);
|
|
46
|
+
await dbRun("UPDATE users SET default_address_text = ?, default_address_region = ?, default_address_json = ?, updated_at = datetime('now') WHERE id = ?", [text || null, state || null, JSON.stringify(structured), user.id]);
|
|
47
47
|
res.json({ ok: true, address: structured, derived_text: text, derived_region: state });
|
|
48
48
|
});
|
|
49
49
|
// 隐私开关(旧 API,向后兼容;新代码用 PATCH /api/profile)
|
|
50
|
-
app.patch('/api/profile/feed-visible', (req, res) => {
|
|
50
|
+
app.patch('/api/profile/feed-visible', async (req, res) => {
|
|
51
51
|
const user = auth(req, res);
|
|
52
52
|
if (!user)
|
|
53
53
|
return;
|
|
54
54
|
const v = req.body?.visible ? 1 : 0;
|
|
55
|
-
|
|
55
|
+
await dbRun("UPDATE users SET feed_visible = ?, updated_at = datetime('now') WHERE id = ?", [v, user.id]);
|
|
56
56
|
res.json({ ok: true, feed_visible: v });
|
|
57
57
|
});
|
|
58
58
|
// 通用 profile patch(search_anchor / bio / feed_visible)
|
|
59
|
-
app.patch('/api/profile', (req, res) => {
|
|
59
|
+
app.patch('/api/profile', async (req, res) => {
|
|
60
60
|
const user = auth(req, res);
|
|
61
61
|
if (!user)
|
|
62
62
|
return;
|
|
@@ -86,8 +86,8 @@ export function registerProfilePrefsRoutes(app, deps) {
|
|
|
86
86
|
return void res.json({ error: '没有可更新的字段' });
|
|
87
87
|
updates.push(`updated_at = datetime('now')`);
|
|
88
88
|
values.push(user.id);
|
|
89
|
-
|
|
90
|
-
const u =
|
|
89
|
+
await dbRun(`UPDATE users SET ${updates.join(', ')} WHERE id = ?`, values);
|
|
90
|
+
const u = (await dbOne("SELECT search_anchor, bio, feed_visible FROM users WHERE id = ?", [user.id]));
|
|
91
91
|
res.json({ ok: true, search_anchor: u.search_anchor, bio: u.bio, feed_visible: Number(u.feed_visible ?? 1) });
|
|
92
92
|
});
|
|
93
93
|
}
|