@seasonkoh/webaz 0.1.26 → 0.1.27
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/LICENSE +2 -2
- package/NOTICE +24 -3
- package/README.md +74 -330
- package/README.zh-CN.md +419 -0
- package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +21 -0
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +8 -3
- package/dist/layer1-agent/L1-1-mcp-server/auth.js +13 -1
- package/dist/layer1-agent/L1-1-mcp-server/server.js +36 -28
- package/dist/layer2-business/L2-9-contribution/admin-coordination-ingestion-engine.js +181 -0
- package/dist/layer2-business/L2-9-contribution/admin-coordination-resolver.js +114 -0
- package/dist/layer2-business/L2-9-contribution/admin-coordination-store.js +251 -0
- package/dist/layer2-business/L2-9-contribution/admin-operator-claim-workflow.js +390 -0
- package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +24 -0
- package/dist/layer2-business/L2-9-contribution/build-task-participation.js +6 -2
- package/dist/layer2-business/L2-9-contribution/build-task-quota.js +337 -0
- package/dist/layer2-business/L2-9-contribution/build-task-read.js +25 -2
- package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +57 -7
- package/dist/layer2-business/L2-9-contribution/canonical-contribution-target.js +1 -1
- package/dist/layer2-business/L2-9-contribution/contribution-facts-read.js +66 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-draft.js +187 -18
- package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +29 -4
- package/dist/ledger.js +1 -1
- package/dist/pwa/admin-audit.js +38 -0
- package/dist/pwa/anti-abuse-thresholds.js +135 -0
- package/dist/pwa/cf-origin-guard.js +33 -0
- package/dist/pwa/contract-fingerprint.js +1 -0
- package/dist/pwa/data/onboarding-cases.js +2 -2
- package/dist/pwa/data/onboarding-quiz.js +1 -1
- package/dist/pwa/economic-participation.js +2 -2
- package/dist/pwa/integration-contract.js +46 -4
- package/dist/pwa/internal/pv-settlement.js +12 -0
- package/dist/pwa/internal/wallet-signer.js +26 -0
- package/dist/pwa/public/app.js +679 -679
- package/dist/pwa/public/i18n.js +15 -28
- package/dist/pwa/public/index.html +1 -1
- package/dist/pwa/public/openapi.json +4760 -2769
- package/dist/pwa/pv-kill-switch.js +31 -0
- package/dist/pwa/routes/admin-admins.js +48 -1
- package/dist/pwa/routes/admin-analytics.js +1 -10
- package/dist/pwa/routes/admin-atomic.js +4 -17
- package/dist/pwa/routes/admin-operator-claims.js +280 -0
- package/dist/pwa/routes/admin-reports.js +4 -26
- package/dist/pwa/routes/admin-tokenomics.js +2 -76
- package/dist/pwa/routes/admin-users-lifecycle.js +1 -14
- package/dist/pwa/routes/admin-users-query.js +23 -1
- package/dist/pwa/routes/admin-wallet-ops.js +1 -1
- package/dist/pwa/routes/auth-read.js +1 -5
- package/dist/pwa/routes/auth-register.js +3 -13
- package/dist/pwa/routes/build-task-quota.js +113 -0
- package/dist/pwa/routes/claim-verify.js +15 -11
- package/dist/pwa/routes/contribution-facts.js +18 -0
- package/dist/pwa/routes/dispute-cases.js +5 -4
- package/dist/pwa/routes/growth.js +3 -3
- package/dist/pwa/routes/orders-action.js +27 -10
- package/dist/pwa/routes/orders-create.js +1 -1
- package/dist/pwa/routes/products-meta.js +19 -6
- package/dist/pwa/routes/profile-placement.js +1 -1
- package/dist/pwa/routes/promoter.js +10 -29
- package/dist/pwa/routes/public-build-tasks.js +5 -1
- package/dist/pwa/routes/public-utils.js +9 -12
- package/dist/pwa/routes/referral.js +5 -26
- package/dist/pwa/routes/rewards-apply.js +3 -2
- package/dist/pwa/routes/share-redirects.js +1 -1
- package/dist/pwa/routes/shareables-interactions.js +2 -1
- package/dist/pwa/routes/task-proposals.js +85 -9
- package/dist/pwa/routes/users-public.js +1 -4
- package/dist/pwa/routes/wallet-read.js +2 -14
- package/dist/pwa/routes/webauthn.js +1 -1
- package/dist/pwa/server.js +156 -469
- package/dist/settlement-math.js +3 -3
- package/dist/version.js +6 -4
- package/package.json +33 -7
- package/dist/index.js +0 -182
- package/dist/pwa/public/docs/ECONOMIC-MODEL.md +0 -287
- package/dist/pwa/public/docs/INTEGRATOR.md +0 -67
- package/dist/pwa/public/docs/META-RULES-FULL.md +0 -543
- package/dist/test-dispute.js +0 -153
- package/dist/test-manifest.js +0 -61
- package/dist/test-mcp-tools.js +0 -135
- package/dist/test-reputation.js +0 -116
- package/dist/test-skill-market.js +0 -101
|
@@ -89,12 +89,35 @@ export function registerAdminUsersQueryRoutes(app, deps) {
|
|
|
89
89
|
return void res.status(400).json({ error: 'action 必须 suspend/unsuspend' });
|
|
90
90
|
const reasonStr = action === 'suspend' ? (reason ? String(reason).slice(0, 200) : 'admin 批量暂停') : null;
|
|
91
91
|
const results = [];
|
|
92
|
+
// Per-uid authorization boundary (res-free, so one bad uid never aborts the whole batch). Mirrors
|
|
93
|
+
// adminCanOperateOn but stricter for admin targets: an admin target is ROOT-only regardless of scope.
|
|
94
|
+
const actingRoot = isRootAdmin(admin);
|
|
95
|
+
const actingScope = admin.admin_scope || 'global';
|
|
96
|
+
const canOperate = (t) => {
|
|
97
|
+
if (t.admin_type)
|
|
98
|
+
return actingRoot ? { ok: true } : { ok: false, reason: '仅 root 可操作 admin 账号' };
|
|
99
|
+
if (actingRoot || actingScope === 'global')
|
|
100
|
+
return { ok: true };
|
|
101
|
+
if (t.region && t.region !== actingScope)
|
|
102
|
+
return { ok: false, reason: `跨区用户(${t.region})仅本区/全局 admin 可操作` };
|
|
103
|
+
return { ok: true };
|
|
104
|
+
};
|
|
92
105
|
for (const uid of user_ids) {
|
|
93
106
|
try {
|
|
94
107
|
if (uid === 'sys_protocol' || uid === admin.id) {
|
|
95
108
|
results.push({ user_id: uid, status: 'skipped', reason: '保留账户或自己' });
|
|
96
109
|
continue;
|
|
97
110
|
}
|
|
111
|
+
const target = await dbOne('SELECT admin_type, region FROM users WHERE id = ?', [uid]);
|
|
112
|
+
if (!target) {
|
|
113
|
+
results.push({ user_id: uid, status: 'skipped', reason: '用户不存在' });
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const gate = canOperate(target);
|
|
117
|
+
if (!gate.ok) {
|
|
118
|
+
results.push({ user_id: uid, status: 'skipped', reason: gate.reason });
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
98
121
|
if (action === 'suspend') {
|
|
99
122
|
await dbRun(`INSERT INTO user_moderation (user_id, suspended, reason, suspended_by, suspended_at)
|
|
100
123
|
VALUES (?, 1, ?, ?, datetime('now'))
|
|
@@ -386,7 +409,6 @@ export function registerAdminUsersQueryRoutes(app, deps) {
|
|
|
386
409
|
reputation: user.reputation,
|
|
387
410
|
failed_attempts: user.failed_attempts ?? 0,
|
|
388
411
|
locked_until: user.locked_until,
|
|
389
|
-
mgmt_bonus_eligible: !!user.mgmt_bonus_eligible,
|
|
390
412
|
l1_share_override: Number(user.l1_share_override ?? 0),
|
|
391
413
|
can_l1_share: isAllowedSponsor(user.id),
|
|
392
414
|
},
|
|
@@ -58,7 +58,7 @@ export function registerAdminWalletOpsRoutes(app, deps) {
|
|
|
58
58
|
res.json(list);
|
|
59
59
|
});
|
|
60
60
|
app.post('/api/admin/withdrawals/:id/approve', async (req, res) => {
|
|
61
|
-
//
|
|
61
|
+
// 双重受理过渡鉴权:优先认登录的 protocol-admin(Bearer)→ 记其真实 admin id;
|
|
62
62
|
// 否则回落到共享 ADMIN_KEY(adminAuth,既有运维路径,行为不变)→ actor 记中性标记 'admin_key'。
|
|
63
63
|
// 仅认 protocol 权限的 admin;非 protocol 的 Bearer 不放行(soft 解析返回 null),不扩大访问面,只精确归属。
|
|
64
64
|
let actorId = 'admin_key';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { dbOne } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
2
2
|
export function registerAuthReadRoutes(app, deps) {
|
|
3
3
|
// db 已全量走 RFC-016 异步 seam(dbOne),不再直接用 deps.db
|
|
4
|
-
const { auth, safeRoles, getRegionMaxLevels, userMlmGate
|
|
4
|
+
const { auth, safeRoles, getRegionMaxLevels, userMlmGate } = deps;
|
|
5
5
|
app.get('/api/me', async (req, res) => {
|
|
6
6
|
const user = auth(req, res);
|
|
7
7
|
if (!user)
|
|
@@ -34,7 +34,6 @@ export function registerAuthReadRoutes(app, deps) {
|
|
|
34
34
|
const wallet = await dbOne('SELECT balance, staked, escrowed, earned FROM wallets WHERE user_id = ?', [user.id]);
|
|
35
35
|
const roles = safeRoles(user);
|
|
36
36
|
const pv = await dbOne("SELECT total_left_pv, total_right_pv FROM users WHERE id = ?", [user.id]);
|
|
37
|
-
const pendingScore = (await dbOne("SELECT COALESCE(SUM(score),0) as s FROM binary_score_records WHERE user_id = ? AND settled_at IS NULL", [user.id])).s;
|
|
38
37
|
res.json({
|
|
39
38
|
id: user.id, name: user.name, role: user.role, roles, api_key: user.api_key, wallet: wallet || null,
|
|
40
39
|
permanent_code: user.permanent_code ?? null,
|
|
@@ -69,11 +68,8 @@ export function registerAuthReadRoutes(app, deps) {
|
|
|
69
68
|
return null;
|
|
70
69
|
}
|
|
71
70
|
})(),
|
|
72
|
-
pending_score: Number(pendingScore),
|
|
73
71
|
total_left_pv: Number(pv?.total_left_pv ?? 0),
|
|
74
72
|
total_right_pv: Number(pv?.total_right_pv ?? 0),
|
|
75
|
-
lifetime_score: Number(user.lifetime_score ?? 0),
|
|
76
|
-
user_level: getUserLevel(Number(user.lifetime_score ?? 0)),
|
|
77
73
|
});
|
|
78
74
|
});
|
|
79
75
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { dbOne, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
2
2
|
export function registerAuthRegisterRoutes(app, deps) {
|
|
3
|
-
// VALID_REGIONS
|
|
4
|
-
// (server.ts 用 getter 注入;destructure at register-time would trigger TDZ
|
|
5
|
-
const { db, errorRes, INTERNAL_AUDITOR_ID, isAllowedSponsor, resolveInviteCodeRef, generateId, generateSecureKey, generatePermanentCode, deriveHandle, clientIpHash, clientUaHash, pickPreferredSide, joinPowerLeg,
|
|
3
|
+
// VALID_REGIONS 通过 deps.X 在 handler 内延迟读
|
|
4
|
+
// (server.ts 用 getter 注入;destructure at register-time would trigger TDZ 因为它在下方 const)
|
|
5
|
+
const { db, errorRes, INTERNAL_AUDITOR_ID, isAllowedSponsor, resolveInviteCodeRef, generateId, generateSecureKey, generatePermanentCode, deriveHandle, clientIpHash, clientUaHash, pickPreferredSide, joinPowerLeg, issueCode, findActiveCode, canDeliverCodes, emailDeliveryNotConfigured, recordSession, broadcastSystemEvent } = deps;
|
|
6
6
|
// CODE_TTL_MIN / MAX_CODE_ATTEMPTS 通过 deps.X 在 handler 内延迟读(它们在 server.ts 是后置 const,
|
|
7
7
|
// register-time destructure 会触发 TDZ)。
|
|
8
8
|
const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
|
|
@@ -205,16 +205,6 @@ export function registerAuthRegisterRoutes(app, deps) {
|
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
|
-
const rotationEnabled = db.prepare("SELECT value FROM system_state WHERE key='invite_rotation_enabled'").get()?.value === '1';
|
|
209
|
-
if (rotationEnabled && sponsorId) {
|
|
210
|
-
for (let i = 0; i < deps.INVITE_ROTATION_HANDLES.length; i++) {
|
|
211
|
-
const u = inviteRotationLookup(i);
|
|
212
|
-
if (u && u.id === sponsorId) {
|
|
213
|
-
db.prepare("UPDATE invite_rotation_stats SET registered_count = registered_count + 1 WHERE slot = ?").run(i);
|
|
214
|
-
break;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
208
|
return { placement, effectiveInviter, effectiveSide };
|
|
219
209
|
});
|
|
220
210
|
let txResult;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { createQuotaRequest, listMyQuotaRequests, listQuotaRequests, getQuotaRequest, approveQuotaRequest, rejectQuotaRequest, revokeQuotaRequest, requesterUsage24h, remainingQuota, isQuotaError, } from '../../layer2-business/L2-9-contribution/build-task-quota.js';
|
|
2
|
+
// map a store error_code to an HTTP status
|
|
3
|
+
function httpFor(code) {
|
|
4
|
+
if (code === 'NOT_FOUND')
|
|
5
|
+
return 404;
|
|
6
|
+
if (code === 'ALREADY_PENDING' || code === 'BAD_STATE')
|
|
7
|
+
return 409;
|
|
8
|
+
if (code === 'SELF_DECISION')
|
|
9
|
+
return 403;
|
|
10
|
+
return 400;
|
|
11
|
+
}
|
|
12
|
+
// parse the stored linked_refs JSON + surface a derived remaining count for approved grants
|
|
13
|
+
function shapeRequest(r) {
|
|
14
|
+
let linked = [];
|
|
15
|
+
try {
|
|
16
|
+
linked = JSON.parse(String(r.linked_refs ?? '[]'));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
linked = [];
|
|
20
|
+
}
|
|
21
|
+
const granted = r.granted_count == null ? null : Number(r.granted_count);
|
|
22
|
+
const consumed = Number(r.consumed_count ?? 0);
|
|
23
|
+
return { ...r, linked_refs: linked, remaining: granted == null ? null : Math.max(0, granted - consumed) };
|
|
24
|
+
}
|
|
25
|
+
export function registerBuildTaskQuotaRoutes(app, deps) {
|
|
26
|
+
const { db, errorRes, auth, requireRootAdmin } = deps;
|
|
27
|
+
// ── requester surface ─────────────────────────────────────────────────────
|
|
28
|
+
// submit a quota-increase request
|
|
29
|
+
app.post('/api/me/quota-requests', (req, res) => {
|
|
30
|
+
const user = auth(req, res);
|
|
31
|
+
if (!user)
|
|
32
|
+
return;
|
|
33
|
+
const b = (req.body ?? {});
|
|
34
|
+
const r = createQuotaRequest(db, {
|
|
35
|
+
requesterId: String(user.id),
|
|
36
|
+
requestedExtraCount: Number(b.requested_extra_count),
|
|
37
|
+
reason: String(b.reason ?? ''),
|
|
38
|
+
linkedRefs: b.linked_refs,
|
|
39
|
+
urgency: b.urgency,
|
|
40
|
+
requestedDurationHours: b.requested_duration_hours == null ? null : Number(b.requested_duration_hours),
|
|
41
|
+
quotaType: b.quota_type,
|
|
42
|
+
});
|
|
43
|
+
if (isQuotaError(r))
|
|
44
|
+
return void errorRes(res, httpFor(r.error_code), r.error_code, r.error);
|
|
45
|
+
res.json({ request: r });
|
|
46
|
+
});
|
|
47
|
+
// list my own requests + current remaining temporary quota
|
|
48
|
+
app.get('/api/me/quota-requests', (req, res) => {
|
|
49
|
+
const user = auth(req, res);
|
|
50
|
+
if (!user)
|
|
51
|
+
return;
|
|
52
|
+
const requests = listMyQuotaRequests(db, String(user.id)).map(shapeRequest);
|
|
53
|
+
res.json({ requests, remaining_quota: remainingQuota(db, String(user.id)) });
|
|
54
|
+
});
|
|
55
|
+
// ── ROOT admin review surface ─────────────────────────────────────────────
|
|
56
|
+
// list quota requests (optional ?status=)
|
|
57
|
+
app.get('/api/admin/quota-requests', (req, res) => {
|
|
58
|
+
const admin = requireRootAdmin(req, res);
|
|
59
|
+
if (!admin)
|
|
60
|
+
return;
|
|
61
|
+
const status = typeof req.query.status === 'string' ? req.query.status : undefined;
|
|
62
|
+
const requests = listQuotaRequests(db, { status }).map(shapeRequest);
|
|
63
|
+
res.json({ requests });
|
|
64
|
+
});
|
|
65
|
+
// detail of one request + the requester's live 24h create usage (reviewer context)
|
|
66
|
+
app.get('/api/admin/quota-requests/:id', (req, res) => {
|
|
67
|
+
const admin = requireRootAdmin(req, res);
|
|
68
|
+
if (!admin)
|
|
69
|
+
return;
|
|
70
|
+
const r = getQuotaRequest(db, String(req.params.id));
|
|
71
|
+
if (!r)
|
|
72
|
+
return void errorRes(res, 404, 'NOT_FOUND', 'quota request not found');
|
|
73
|
+
res.json({ request: shapeRequest(r), requester_usage_24h: requesterUsage24h(db, String(r.requester_user_id)) });
|
|
74
|
+
});
|
|
75
|
+
// approve → time-boxed counted grant (self-approval rejected in the store)
|
|
76
|
+
app.post('/api/admin/quota-requests/:id/approve', (req, res) => {
|
|
77
|
+
const admin = requireRootAdmin(req, res);
|
|
78
|
+
if (!admin)
|
|
79
|
+
return;
|
|
80
|
+
const b = (req.body ?? {});
|
|
81
|
+
const r = approveQuotaRequest(db, String(req.params.id), String(admin.id), {
|
|
82
|
+
grantedCount: b.extra_count == null ? undefined : Number(b.extra_count),
|
|
83
|
+
durationHours: b.duration_hours == null ? undefined : Number(b.duration_hours),
|
|
84
|
+
expiresAt: typeof b.expires_at === 'string' ? b.expires_at : undefined,
|
|
85
|
+
decisionNote: typeof b.approval_note === 'string' ? b.approval_note : undefined,
|
|
86
|
+
});
|
|
87
|
+
if (isQuotaError(r))
|
|
88
|
+
return void errorRes(res, httpFor(r.error_code), r.error_code, r.error);
|
|
89
|
+
res.json({ approved: r });
|
|
90
|
+
});
|
|
91
|
+
// reject (self-rejection also blocked by the store's SELF_DECISION guard)
|
|
92
|
+
app.post('/api/admin/quota-requests/:id/reject', (req, res) => {
|
|
93
|
+
const admin = requireRootAdmin(req, res);
|
|
94
|
+
if (!admin)
|
|
95
|
+
return;
|
|
96
|
+
const b = (req.body ?? {});
|
|
97
|
+
const r = rejectQuotaRequest(db, String(req.params.id), String(admin.id), { decisionNote: typeof b.rejection_note === 'string' ? b.rejection_note : undefined });
|
|
98
|
+
if (isQuotaError(r))
|
|
99
|
+
return void errorRes(res, httpFor(r.error_code), r.error_code, r.error);
|
|
100
|
+
res.json({ rejected: r });
|
|
101
|
+
});
|
|
102
|
+
// revoke an already-approved grant (root)
|
|
103
|
+
app.post('/api/admin/quota-requests/:id/revoke', (req, res) => {
|
|
104
|
+
const admin = requireRootAdmin(req, res);
|
|
105
|
+
if (!admin)
|
|
106
|
+
return;
|
|
107
|
+
const b = (req.body ?? {});
|
|
108
|
+
const r = revokeQuotaRequest(db, String(req.params.id), String(admin.id), { decisionNote: typeof b.revocation_note === 'string' ? b.revocation_note : undefined });
|
|
109
|
+
if (isQuotaError(r))
|
|
110
|
+
return void errorRes(res, httpFor(r.error_code), r.error_code, r.error);
|
|
111
|
+
res.json({ revoked: r });
|
|
112
|
+
});
|
|
113
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js';
|
|
2
|
+
// #420 P1-3 — verifier outlier 阈值改由 governance-adjustable protocol_params 驱动
|
|
3
|
+
import { readAntiAbuseThresholds, verifierOutlierBand } from '../anti-abuse-thresholds.js';
|
|
2
4
|
// RFC-016 Phase 1 — 仅端点纯校验读/列表/公开查询/读回 + 单语句标记/字段写 + 写后通知 → async seam。
|
|
3
5
|
// 全部保持同步(Phase 3 再用 pg tx/行锁):
|
|
4
6
|
// - 模块级 helper(settleClaimTask 三路径结算 / distributePool / checkAndApplyOutlierStrike /
|
|
@@ -23,11 +25,10 @@ const CLAIM_VALID_VOTES = new Set(['pass', 'fail', 'no_fault', 'abstain']);
|
|
|
23
25
|
// V3:abstain 不计入 3-vote 共识、不参与 majority、不触发 outlier
|
|
24
26
|
const CLAIM_SELLER_FINE_RATE = 0.10; // pass 时扣 product.stake_amount × 10%
|
|
25
27
|
const CLAIM_NO_FAULT_SUBSIDY = 1; // no_fault 路径协议池补贴每个 verifier 1 WAZ
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
export const CLAIM_OUTLIER_WINDOW_DAYS = 180;
|
|
28
|
+
// #420 P1-3:verifier outlier 阈值(暂停/撤销/窗口/暂停时长)已抽到 governance-adjustable
|
|
29
|
+
// protocol_params,单一真相源在 ../anti-abuse-thresholds.ts(DEFAULT_ANTI_ABUSE_THRESHOLDS:
|
|
30
|
+
// outlierSuspendCount=3 / outlierRevokeCount=5 / outlierSuspendDays=30 / outlierWindowDays=180)。
|
|
31
|
+
// checkAndApplyOutlierStrike + server.ts checkVerifierOutlier 通过 readAntiAbuseThresholds(db) 读取。
|
|
31
32
|
// ─── helpers (module-level, db 通过参数传) ───────────────────
|
|
32
33
|
// 2026-05-22 V2:通知所有资格内 verifier 有新 claim 任务
|
|
33
34
|
export function notifyEligibleVerifiers(db, generateId, args) {
|
|
@@ -99,28 +100,31 @@ export function activeClaimTaskCountForVerifier(db, userId) {
|
|
|
99
100
|
}
|
|
100
101
|
// M7.3b:单个 outlier 处罚检查
|
|
101
102
|
function checkAndApplyOutlierStrike(db, generateId, userId) {
|
|
103
|
+
// #420 P1-3:窗口/暂停/撤销阈值由 protocol_params 驱动(默认 = 原 180d/≥5/≥3/30d)
|
|
104
|
+
const t = readAntiAbuseThresholds(db);
|
|
102
105
|
const cnt = db.prepare(`
|
|
103
106
|
SELECT COUNT(*) as n FROM claim_verification_votes cvv
|
|
104
107
|
JOIN claim_verification_tasks cvt ON cvt.id = cvv.task_id
|
|
105
108
|
WHERE cvv.verifier_id = ?
|
|
106
109
|
AND cvv.was_majority = 0
|
|
107
110
|
AND cvt.resolved_at IS NOT NULL
|
|
108
|
-
AND cvt.resolved_at >= datetime('now', '-${
|
|
111
|
+
AND cvt.resolved_at >= datetime('now', '-${t.outlierWindowDays} days')
|
|
109
112
|
`).get(userId).n;
|
|
110
113
|
const existing = db.prepare(`SELECT type, outlier_count FROM claim_verifier_suspensions
|
|
111
114
|
WHERE user_id = ? AND (type = 'revoked' OR until_at > datetime('now'))
|
|
112
115
|
ORDER BY created_at DESC LIMIT 1`).get(userId);
|
|
113
116
|
if (existing?.type === 'revoked')
|
|
114
117
|
return { strikes_180d: cnt };
|
|
115
|
-
|
|
118
|
+
const band = verifierOutlierBand(cnt, t);
|
|
119
|
+
if (band === 'revoke' && (!existing || existing.outlier_count < t.outlierRevokeCount)) {
|
|
116
120
|
db.prepare(`INSERT INTO claim_verifier_suspensions (id, user_id, type, reason, outlier_count)
|
|
117
|
-
VALUES (?,?, 'revoked', ?, ?)`).run(generateId('cvs'), userId,
|
|
121
|
+
VALUES (?,?, 'revoked', ?, ?)`).run(generateId('cvs'), userId, `${t.outlierWindowDays}d 内累计 ${cnt} 次 outlier`, cnt);
|
|
118
122
|
return { strikes_180d: cnt, suspension: { type: 'revoked', until_at: null } };
|
|
119
123
|
}
|
|
120
|
-
if (
|
|
121
|
-
const until = new Date(Date.now() +
|
|
124
|
+
if (band === 'suspend' && !existing) {
|
|
125
|
+
const until = new Date(Date.now() + t.outlierSuspendDays * 86400_000).toISOString();
|
|
122
126
|
db.prepare(`INSERT INTO claim_verifier_suspensions (id, user_id, type, until_at, reason, outlier_count)
|
|
123
|
-
VALUES (?,?, 'suspended', ?, ?, ?)`).run(generateId('cvs'), userId, until,
|
|
127
|
+
VALUES (?,?, 'suspended', ?, ?, ?)`).run(generateId('cvs'), userId, until, `${t.outlierWindowDays}d 内累计 ${cnt} 次 outlier`, cnt);
|
|
124
128
|
return { strikes_180d: cnt, suspension: { type: 'suspended', until_at: until } };
|
|
125
129
|
}
|
|
126
130
|
return { strikes_180d: cnt };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { getMyContributionFacts } from '../../layer2-business/L2-9-contribution/contribution-facts-read.js';
|
|
2
|
+
import { withUncommittedValueBoundary } from '../../layer2-business/L2-9-contribution/contribution-display-envelope.js';
|
|
3
|
+
export function registerContributionFactsRoutes(app, deps) {
|
|
4
|
+
const { db, auth, errorRes } = deps;
|
|
5
|
+
// ── READ-ONLY: the caller's OWN attributable contribution facts (GitHub + admin coordination) ──
|
|
6
|
+
app.get('/api/contribution-facts/me', (req, res) => {
|
|
7
|
+
const user = auth(req, res);
|
|
8
|
+
if (!user)
|
|
9
|
+
return;
|
|
10
|
+
try {
|
|
11
|
+
const surface = getMyContributionFacts(db, user.id);
|
|
12
|
+
res.json(withUncommittedValueBoundary(surface));
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return void errorRes(res, 500, 'INTERNAL', '内部错误'); // never leak a stack / query
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
2
|
+
import { genuineSalePredicate } from '../../layer0-foundation/L0-2-state-machine/genuine-sale.js'; // 真实成交单一真相源
|
|
2
3
|
export function registerDisputeCasesRoutes(app, deps) {
|
|
3
4
|
const { db, auth, getUser, generateId, piiSanitize, detectFraud, commentBlocklistHit, llmModerateComment } = deps;
|
|
4
5
|
// 公共发言门槛 — 防新号/小号刷评论/投票
|
|
@@ -7,7 +8,7 @@ export function registerDisputeCasesRoutes(app, deps) {
|
|
|
7
8
|
const lifetime = Number(user.lifetime_score || 0);
|
|
8
9
|
if (lifetime >= 5)
|
|
9
10
|
return { ok: true };
|
|
10
|
-
const completed = (await dbOne(`SELECT COUNT(*) as n FROM orders WHERE (buyer_id = ? OR seller_id = ?) AND
|
|
11
|
+
const completed = (await dbOne(`SELECT COUNT(*) as n FROM orders WHERE (buyer_id = ? OR seller_id = ?) AND ${genuineSalePredicate('orders')}`, [user.id, user.id])).n; // 真实成交,排除退款/违约
|
|
11
12
|
if (completed >= 1)
|
|
12
13
|
return { ok: true };
|
|
13
14
|
const created = user.created_at ? new Date(String(user.created_at).replace(' ', 'T') + 'Z').getTime() : 0;
|
|
@@ -93,9 +94,9 @@ export function registerDisputeCasesRoutes(app, deps) {
|
|
|
93
94
|
};
|
|
94
95
|
// 评论 + 自动身份标签
|
|
95
96
|
const rawComments = await dbAll(`
|
|
96
|
-
SELECT dc.*, u.handle, u.name, u.role,
|
|
97
|
+
SELECT dc.*, u.handle, u.name, u.role,
|
|
97
98
|
(SELECT COUNT(*) FROM orders o
|
|
98
|
-
WHERE o.buyer_id = dc.commenter_id AND o.product_id = ? AND o
|
|
99
|
+
WHERE o.buyer_id = dc.commenter_id AND o.product_id = ? AND ${genuineSalePredicate('o')}) as bought_count,
|
|
99
100
|
(SELECT COUNT(*) FROM products p
|
|
100
101
|
WHERE p.seller_id = dc.commenter_id AND p.category = (SELECT category FROM products WHERE id = ?) AND p.status = 'active') as same_cat_seller_count
|
|
101
102
|
FROM dispute_comments dc
|
|
@@ -118,7 +119,7 @@ export function registerDisputeCasesRoutes(app, deps) {
|
|
|
118
119
|
// W5: 取所有子回复,按 parent_comment_id 分组挂在 comments 下
|
|
119
120
|
const commentIds = rawComments.map(r => r.id);
|
|
120
121
|
const rawReplies = commentIds.length > 0 ? await dbAll(`
|
|
121
|
-
SELECT r.*, u.handle, u.name, u.role
|
|
122
|
+
SELECT r.*, u.handle, u.name, u.role
|
|
122
123
|
FROM dispute_comment_replies r LEFT JOIN users u ON u.id = r.replier_id
|
|
123
124
|
WHERE r.parent_comment_id IN (${commentIds.map(() => '?').join(',')})
|
|
124
125
|
ORDER BY r.created_at ASC
|
|
@@ -49,7 +49,7 @@ const GROWTH_TASK_CATALOG = [
|
|
|
49
49
|
{ id: 'tier1_match', chapter: 3,
|
|
50
50
|
title_zh: '持续贡献阶段 1', title_en: 'Contribution stage 1',
|
|
51
51
|
desc_zh: '推荐网络贡献达到第一阶段标准', desc_en: 'Referral-network contribution reaches stage-1 threshold',
|
|
52
|
-
evaluate: c => c.
|
|
52
|
+
evaluate: c => c.min_leg_pv >= 30000 },
|
|
53
53
|
// 第 4 关:分享达人
|
|
54
54
|
{ id: 'monthly_100', chapter: 4,
|
|
55
55
|
title_zh: '月度推荐收益 100 WAZ', title_en: 'Monthly referral income 100 WAZ',
|
|
@@ -83,7 +83,7 @@ async function buildGrowthTaskCtx(_db, userId) {
|
|
|
83
83
|
const sCount = (await dbOne("SELECT COUNT(*) AS n FROM shareables WHERE owner_id = ? AND status = 'active'", [userId])).n;
|
|
84
84
|
const mCount = (await dbOne("SELECT COUNT(*) AS n FROM manifest_registry WHERE owner_id = ? AND status = 'active'", [userId])).n;
|
|
85
85
|
const pv = await dbOne("SELECT total_left_pv, total_right_pv FROM users WHERE id = ?", [userId]);
|
|
86
|
-
const
|
|
86
|
+
const minLeg = Math.min(Number(pv?.total_left_pv || 0), Number(pv?.total_right_pv || 0));
|
|
87
87
|
const comm30 = (await dbOne(`SELECT COALESCE(SUM(amount),0) AS s FROM commission_records WHERE beneficiary_id = ? AND created_at >= datetime('now','-30 days')`, [userId])).s;
|
|
88
88
|
const waz30 = (await dbOne(`SELECT COALESCE(SUM(waz_amount),0) AS s FROM binary_score_records WHERE user_id = ? AND settled_at >= datetime('now','-30 days')`, [userId])).s;
|
|
89
89
|
return {
|
|
@@ -97,7 +97,7 @@ async function buildGrowthTaskCtx(_db, userId) {
|
|
|
97
97
|
earnings_grand: grand,
|
|
98
98
|
shareables_count: sCount,
|
|
99
99
|
manifests_count: mCount,
|
|
100
|
-
|
|
100
|
+
min_leg_pv: minLeg,
|
|
101
101
|
last_30_total: comm30 + waz30,
|
|
102
102
|
};
|
|
103
103
|
}
|
|
@@ -320,12 +320,27 @@ export function registerOrdersActionRoutes(app, deps) {
|
|
|
320
320
|
commissionDistributed += Number(r.amount);
|
|
321
321
|
}
|
|
322
322
|
const commissionRedirected = round2(commissionPool - commissionDistributed);
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
const
|
|
328
|
-
const
|
|
323
|
+
// 2026-06-04 三科目解耦后:未发出的 commission 不再进 charity_fund / global_fund。
|
|
324
|
+
// region_cap / chain_gap / orphan_sponsor / opt_out_deactivated → commission_reserve(按 kind)
|
|
325
|
+
// opt-out 未激活(never_activated / auto_downgrade) → pending_commission_escrow(30 天内 recipient opt-in 可恢复)
|
|
326
|
+
// 此处只读汇总本单去向,让 agent 看清 redirected_total 实际落点(settleOrder 已完成,无写、无原子性要求)。
|
|
327
|
+
const crRows = await dbAll("SELECT kind, COALESCE(SUM(amount),0) AS s FROM commission_reserve_txns WHERE related_order_id = ? GROUP BY kind", [req.params.id]);
|
|
328
|
+
const reserveByKind = { region_cap: 0, chain_gap: 0, orphan_sponsor: 0, opt_out_deactivated: 0, escrow_expired: 0 };
|
|
329
|
+
for (const r of crRows) {
|
|
330
|
+
if (r.kind === 'redirect_region_cap')
|
|
331
|
+
reserveByKind.region_cap = round2(Number(r.s));
|
|
332
|
+
else if (r.kind === 'redirect_chain_gap')
|
|
333
|
+
reserveByKind.chain_gap = round2(Number(r.s));
|
|
334
|
+
else if (r.kind === 'redirect_orphan_sponsor')
|
|
335
|
+
reserveByKind.orphan_sponsor = round2(Number(r.s));
|
|
336
|
+
else if (r.kind === 'redirect_opt_out_deactivated')
|
|
337
|
+
reserveByKind.opt_out_deactivated = round2(Number(r.s));
|
|
338
|
+
else if (r.kind === 'redirect_escrow_expired')
|
|
339
|
+
reserveByKind.escrow_expired = round2(Number(r.s));
|
|
340
|
+
}
|
|
341
|
+
const redirectedToCommissionReserve = round2(reserveByKind.region_cap + reserveByKind.chain_gap + reserveByKind.orphan_sponsor + reserveByKind.opt_out_deactivated + reserveByKind.escrow_expired);
|
|
342
|
+
const escrowRow = (await dbOne("SELECT COALESCE(SUM(amount),0) AS s FROM pending_commission_escrow WHERE order_id = ? AND status = 'pending'", [req.params.id]));
|
|
343
|
+
const heldInOptOutEscrow = round2(Number(escrowRow.s));
|
|
329
344
|
// QA 轮 9.5 P2:payouts 表只 MCP legacy 写,PWA settleOrder 直更 wallet.balance 不写 payouts
|
|
330
345
|
// 改用公式推算 sellerAmount(跟 PWA settleOrder 内部计算一致),更可靠
|
|
331
346
|
const fundBase1pct = round2(total * 0.01);
|
|
@@ -336,7 +351,7 @@ export function registerOrdersActionRoutes(app, deps) {
|
|
|
336
351
|
order_amount: total,
|
|
337
352
|
distribution: {
|
|
338
353
|
seller_net: { amount: sellerAmountComputed, to: ord.seller_id, note: '不含可能的首销 stake 锁定(settleOrder 内 stake_locked_at 首次锁,从 sellerAmount 划出)' },
|
|
339
|
-
protocol_fund_2pct: { amount: protocolFee, split: {
|
|
354
|
+
protocol_fund_2pct: { amount: protocolFee, split: { protocol_reserve_pool: round2(protocolFee / 2), sys_protocol_ops: round2(protocolFee / 2) } },
|
|
340
355
|
logistics_fee: { amount: logisticsActual, rate: isInPerson ? 'N/A in_person' : (ord.logistics_id ? '5%' : 'N/A self-fulfill') },
|
|
341
356
|
commission_pool: { total: commissionPool, rate: `${(commissionRate * 100).toFixed(1)}%` },
|
|
342
357
|
commission_distribution_7_2_1: {
|
|
@@ -345,9 +360,11 @@ export function registerOrdersActionRoutes(app, deps) {
|
|
|
345
360
|
l3: commByLevel[3],
|
|
346
361
|
distributed_total: round2(commissionDistributed),
|
|
347
362
|
redirected_total: commissionRedirected,
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
363
|
+
redirected_to_commission_reserve: redirectedToCommissionReserve,
|
|
364
|
+
reserve_by_kind: reserveByKind, // region_cap / chain_gap / orphan_sponsor / opt_out_deactivated / escrow_expired
|
|
365
|
+
held_in_opt_out_escrow: heldInOptOutEscrow, // never_activated / auto_downgrade — recipient opt-in 可恢复
|
|
366
|
+
redirect_accounted_ok: Math.abs(commissionRedirected - round2(redirectedToCommissionReserve + heldInOptOutEscrow)) < 0.01,
|
|
367
|
+
redirect_note: '未发出佣金 → commission_reserve(region_cap / chain_gap / orphan_sponsor / opt_out_deactivated);opt-out 未激活(never_activated / auto_downgrade)暂存 pending_commission_escrow(30 天内 opt-in 可恢复),逾期未恢复则转入 commission_reserve(escrow_expired)。2026-06-04 起不再进 charity_fund / global_fund。',
|
|
351
368
|
},
|
|
352
369
|
fund_base_1pct: fundBase1pct,
|
|
353
370
|
},
|
|
@@ -371,7 +371,7 @@ export function registerOrdersCreateRoutes(app, deps) {
|
|
|
371
371
|
}
|
|
372
372
|
transition(db, orderId, 'paid', user.id, [], '模拟支付完成');
|
|
373
373
|
notifyTransition(db, orderId, 'created', 'paid');
|
|
374
|
-
// 里程碑 3-C
|
|
374
|
+
// 里程碑 3-C:放置同支检测(监测+审计;不阻断)
|
|
375
375
|
try {
|
|
376
376
|
auditSponsorChainCross(orderId, user.id, String(product.seller_uid), buyer.sponsor_path);
|
|
377
377
|
}
|
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
2
|
+
import { genuineSalePredicate } from '../../layer0-foundation/L0-2-state-machine/genuine-sale.js'; // 真实成交单一真相源
|
|
3
|
+
/**
|
|
4
|
+
* 真实收货完成的订单数(分享资格判据)。
|
|
5
|
+
* 关键:status='completed' 是状态机的通用终态 — 不只「无争议自然完成」(confirmed→completed),
|
|
6
|
+
* 还包括 fault_seller / fault_logistics / fault_buyer / declined_nofault / disputed → completed
|
|
7
|
+
* 这些退款 / 违约 / 争议处置终态。单看 status='completed' 会把「被退款的失败交易」当成有效成交,
|
|
8
|
+
* 错误授予分享(进而分享分润)资格。
|
|
9
|
+
* 真实收货 = 该订单曾进入过 confirmed(买家确认收货,或送达后 72h 自动确认)— 仅 happy path 经过,
|
|
10
|
+
* 所有 fault/争议/退款终态都不经过 confirmed,据此排除。
|
|
11
|
+
*/
|
|
12
|
+
async function genuineReceiptCount(buyerId, productId) {
|
|
13
|
+
return (await dbOne(`SELECT COUNT(*) AS n FROM orders WHERE buyer_id = ? AND product_id = ? AND ${genuineSalePredicate('orders')}`, [buyerId, productId])).n;
|
|
14
|
+
}
|
|
2
15
|
export function registerProductsMetaRoutes(app, deps) {
|
|
3
16
|
const { db, auth, generateId, rateLimitOk, flagNewAccountShareable, refreshProductSharerCount } = deps;
|
|
4
17
|
void db; // RFC-016: 本文件已全量走异步 seam;db 仍在 deps 由调用方注入,此处不直接使用
|
|
@@ -117,16 +130,16 @@ export function registerProductsMetaRoutes(app, deps) {
|
|
|
117
130
|
return void res.status(404).json({ error: 'not_found' });
|
|
118
131
|
res.json(row);
|
|
119
132
|
});
|
|
120
|
-
//
|
|
133
|
+
// 分享许可:是否真实收货完成该商品(经过 confirmed,排除退款/违约/争议终态)
|
|
121
134
|
app.get('/api/products/:id/can-share', async (req, res) => {
|
|
122
135
|
const user = auth(req, res);
|
|
123
136
|
if (!user)
|
|
124
137
|
return;
|
|
125
|
-
const completed =
|
|
138
|
+
const completed = await genuineReceiptCount(user.id, req.params.id);
|
|
126
139
|
res.json({
|
|
127
140
|
can_share: completed > 0,
|
|
128
141
|
completed_orders: completed,
|
|
129
|
-
reason: completed > 0 ? '
|
|
142
|
+
reason: completed > 0 ? 'genuine_receipt_of_product' : 'need_genuine_receipt_of_this_product',
|
|
130
143
|
});
|
|
131
144
|
});
|
|
132
145
|
// 获取或创建商品 shareable(被 sharePromoLink 用,走 /s/<id> 短链)
|
|
@@ -144,7 +157,7 @@ export function registerProductsMetaRoutes(app, deps) {
|
|
|
144
157
|
};
|
|
145
158
|
const minOrders = await getParam('rewards_opt_in.min_completed_orders', 1);
|
|
146
159
|
const requirePasskey = await getParam('rewards_opt_in.require_passkey', 1);
|
|
147
|
-
const totalCompleted = (await dbOne(
|
|
160
|
+
const totalCompleted = (await dbOne(`SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND ${genuineSalePredicate('orders')}`, [user.id])).n; // 真实成交,排除退款/违约
|
|
148
161
|
const passkeyCount = (await dbOne("SELECT COUNT(*) as n FROM webauthn_credentials WHERE user_id = ?", [user.id])).n;
|
|
149
162
|
const missing = [];
|
|
150
163
|
if (totalCompleted < minOrders)
|
|
@@ -165,9 +178,9 @@ export function registerProductsMetaRoutes(app, deps) {
|
|
|
165
178
|
],
|
|
166
179
|
});
|
|
167
180
|
}
|
|
168
|
-
const completed =
|
|
181
|
+
const completed = await genuineReceiptCount(user.id, productId);
|
|
169
182
|
if (completed === 0)
|
|
170
|
-
return void res.json({ error: '
|
|
183
|
+
return void res.json({ error: '需先真实收货完成该商品的购买才能分享(退款 / 违约 / 争议订单不算)', completed_orders: 0 });
|
|
171
184
|
// 优先复用现有 active shareable
|
|
172
185
|
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]);
|
|
173
186
|
if (existing) {
|
|
@@ -35,7 +35,7 @@ export function registerProfilePlacementRoutes(app, deps) {
|
|
|
35
35
|
return void res.json({ error: '不能挂靠到自己' });
|
|
36
36
|
const u = await dbOne("SELECT placement_id, left_child_id, right_child_id FROM users WHERE id = ?", [user.id]);
|
|
37
37
|
if (u?.placement_id)
|
|
38
|
-
return void res.json({ error: '
|
|
38
|
+
return void res.json({ error: '你已在放置树中(永久第一触点,不可改)' });
|
|
39
39
|
if (u?.left_child_id || u?.right_child_id)
|
|
40
40
|
return void res.json({ error: '你已有下线,不可补绑(防破坏树结构)' });
|
|
41
41
|
const inviter = await dbOne("SELECT id, placement_path FROM users WHERE id = ? AND id NOT IN ('sys_protocol', ?)", [resolvedInviterId, internalAuditorId]);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
2
|
+
import { genuineSalePredicate } from '../../layer0-foundation/L0-2-state-machine/genuine-sale.js'; // 真实成交单一真相源
|
|
2
3
|
export function registerPromoterRoutes(app, deps) {
|
|
3
|
-
const { db, auth, isAllowedSponsor } = deps;
|
|
4
|
+
const { db, auth, isAllowedSponsor, participationRecordingActive } = deps;
|
|
4
5
|
void db; // RFC-016: 本文件已全量走异步 seam;db 仍在 deps 由调用方注入,此处不直接使用
|
|
5
6
|
app.get('/api/promoter/dashboard', async (req, res) => {
|
|
6
7
|
const user = auth(req, res);
|
|
@@ -41,21 +42,9 @@ export function registerPromoterRoutes(app, deps) {
|
|
|
41
42
|
const leftChildName = myUser?.left_child_id ? (await dbOne("SELECT name FROM users WHERE id = ?", [myUser.left_child_id]))?.name : null;
|
|
42
43
|
const rightChildName = myUser?.right_child_id ? (await dbOne("SELECT name FROM users WHERE id = ?", [myUser.right_child_id]))?.name : null;
|
|
43
44
|
const myPlacementName = myUser?.placement_id ? (await dbOne("SELECT name FROM users WHERE id = ?", [myUser.placement_id]))?.name : null;
|
|
44
|
-
|
|
45
|
-
SELECT
|
|
46
|
-
COALESCE(SUM(CASE WHEN settled_at IS NULL THEN score ELSE 0 END),0) as pending_score,
|
|
47
|
-
COALESCE(SUM(CASE WHEN settled_at IS NOT NULL THEN waz_amount ELSE 0 END),0) as settled_waz,
|
|
48
|
-
COUNT(*) as total_hits
|
|
49
|
-
FROM binary_score_records WHERE user_id = ?
|
|
50
|
-
`, [userId]));
|
|
51
|
-
const recentBinary = await dbAll(`
|
|
52
|
-
SELECT id, tier, score, settled_at, waz_amount, created_at
|
|
53
|
-
FROM binary_score_records WHERE user_id = ?
|
|
54
|
-
ORDER BY created_at DESC LIMIT 10
|
|
55
|
-
`, [userId]);
|
|
56
|
-
const tiers = await dbAll("SELECT tier, pv_threshold, score_per_hit FROM binary_tier_config WHERE active=1 ORDER BY tier ASC");
|
|
45
|
+
// matching-rewards reads (score / recent matches / tier config) removed — engine excised (#401).
|
|
57
46
|
const canL1Share = isAllowedSponsor(userId);
|
|
58
|
-
const completedOrders = (await dbOne(
|
|
47
|
+
const completedOrders = (await dbOne(`SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND ${genuineSalePredicate('orders')}`, [userId])).n; // 真实成交,排除退款/违约
|
|
59
48
|
const overrideRow = await dbOne("SELECT l1_share_override FROM users WHERE id = ?", [userId]);
|
|
60
49
|
const shareableProducts = await dbAll(`
|
|
61
50
|
SELECT p.id, p.title, p.price, p.category, p.commission_rate,
|
|
@@ -64,7 +53,7 @@ export function registerPromoterRoutes(app, deps) {
|
|
|
64
53
|
JOIN orders o2 ON o2.id = cr.order_id
|
|
65
54
|
WHERE cr.beneficiary_id = ? AND o2.product_id = p.id), 0) as my_earned
|
|
66
55
|
FROM products p
|
|
67
|
-
WHERE p.id IN (SELECT DISTINCT product_id FROM orders WHERE buyer_id = ? AND
|
|
56
|
+
WHERE p.id IN (SELECT DISTINCT product_id FROM orders WHERE buyer_id = ? AND ${genuineSalePredicate('orders')})
|
|
68
57
|
AND p.commission_rate IS NOT NULL AND p.commission_rate > 0
|
|
69
58
|
AND p.status = 'active'
|
|
70
59
|
ORDER BY my_earned DESC, total_sales DESC LIMIT 20
|
|
@@ -77,21 +66,15 @@ export function registerPromoterRoutes(app, deps) {
|
|
|
77
66
|
SELECT COALESCE(SUM(amount),0) as total FROM commission_records
|
|
78
67
|
WHERE beneficiary_id = ? AND created_at >= datetime('now','-60 days')
|
|
79
68
|
AND created_at < datetime('now','-30 days')
|
|
80
|
-
`, [userId])).total;
|
|
81
|
-
const wazLast30 = (await dbOne(`
|
|
82
|
-
SELECT COALESCE(SUM(waz_amount),0) as total FROM binary_score_records
|
|
83
|
-
WHERE user_id = ? AND settled_at >= datetime('now','-30 days')
|
|
84
69
|
`, [userId])).total;
|
|
85
70
|
const projection = {
|
|
86
71
|
last_30_commission: earnedLast30,
|
|
87
72
|
prev_30_commission: earnedPrev30,
|
|
88
|
-
last_30_atomic_waz: wazLast30,
|
|
89
73
|
growth_rate: earnedPrev30 > 0 ? earnedLast30 / earnedPrev30 - 1 : null,
|
|
90
|
-
next_30_estimate: earnedLast30
|
|
74
|
+
next_30_estimate: earnedLast30,
|
|
91
75
|
};
|
|
92
76
|
const insights = [];
|
|
93
|
-
//
|
|
94
|
-
// 不在用户面 surface 营销主推弱腿 / pairing 公式 / PV-tier 进度等玩法。位置 / PV 仅为参与记录,非收益路径。
|
|
77
|
+
// 匹配奖励引擎已切除(#401):不展示任何奖励经营建议;位置 / PV 仅为参与记录,非收益路径。
|
|
95
78
|
const lastInvite = (await dbOne(`SELECT MAX(created_at) as t FROM users WHERE sponsor_id = ?`, [userId]));
|
|
96
79
|
if (lastInvite.t) {
|
|
97
80
|
const days = Math.floor((Date.now() - new Date(lastInvite.t).getTime()) / 86400_000);
|
|
@@ -167,17 +150,15 @@ export function registerPromoterRoutes(app, deps) {
|
|
|
167
150
|
shareable_products: shareableProducts,
|
|
168
151
|
projection,
|
|
169
152
|
insights,
|
|
153
|
+
gates: { participation_recording_active: participationRecordingActive() },
|
|
154
|
+
// Neutral participation record only — placement position + per-leg PV. No rewards (matching engine excised #401).
|
|
155
|
+
// (response keys `atomic` / `binary_tree` kept as the placement structure's stable shape — frontend reads them)
|
|
170
156
|
atomic: {
|
|
171
|
-
left_invite_url: codeForLink ? `${host}/i/${codeForLink}-L` : null,
|
|
172
|
-
right_invite_url: codeForLink ? `${host}/i/${codeForLink}-R` : null,
|
|
173
157
|
total_left_pv: Number(myUser?.total_left_pv ?? 0),
|
|
174
158
|
total_right_pv: Number(myUser?.total_right_pv ?? 0),
|
|
175
159
|
left_child: myUser?.left_child_id ? { id: myUser.left_child_id, name: leftChildName } : null,
|
|
176
160
|
right_child: myUser?.right_child_id ? { id: myUser.right_child_id, name: rightChildName } : null,
|
|
177
161
|
my_placement: myUser?.placement_id ? { id: myUser.placement_id, name: myPlacementName, side: myUser.placement_side } : null,
|
|
178
|
-
score: scoreAgg,
|
|
179
|
-
recent_binary: recentBinary,
|
|
180
|
-
tier_config: tiers,
|
|
181
162
|
binary_tree: binaryTree,
|
|
182
163
|
},
|
|
183
164
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { listBuildTasksWithAgentMetadata, getBuildTaskWithAgentMetadata, validateTaskFilters, withContributionReadEnvelope } from '../../layer2-business/L2-9-contribution/build-task-read.js';
|
|
2
|
+
import { caseIdForTask } from '../../layer2-business/L2-9-contribution/task-proposal-draft.js';
|
|
2
3
|
export function registerPublicBuildTasksRoutes(app, deps) {
|
|
3
4
|
const { db, errorRes } = deps;
|
|
4
5
|
app.get('/api/public/build-tasks', (req, res) => {
|
|
@@ -14,6 +15,9 @@ export function registerPublicBuildTasksRoutes(app, deps) {
|
|
|
14
15
|
const task = getBuildTaskWithAgentMetadata(db, String(req.params.id), 'public');
|
|
15
16
|
if (!task)
|
|
16
17
|
return void errorRes(res, 404, 'NOT_FOUND', '任务不存在');
|
|
17
|
-
|
|
18
|
+
// case_id threads proposal → task → PR (= source proposal id if converted from a proposal, else the task id),
|
|
19
|
+
// so the proposer, the contributor, and the PR all quote one id. (Helper lives in the store — keeps this
|
|
20
|
+
// route off the RFC-016 raw-db seam.)
|
|
21
|
+
res.json(withContributionReadEnvelope({ task: { ...task, case_id: caseIdForTask(db, String(req.params.id)) } }));
|
|
18
22
|
});
|
|
19
23
|
}
|