@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,5 +1,6 @@
|
|
|
1
1
|
import { transition } from '../../layer0-foundation/L0-2-state-machine/engine.js';
|
|
2
2
|
import { notifyTransition } from '../../layer2-business/L2-6-notifications/notification-engine.js';
|
|
3
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
3
4
|
const SH_CATEGORIES = new Set(['phone', 'computer', 'appliance', 'furniture', 'clothing', 'book', 'toy', 'sports', 'other']);
|
|
4
5
|
const SH_CONDITIONS = new Set(['brand_new', 'like_new', 'lightly_used', 'well_used', 'heavily_used']);
|
|
5
6
|
const SH_FULFILLMENT = new Set(['shipping', 'in_person', 'both']);
|
|
@@ -8,9 +9,11 @@ function addHours(date, hours) {
|
|
|
8
9
|
return new Date(date.getTime() + hours * 3_600_000).toISOString();
|
|
9
10
|
}
|
|
10
11
|
export function registerSecondhandRoutes(app, deps) {
|
|
12
|
+
// db 仍保留:order 下单是 money/escrow 路径(pragma FK-OFF 窗口 + CAS + 钱包扣减),
|
|
13
|
+
// 不能引入 await 否则会在 FK-OFF 窗口被其他请求并发穿插;随 order/资金路径在 Phase 3 一并迁移。
|
|
11
14
|
const { db, generateId, auth, errorRes } = deps;
|
|
12
15
|
// 1. 发布
|
|
13
|
-
app.post('/api/secondhand', (req, res) => {
|
|
16
|
+
app.post('/api/secondhand', async (req, res) => {
|
|
14
17
|
const user = auth(req, res);
|
|
15
18
|
if (!user)
|
|
16
19
|
return;
|
|
@@ -32,12 +35,12 @@ export function registerSecondhandRoutes(app, deps) {
|
|
|
32
35
|
const ff = SH_FULFILLMENT.has(fulfillment) ? fulfillment : 'both';
|
|
33
36
|
const reg = region ? String(region).trim().slice(0, 40) : null;
|
|
34
37
|
const id = generateId('shi');
|
|
35
|
-
|
|
36
|
-
VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
|
38
|
+
await dbRun(`INSERT INTO secondhand_items (id, seller_id, title, description, category, condition_grade, price, negotiable, images, region, fulfillment)
|
|
39
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?)`, [id, user.id, t, desc, category, condition_grade, p, negotiable ? 1 : 0, JSON.stringify(imgs), reg, ff]);
|
|
37
40
|
res.json({ success: true, id });
|
|
38
41
|
});
|
|
39
42
|
// 2. 列表(市场入口)
|
|
40
|
-
app.get('/api/secondhand', (req, res) => {
|
|
43
|
+
app.get('/api/secondhand', async (req, res) => {
|
|
41
44
|
const category = String(req.query.category || '').trim();
|
|
42
45
|
const conditionList = String(req.query.condition || '').split(',').map(s => s.trim()).filter(s => SH_CONDITIONS.has(s));
|
|
43
46
|
const region = String(req.query.region || '').trim();
|
|
@@ -76,18 +79,18 @@ export function registerSecondhandRoutes(app, deps) {
|
|
|
76
79
|
// 排除自己(如登录)
|
|
77
80
|
const me = (req.headers.authorization || '').replace('Bearer ', '');
|
|
78
81
|
if (me) {
|
|
79
|
-
const u =
|
|
82
|
+
const u = await dbOne("SELECT id FROM users WHERE api_key = ?", [me]);
|
|
80
83
|
if (u) {
|
|
81
84
|
where.push('seller_id != ?');
|
|
82
85
|
args.push(u.id);
|
|
83
86
|
}
|
|
84
87
|
}
|
|
85
|
-
const rows =
|
|
88
|
+
const rows = await dbAll(`SELECT si.id, si.seller_id, si.title, si.category, si.condition_grade, si.price, si.negotiable,
|
|
86
89
|
si.region, si.fulfillment, si.images, si.view_count, si.created_at,
|
|
87
90
|
u.name as seller_name, u.handle as seller_handle
|
|
88
91
|
FROM secondhand_items si JOIN users u ON u.id = si.seller_id
|
|
89
92
|
WHERE ${where.join(' AND ')}
|
|
90
|
-
ORDER BY ${orderBy} LIMIT ${limit}
|
|
93
|
+
ORDER BY ${orderBy} LIMIT ${limit}`, args);
|
|
91
94
|
for (const r of rows) {
|
|
92
95
|
try {
|
|
93
96
|
const arr = JSON.parse(r.images);
|
|
@@ -101,13 +104,13 @@ export function registerSecondhandRoutes(app, deps) {
|
|
|
101
104
|
res.json({ items: rows });
|
|
102
105
|
});
|
|
103
106
|
// 3. 我的二手发布
|
|
104
|
-
app.get('/api/secondhand/mine', (req, res) => {
|
|
107
|
+
app.get('/api/secondhand/mine', async (req, res) => {
|
|
105
108
|
const user = auth(req, res);
|
|
106
109
|
if (!user)
|
|
107
110
|
return;
|
|
108
|
-
const rows =
|
|
111
|
+
const rows = await dbAll(`SELECT id, title, category, condition_grade, price, negotiable, status,
|
|
109
112
|
region, fulfillment, images, view_count, created_at, sold_at, sold_order_id
|
|
110
|
-
FROM secondhand_items WHERE seller_id = ? ORDER BY created_at DESC LIMIT 100
|
|
113
|
+
FROM secondhand_items WHERE seller_id = ? ORDER BY created_at DESC LIMIT 100`, [user.id]);
|
|
111
114
|
for (const r of rows) {
|
|
112
115
|
try {
|
|
113
116
|
const arr = JSON.parse(r.images);
|
|
@@ -118,33 +121,33 @@ export function registerSecondhandRoutes(app, deps) {
|
|
|
118
121
|
}
|
|
119
122
|
delete r.images;
|
|
120
123
|
}
|
|
121
|
-
const stats =
|
|
124
|
+
const stats = (await dbOne(`SELECT
|
|
122
125
|
SUM(CASE WHEN status='available' THEN 1 ELSE 0 END) as available_count,
|
|
123
126
|
SUM(CASE WHEN status='reserved' THEN 1 ELSE 0 END) as reserved_count,
|
|
124
127
|
SUM(CASE WHEN status='sold' THEN 1 ELSE 0 END) as sold_count,
|
|
125
128
|
SUM(CASE WHEN status='closed' THEN 1 ELSE 0 END) as closed_count,
|
|
126
129
|
COALESCE(SUM(CASE WHEN status='sold' THEN price ELSE 0 END), 0) as gross_sold_amount
|
|
127
|
-
FROM secondhand_items WHERE seller_id =
|
|
130
|
+
FROM secondhand_items WHERE seller_id = ?`, [user.id]));
|
|
128
131
|
// 估算净收入(扣 1% 协议 + 1% 基金)
|
|
129
132
|
stats.estimated_earned = Math.round((stats.gross_sold_amount || 0) * 0.98 * 100) / 100;
|
|
130
133
|
res.json({ items: rows, stats });
|
|
131
134
|
});
|
|
132
135
|
// 4. 详情(view_count++)+ 同卖家其他在售
|
|
133
|
-
app.get('/api/secondhand/:id', (req, res) => {
|
|
134
|
-
const row =
|
|
135
|
-
FROM secondhand_items si JOIN users u ON u.id = si.seller_id WHERE si.id =
|
|
136
|
+
app.get('/api/secondhand/:id', async (req, res) => {
|
|
137
|
+
const row = await dbOne(`SELECT si.*, u.name as seller_name, u.handle as seller_handle, u.permanent_code as seller_code
|
|
138
|
+
FROM secondhand_items si JOIN users u ON u.id = si.seller_id WHERE si.id = ?`, [req.params.id]);
|
|
136
139
|
if (!row)
|
|
137
140
|
return void res.status(404).json({ error: '物品不存在' });
|
|
138
|
-
|
|
141
|
+
await dbRun('UPDATE secondhand_items SET view_count = view_count + 1 WHERE id = ?', [req.params.id]);
|
|
139
142
|
try {
|
|
140
143
|
row.images = JSON.parse(row.images);
|
|
141
144
|
}
|
|
142
145
|
catch {
|
|
143
146
|
row.images = [];
|
|
144
147
|
}
|
|
145
|
-
const sellerOthers =
|
|
148
|
+
const sellerOthers = await dbAll(`SELECT id, title, category, condition_grade, price, images, region
|
|
146
149
|
FROM secondhand_items WHERE seller_id = ? AND status='available' AND id != ?
|
|
147
|
-
ORDER BY created_at DESC LIMIT 6
|
|
150
|
+
ORDER BY created_at DESC LIMIT 6`, [row.seller_id, req.params.id]);
|
|
148
151
|
for (const o of sellerOthers) {
|
|
149
152
|
try {
|
|
150
153
|
const arr = JSON.parse(o.images);
|
|
@@ -158,11 +161,11 @@ export function registerSecondhandRoutes(app, deps) {
|
|
|
158
161
|
res.json({ item: row, seller_others: sellerOthers });
|
|
159
162
|
});
|
|
160
163
|
// 5. 编辑(仅 owner;可改 price / description / negotiable / status / fulfillment)
|
|
161
|
-
app.patch('/api/secondhand/:id', (req, res) => {
|
|
164
|
+
app.patch('/api/secondhand/:id', async (req, res) => {
|
|
162
165
|
const user = auth(req, res);
|
|
163
166
|
if (!user)
|
|
164
167
|
return;
|
|
165
|
-
const item =
|
|
168
|
+
const item = await dbOne('SELECT * FROM secondhand_items WHERE id = ?', [req.params.id]);
|
|
166
169
|
if (!item)
|
|
167
170
|
return void res.status(404).json({ error: '物品不存在' });
|
|
168
171
|
if (item.seller_id !== user.id)
|
|
@@ -210,10 +213,10 @@ export function registerSecondhandRoutes(app, deps) {
|
|
|
210
213
|
return void res.status(400).json({ error: '无字段更新' });
|
|
211
214
|
sets.push("updated_at = datetime('now')");
|
|
212
215
|
args.push(req.params.id);
|
|
213
|
-
|
|
216
|
+
await dbRun(`UPDATE secondhand_items SET ${sets.join(', ')} WHERE id = ?`, args);
|
|
214
217
|
res.json({ success: true });
|
|
215
218
|
});
|
|
216
|
-
// 6. 下单(CAS
|
|
219
|
+
// 6. 下单(CAS 锁库存)— money/escrow + pragma FK-OFF 窗口,保持同步,Phase 3 随资金路径迁移
|
|
217
220
|
app.post('/api/secondhand/:id/order', (req, res) => {
|
|
218
221
|
const user = auth(req, res);
|
|
219
222
|
if (!user)
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerSellerQuotaRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
+
// db 已走 RFC-016 异步 seam(dbOne/dbAll/dbRun),不再直接用 deps.db
|
|
4
|
+
const { generateId, auth, requireUsersAdmin, safeRoles, checkSellerCanList, adminCanOperateOn, logAdminAction, QUOTA_TIERS } = deps;
|
|
3
5
|
// 配额状态
|
|
4
|
-
app.get('/api/seller/quota-status', (req, res) => {
|
|
6
|
+
app.get('/api/seller/quota-status', async (req, res) => {
|
|
5
7
|
const user = auth(req, res);
|
|
6
8
|
if (!user)
|
|
7
9
|
return;
|
|
@@ -9,7 +11,7 @@ export function registerSellerQuotaRoutes(app, deps) {
|
|
|
9
11
|
return void res.json({ error: '仅卖家可查看配额' });
|
|
10
12
|
}
|
|
11
13
|
const status = checkSellerCanList(user);
|
|
12
|
-
const pending =
|
|
14
|
+
const pending = await dbOne("SELECT id, requested_quota, applied_at FROM quota_increase_applications WHERE user_id = ? AND status = 'pending' ORDER BY applied_at DESC LIMIT 1", [user.id]);
|
|
13
15
|
const max = Number(user.max_products ?? 200);
|
|
14
16
|
const nextTierIdx = QUOTA_TIERS.indexOf(max);
|
|
15
17
|
const nextTier = nextTierIdx >= 0 && nextTierIdx < QUOTA_TIERS.length - 1 ? QUOTA_TIERS[nextTierIdx + 1] : null;
|
|
@@ -28,7 +30,7 @@ export function registerSellerQuotaRoutes(app, deps) {
|
|
|
28
30
|
});
|
|
29
31
|
});
|
|
30
32
|
// 数据中心(30d GMV / 7d 曲线 / Top 5 / 客户洞察 / 状态分布)
|
|
31
|
-
app.get('/api/seller/insights', (req, res) => {
|
|
33
|
+
app.get('/api/seller/insights', async (req, res) => {
|
|
32
34
|
const user = auth(req, res);
|
|
33
35
|
if (!user)
|
|
34
36
|
return;
|
|
@@ -42,7 +44,7 @@ export function registerSellerQuotaRoutes(app, deps) {
|
|
|
42
44
|
const fmt = (d) => d.toISOString().replace('T', ' ').slice(0, 19);
|
|
43
45
|
const d30 = fmt(new Date(now - 30 * 86400000));
|
|
44
46
|
const d60 = fmt(new Date(now - 60 * 86400000));
|
|
45
|
-
const orders =
|
|
47
|
+
const orders = await dbAll(`
|
|
46
48
|
SELECT o.id, o.product_id, o.buyer_id, o.status, o.total_amount, o.created_at,
|
|
47
49
|
COALESCE(p.title, '已下架商品') as product_title,
|
|
48
50
|
COALESCE(ub.name, '匿名') as buyer_name
|
|
@@ -51,7 +53,7 @@ export function registerSellerQuotaRoutes(app, deps) {
|
|
|
51
53
|
LEFT JOIN users ub ON o.buyer_id = ub.id
|
|
52
54
|
WHERE o.seller_id = ? AND o.created_at >= ?
|
|
53
55
|
ORDER BY o.created_at DESC
|
|
54
|
-
|
|
56
|
+
`, [sellerId, d60]);
|
|
55
57
|
const completedStatuses = new Set(['completed', 'confirmed']);
|
|
56
58
|
const disputedStatuses = new Set(['disputed', 'fault_seller', 'fault_buyer', 'fault_logistics', 'resolved_for_seller', 'refunded_partial', 'refunded_full', 'dispute_dismissed']);
|
|
57
59
|
const cancelledStatuses = new Set(['cancelled', 'expired']);
|
|
@@ -135,7 +137,7 @@ export function registerSellerQuotaRoutes(app, deps) {
|
|
|
135
137
|
status_breakdown: statusBreakdown,
|
|
136
138
|
});
|
|
137
139
|
});
|
|
138
|
-
app.post('/api/seller/apply-quota-increase', (req, res) => {
|
|
140
|
+
app.post('/api/seller/apply-quota-increase', async (req, res) => {
|
|
139
141
|
const user = auth(req, res);
|
|
140
142
|
if (!user)
|
|
141
143
|
return;
|
|
@@ -152,45 +154,43 @@ export function registerSellerQuotaRoutes(app, deps) {
|
|
|
152
154
|
if (Number(requested_quota) !== nextTier) {
|
|
153
155
|
return void res.json({ error: `下一档配额应为 ${nextTier}` });
|
|
154
156
|
}
|
|
155
|
-
const existing =
|
|
157
|
+
const existing = await dbOne("SELECT 1 FROM quota_increase_applications WHERE user_id = ? AND status = 'pending' LIMIT 1", [user.id]);
|
|
156
158
|
if (existing)
|
|
157
159
|
return void res.json({ error: '已有待审申请' });
|
|
158
|
-
|
|
159
|
-
.run(generateId('qapp'), user.id, current, nextTier, (reason || '').toString().slice(0, 500));
|
|
160
|
+
await dbRun(`INSERT INTO quota_increase_applications (id, user_id, current_quota, requested_quota, reason) VALUES (?,?,?,?,?)`, [generateId('qapp'), user.id, current, nextTier, (reason || '').toString().slice(0, 500)]);
|
|
160
161
|
res.json({ success: true, requested_quota: nextTier });
|
|
161
162
|
});
|
|
162
|
-
app.post('/api/seller/withdraw-quota-application', (req, res) => {
|
|
163
|
+
app.post('/api/seller/withdraw-quota-application', async (req, res) => {
|
|
163
164
|
const user = auth(req, res);
|
|
164
165
|
if (!user)
|
|
165
166
|
return;
|
|
166
|
-
const pending =
|
|
167
|
+
const pending = await dbOne("SELECT id FROM quota_increase_applications WHERE user_id = ? AND status = 'pending' LIMIT 1", [user.id]);
|
|
167
168
|
if (!pending)
|
|
168
169
|
return void res.json({ error: '没有待审申请' });
|
|
169
|
-
|
|
170
|
+
await dbRun("UPDATE quota_increase_applications SET status = 'withdrawn', reviewed_at = datetime('now') WHERE id = ?", [pending.id]);
|
|
170
171
|
res.json({ success: true });
|
|
171
172
|
});
|
|
172
173
|
// Admin
|
|
173
|
-
app.get('/api/admin/quota-applications', (req, res) => {
|
|
174
|
+
app.get('/api/admin/quota-applications', async (req, res) => {
|
|
174
175
|
const admin = requireUsersAdmin(req, res);
|
|
175
176
|
if (!admin)
|
|
176
177
|
return;
|
|
177
178
|
const status = req.query.status || 'pending';
|
|
178
|
-
const rows =
|
|
179
|
+
const rows = await dbAll(`
|
|
179
180
|
SELECT qa.*, u.name as user_name, u.email
|
|
180
181
|
FROM quota_increase_applications qa
|
|
181
182
|
LEFT JOIN users u ON u.id = qa.user_id
|
|
182
183
|
WHERE qa.status = ?
|
|
183
184
|
ORDER BY qa.applied_at DESC LIMIT 100
|
|
184
|
-
|
|
185
|
+
`, [status]);
|
|
185
186
|
res.json({ applications: rows });
|
|
186
187
|
});
|
|
187
|
-
app.post('/api/admin/quota-applications/:id/approve', (req, res) => {
|
|
188
|
+
app.post('/api/admin/quota-applications/:id/approve', async (req, res) => {
|
|
188
189
|
const admin = requireUsersAdmin(req, res);
|
|
189
190
|
if (!admin)
|
|
190
191
|
return;
|
|
191
192
|
const { note } = req.body;
|
|
192
|
-
const appRow =
|
|
193
|
-
.get(req.params.id);
|
|
193
|
+
const appRow = await dbOne("SELECT id, user_id, requested_quota, status FROM quota_increase_applications WHERE id = ?", [req.params.id]);
|
|
194
194
|
if (!appRow)
|
|
195
195
|
return void res.json({ error: '申请不存在' });
|
|
196
196
|
if (!adminCanOperateOn(admin, appRow.user_id, res))
|
|
@@ -199,26 +199,24 @@ export function registerSellerQuotaRoutes(app, deps) {
|
|
|
199
199
|
return void res.json({ error: '该申请不在待审状态' });
|
|
200
200
|
if (!QUOTA_TIERS.includes(appRow.requested_quota))
|
|
201
201
|
return void res.json({ error: '请求配额不合法' });
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
db.prepare("UPDATE users SET max_products = ?, updated_at = datetime('now') WHERE id = ?").run(appRow.requested_quota, appRow.user_id);
|
|
202
|
+
await dbRun("UPDATE quota_increase_applications SET status='approved', reviewed_at=datetime('now'), reviewed_by=?, decision_note=? WHERE id=?", [admin.id, note || null, appRow.id]);
|
|
203
|
+
await dbRun("UPDATE users SET max_products = ?, updated_at = datetime('now') WHERE id = ?", [appRow.requested_quota, appRow.user_id]);
|
|
205
204
|
logAdminAction(admin.id, 'approve_quota_increase', 'user', appRow.user_id, { quota: appRow.requested_quota, note });
|
|
206
205
|
res.json({ success: true });
|
|
207
206
|
});
|
|
208
|
-
app.post('/api/admin/quota-applications/:id/reject', (req, res) => {
|
|
207
|
+
app.post('/api/admin/quota-applications/:id/reject', async (req, res) => {
|
|
209
208
|
const admin = requireUsersAdmin(req, res);
|
|
210
209
|
if (!admin)
|
|
211
210
|
return;
|
|
212
211
|
const { note } = req.body;
|
|
213
|
-
const appRow =
|
|
212
|
+
const appRow = await dbOne("SELECT id, user_id, status FROM quota_increase_applications WHERE id = ?", [req.params.id]);
|
|
214
213
|
if (!appRow)
|
|
215
214
|
return void res.json({ error: '申请不存在' });
|
|
216
215
|
if (!adminCanOperateOn(admin, appRow.user_id, res))
|
|
217
216
|
return;
|
|
218
217
|
if (appRow.status !== 'pending')
|
|
219
218
|
return void res.json({ error: '该申请不在待审状态' });
|
|
220
|
-
|
|
221
|
-
.run(admin.id, note || null, appRow.id);
|
|
219
|
+
await dbRun("UPDATE quota_increase_applications SET status='rejected', reviewed_at=datetime('now'), reviewed_by=?, decision_note=? WHERE id=?", [admin.id, note || null, appRow.id]);
|
|
222
220
|
logAdminAction(admin.id, 'reject_quota_increase', 'user', appRow.user_id, { note });
|
|
223
221
|
res.json({ success: true });
|
|
224
222
|
});
|
|
@@ -1,7 +1,29 @@
|
|
|
1
1
|
import QRCode from 'qrcode';
|
|
2
2
|
import { createHash } from 'crypto';
|
|
3
|
+
import { dbOne, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
3
4
|
export function registerShareRedirectsRoutes(app, deps) {
|
|
4
|
-
|
|
5
|
+
// db 已全量走 RFC-016 异步 seam(dbOne/dbRun),不再直接用 deps.db
|
|
6
|
+
const { auth, clientIpHash, clientUaHash, resolveInviteCodeRef } = deps;
|
|
7
|
+
// Resolve the ?ref for a /s/<shareable> redirect: a permanent_code ONLY — never the raw owner_id (usr_xxx).
|
|
8
|
+
// Prefer the shareable's stored owner_code; if missing/invalid, look up the owner's permanent_code and
|
|
9
|
+
// opportunistically backfill owner_code. If the owner has no permanent_code, return null (emit NO ref).
|
|
10
|
+
async function shareOwnerRefCode(ownerCode, ownerId) {
|
|
11
|
+
const oc = typeof ownerCode === 'string' ? ownerCode.trim().toUpperCase() : '';
|
|
12
|
+
if (/^[A-Z0-9]{6,7}$/.test(oc))
|
|
13
|
+
return oc;
|
|
14
|
+
const oid = typeof ownerId === 'string' ? ownerId : '';
|
|
15
|
+
if (oid) {
|
|
16
|
+
const r = await dbOne("SELECT permanent_code FROM users WHERE id = ? AND id != 'sys_protocol'", [oid]);
|
|
17
|
+
if (r?.permanent_code) {
|
|
18
|
+
try {
|
|
19
|
+
await dbRun("UPDATE shareables SET owner_code = ? WHERE owner_id = ? AND (owner_code IS NULL OR owner_code = '')", [r.permanent_code, oid]);
|
|
20
|
+
}
|
|
21
|
+
catch { }
|
|
22
|
+
return r.permanent_code;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
5
27
|
// 二维码生成(24h cache + ETag)
|
|
6
28
|
app.get('/api/qr', async (req, res) => {
|
|
7
29
|
const text = String(req.query.text || '').slice(0, 1024).trim();
|
|
@@ -24,27 +46,27 @@ export function registerShareRedirectsRoutes(app, deps) {
|
|
|
24
46
|
});
|
|
25
47
|
// 商品分享短链 /s/<shareable_id>
|
|
26
48
|
// 笔记 → 跳 PWA 内 #note/<id>;商品 → #order-product/<id>;外链 → 直跳外部 URL
|
|
27
|
-
app.get('/s/:id', (req, res) => {
|
|
49
|
+
app.get('/s/:id', async (req, res) => {
|
|
28
50
|
const id = String(req.params.id || '').trim();
|
|
29
|
-
const row =
|
|
51
|
+
const row = await dbOne(`
|
|
30
52
|
SELECT id, owner_id, owner_code, type, external_url, related_product_id, related_anchor, status
|
|
31
53
|
FROM shareables WHERE id = ? AND status = 'active'
|
|
32
|
-
|
|
54
|
+
`, [id]);
|
|
33
55
|
// Phase C 笔记着陆页 — type=note 优先跳 PWA 内的 #note/<id>
|
|
34
56
|
if (row && row.type === 'note') {
|
|
35
|
-
const ownerRef = row.owner_code
|
|
57
|
+
const ownerRef = await shareOwnerRefCode(row.owner_code, row.owner_id);
|
|
36
58
|
const qs = new URLSearchParams();
|
|
37
59
|
if (ownerRef)
|
|
38
|
-
qs.set('ref',
|
|
60
|
+
qs.set('ref', ownerRef);
|
|
39
61
|
qs.set('share_id', id);
|
|
40
62
|
try {
|
|
41
63
|
const ipHash = clientIpHash(req);
|
|
42
64
|
const uaHash = clientUaHash(req);
|
|
43
|
-
const dup =
|
|
44
|
-
|
|
45
|
-
|
|
65
|
+
const dup = await dbOne(`SELECT 1 FROM shareable_click_log WHERE shareable_id = ? AND ip_hash = ? AND ua_hash = ? AND created_at > datetime('now', '-6 hours') LIMIT 1`, [id, ipHash, uaHash]);
|
|
66
|
+
await dbRun(`INSERT INTO shareable_click_log (shareable_id, ip_hash, ua_hash, ref_path) VALUES (?,?,?,?)`, [id, ipHash, uaHash, req.originalUrl || null]);
|
|
67
|
+
await dbRun(`UPDATE shareables SET click_count = COALESCE(click_count,0) + 1 WHERE id = ?`, [id]);
|
|
46
68
|
if (!dup)
|
|
47
|
-
|
|
69
|
+
await dbRun(`UPDATE shareables SET unique_click_count = COALESCE(unique_click_count,0) + 1 WHERE id = ?`, [id]);
|
|
48
70
|
}
|
|
49
71
|
catch (e) {
|
|
50
72
|
console.error('[note-click]', e);
|
|
@@ -57,26 +79,25 @@ export function registerShareRedirectsRoutes(app, deps) {
|
|
|
57
79
|
try {
|
|
58
80
|
const ipHash = clientIpHash(req);
|
|
59
81
|
const uaHash = clientUaHash(req);
|
|
60
|
-
const dup =
|
|
82
|
+
const dup = await dbOne(`
|
|
61
83
|
SELECT 1 FROM shareable_click_log
|
|
62
84
|
WHERE shareable_id = ? AND ip_hash = ? AND ua_hash = ?
|
|
63
85
|
AND created_at > datetime('now', '-6 hours') LIMIT 1
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
db.prepare(`UPDATE shareables SET click_count = COALESCE(click_count,0) + 1 WHERE id = ?`).run(id);
|
|
86
|
+
`, [id, ipHash, uaHash]);
|
|
87
|
+
await dbRun(`INSERT INTO shareable_click_log (shareable_id, ip_hash, ua_hash, ref_path) VALUES (?,?,?,?)`, [id, ipHash, uaHash, req.originalUrl || null]);
|
|
88
|
+
await dbRun(`UPDATE shareables SET click_count = COALESCE(click_count,0) + 1 WHERE id = ?`, [id]);
|
|
68
89
|
if (!dup) {
|
|
69
|
-
|
|
90
|
+
await dbRun(`UPDATE shareables SET unique_click_count = COALESCE(unique_click_count,0) + 1 WHERE id = ?`, [id]);
|
|
70
91
|
}
|
|
71
92
|
}
|
|
72
93
|
catch (e) {
|
|
73
94
|
console.error('[M3-click]', e);
|
|
74
95
|
}
|
|
75
|
-
const ownerRef = row.owner_code
|
|
96
|
+
const ownerRef = await shareOwnerRefCode(row.owner_code, row.owner_id);
|
|
76
97
|
const isProduct = !!row.related_product_id;
|
|
77
98
|
const baseParams = new URLSearchParams();
|
|
78
99
|
if (ownerRef)
|
|
79
|
-
baseParams.set('ref',
|
|
100
|
+
baseParams.set('ref', ownerRef);
|
|
80
101
|
if (isProduct)
|
|
81
102
|
baseParams.set('share_id', id);
|
|
82
103
|
const qs = baseParams.toString() ? '?' + baseParams.toString() : '';
|
|
@@ -92,7 +113,7 @@ export function registerShareRedirectsRoutes(app, deps) {
|
|
|
92
113
|
res.redirect(302, `/${qs}#u/${row.owner_id}`);
|
|
93
114
|
});
|
|
94
115
|
// 商品分享归因落库(前端登录后首次进入带 share_id 时调用)
|
|
95
|
-
app.post('/api/product-share/touch', (req, res) => {
|
|
116
|
+
app.post('/api/product-share/touch', async (req, res) => {
|
|
96
117
|
const user = auth(req, res);
|
|
97
118
|
if (!user)
|
|
98
119
|
return;
|
|
@@ -100,25 +121,25 @@ export function registerShareRedirectsRoutes(app, deps) {
|
|
|
100
121
|
if (!shareable_id || typeof shareable_id !== 'string') {
|
|
101
122
|
return void res.json({ ok: false, error: 'invalid_shareable_id' });
|
|
102
123
|
}
|
|
103
|
-
const s =
|
|
124
|
+
const s = await dbOne(`
|
|
104
125
|
SELECT id, owner_id, related_product_id FROM shareables
|
|
105
126
|
WHERE id = ? AND status = 'active'
|
|
106
|
-
|
|
127
|
+
`, [shareable_id]);
|
|
107
128
|
if (!s || !s.related_product_id)
|
|
108
129
|
return void res.json({ ok: false, error: 'not_product_shareable' });
|
|
109
130
|
if (s.owner_id === user.id)
|
|
110
131
|
return void res.json({ ok: false, error: 'cannot_self_attribute' });
|
|
111
132
|
const expiresAt = new Date(Date.now() + 30 * 86400_000).toISOString().slice(0, 19).replace('T', ' ');
|
|
112
133
|
// first-touch:未过期 → 静默保留;过期 → 替换
|
|
113
|
-
const existing =
|
|
134
|
+
const existing = await dbOne(`
|
|
114
135
|
SELECT sharer_id, expires_at FROM product_share_attribution
|
|
115
136
|
WHERE product_id = ? AND recipient_id = ?
|
|
116
|
-
|
|
137
|
+
`, [s.related_product_id, user.id]);
|
|
117
138
|
if (!existing) {
|
|
118
|
-
|
|
119
|
-
INSERT INTO product_share_attribution (product_id, recipient_id, sharer_id, shareable_id, expires_at)
|
|
120
|
-
VALUES (?, ?, ?, ?, ?)
|
|
121
|
-
|
|
139
|
+
await dbRun(`
|
|
140
|
+
INSERT INTO product_share_attribution (product_id, recipient_id, sharer_id, shareable_id, expires_at, source_type, source_ref)
|
|
141
|
+
VALUES (?, ?, ?, ?, ?, 'direct_share', ?)
|
|
142
|
+
`, [s.related_product_id, user.id, s.owner_id, s.id, expiresAt, s.id]);
|
|
122
143
|
return void res.json({ ok: true, attributed: true, sharer_id: s.owner_id, product_id: s.related_product_id });
|
|
123
144
|
}
|
|
124
145
|
const stillValid = new Date(existing.expires_at.replace(' ', 'T') + 'Z').getTime() > Date.now();
|
|
@@ -126,39 +147,23 @@ export function registerShareRedirectsRoutes(app, deps) {
|
|
|
126
147
|
return void res.json({ ok: true, attributed: false, existing_sharer_id: existing.sharer_id, locked: true });
|
|
127
148
|
}
|
|
128
149
|
// 已过期 → 刷新
|
|
129
|
-
|
|
150
|
+
await dbRun(`
|
|
130
151
|
UPDATE product_share_attribution
|
|
131
|
-
SET sharer_id = ?, shareable_id = ?, created_at = datetime('now'), expires_at =
|
|
152
|
+
SET sharer_id = ?, shareable_id = ?, created_at = datetime('now'), expires_at = ?,
|
|
153
|
+
source_type = 'direct_share', source_ref = ?, source_shop_seller_id = NULL, source_qualified_order_id = NULL
|
|
132
154
|
WHERE product_id = ? AND recipient_id = ?
|
|
133
|
-
|
|
155
|
+
`, [s.owner_id, s.id, expiresAt, s.id, s.related_product_id, user.id]);
|
|
134
156
|
res.json({ ok: true, attributed: true, refreshed: true, sharer_id: s.owner_id });
|
|
135
157
|
});
|
|
136
|
-
// 邀请短链 /i/CODE
|
|
137
|
-
//
|
|
158
|
+
// 邀请短链 /i/CODE — invite-code ONLY (permanent_code, 兼容旧的 -L/-R 后缀). usr_xxx / @handle / 裸 handle
|
|
159
|
+
// 一律 404(不再做 handle 解析)。pre-public 去左右码:/i/CODE 与旧 /i/CODE-L、/i/CODE-R 一律
|
|
160
|
+
// 规范化重定向到 /?ref=CODE(丢弃 side;放置侧别由注册时系统自动决定),旧链接/二维码仍可用。
|
|
161
|
+
// 不受 invite_rotation_enabled 影响:已有用户分享出的 /i/CODE 链接和二维码必须始终可用。
|
|
138
162
|
app.get('/i/:code', (req, res) => {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
raw = m[1];
|
|
144
|
-
side = m[2].toLowerCase() === 'l' ? 'left' : 'right';
|
|
145
|
-
}
|
|
146
|
-
// permanent_code 或 handle 规范化
|
|
147
|
-
let display = null;
|
|
148
|
-
if (/^[A-Z0-9]{6,7}$/.test(raw.toUpperCase()) && !raw.startsWith('@')) {
|
|
149
|
-
const r = db.prepare("SELECT permanent_code FROM users WHERE permanent_code = ? AND id != 'sys_protocol'").get(raw.toUpperCase());
|
|
150
|
-
if (r)
|
|
151
|
-
display = r.permanent_code;
|
|
152
|
-
}
|
|
153
|
-
if (!display) {
|
|
154
|
-
const h = raw.replace(/^@/, '').toLowerCase();
|
|
155
|
-
const r = db.prepare("SELECT handle FROM users WHERE handle = ? AND id != 'sys_protocol'").get(h);
|
|
156
|
-
if (r)
|
|
157
|
-
display = '@' + r.handle; // @ 前缀辨识 handle 形态
|
|
158
|
-
}
|
|
159
|
-
if (!display)
|
|
160
|
-
return void res.status(404).send('Invitation link not found. Code: ' + raw);
|
|
161
|
-
const target = `/?ref=${encodeURIComponent(display)}${side ? `&side=${side}` : ''}`;
|
|
163
|
+
const ref = resolveInviteCodeRef(String(req.params.code || ''));
|
|
164
|
+
if (!ref)
|
|
165
|
+
return void res.status(404).send('Invitation link not found.');
|
|
166
|
+
const target = `/?ref=${encodeURIComponent(ref.code)}`;
|
|
162
167
|
res.redirect(302, target);
|
|
163
168
|
});
|
|
164
169
|
}
|