@seasonkoh/webaz 0.1.20 → 0.1.22
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/dist/layer0-foundation/L0-2-state-machine/engine.js +83 -90
- package/dist/layer1-agent/L1-1-mcp-server/server.js +20 -7
- package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +138 -168
- package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +6 -4
- package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +12 -8
- package/dist/ledger.js +58 -0
- package/dist/money.js +106 -0
- package/dist/pwa/acp-feed.js +103 -0
- package/dist/pwa/public/app.js +55 -54
- package/dist/pwa/public/i18n.js +40 -41
- package/dist/pwa/public/index.html +26 -1
- package/dist/pwa/routes/auction.js +9 -6
- package/dist/pwa/routes/disputes-write.js +12 -11
- package/dist/pwa/routes/orders-create.js +24 -13
- package/dist/pwa/routes/public-utils.js +11 -0
- package/dist/pwa/server.js +59 -41
- package/dist/settlement-math.js +27 -0
- package/package.json +1 -1
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { generateId } from '../../layer0-foundation/L0-1-database/schema.js';
|
|
16
16
|
import { transition } from '../../layer0-foundation/L0-2-state-machine/engine.js';
|
|
17
|
+
// RFC-014 PR5 — 争议资金处置走整数 base-units + 绝对值落库 + allocate 精确拆分。
|
|
18
|
+
import { toUnits, toDecimal, mulRate, allocate } from '../../money.js';
|
|
19
|
+
import { applyWalletDelta, debitStakeThenBalance } from '../../ledger.js';
|
|
17
20
|
// ─── Schema 初始化(幂等,安全重复调用)────────────────────────
|
|
18
21
|
/**
|
|
19
22
|
* 为 disputes 表添加 L3 需要的新列
|
|
@@ -257,7 +260,7 @@ export function arbitrateDispute(db, disputeId, arbitratorId, ruling, reason, re
|
|
|
257
260
|
*
|
|
258
261
|
* 这样确保托管资金守恒(无凭空创造/销毁),责任方额外受罚(去向:sys_protocol)。
|
|
259
262
|
*/
|
|
260
|
-
function executeLiabilitySplit(db, orderId, liabilityParties, buyerRefund) {
|
|
263
|
+
export function executeLiabilitySplit(db, orderId, liabilityParties, buyerRefund) {
|
|
261
264
|
const order = db.prepare('SELECT * FROM orders WHERE id = ?').get(orderId);
|
|
262
265
|
if (!order)
|
|
263
266
|
return { success: false, error: '订单不存在' };
|
|
@@ -268,91 +271,78 @@ function executeLiabilitySplit(db, orderId, liabilityParties, buyerRefund) {
|
|
|
268
271
|
const product = db.prepare('SELECT stake_amount FROM products WHERE id = ?')
|
|
269
272
|
.get(order.product_id);
|
|
270
273
|
const stakeAmount = product?.stake_amount ?? 0;
|
|
271
|
-
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
+
// RFC-014:整数 base-units
|
|
275
|
+
const totalU = toUnits(totalAmount);
|
|
276
|
+
const stakeAmountU = toUnits(stakeAmount);
|
|
277
|
+
const actualRefundU = Math.min(toUnits(buyerRefund ?? totalAmount), totalU);
|
|
278
|
+
const sellerEscrowShareU = totalU - actualRefundU; // 残值,精确
|
|
279
|
+
// 预先计算各责任方实际扣款(资金守恒:责任方扣款 = 协议金库收入),全整数 units
|
|
274
280
|
const settled = [];
|
|
275
281
|
for (const entry of liabilityParties) {
|
|
276
|
-
const wallet = db.prepare('SELECT balance, staked FROM wallets WHERE user_id = ?')
|
|
282
|
+
const wallet = db.prepare('SELECT COALESCE(balance,0) balance, COALESCE(staked,0) staked FROM wallets WHERE user_id = ?')
|
|
277
283
|
.get(entry.user_id);
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
let
|
|
281
|
-
|
|
284
|
+
const availableU = toUnits(wallet?.balance ?? 0) + toUnits(wallet?.staked ?? 0);
|
|
285
|
+
const amountU = toUnits(entry.amount);
|
|
286
|
+
let actualPenaltyU;
|
|
287
|
+
let insuranceCoveredU = 0;
|
|
288
|
+
if (entry.insurance_cap !== undefined && toUnits(entry.insurance_cap) < amountU) {
|
|
282
289
|
// 有保险上限:责任方最多赔 insurance_cap,不足部分由协议垫付
|
|
283
|
-
|
|
284
|
-
|
|
290
|
+
const capU = toUnits(entry.insurance_cap);
|
|
291
|
+
actualPenaltyU = Math.min(capU, availableU);
|
|
292
|
+
insuranceCoveredU = amountU - capU; // 协议垫付(信息性,不实际入账)
|
|
285
293
|
}
|
|
286
294
|
else {
|
|
287
295
|
// 无保险上限:以实际可用余额为上限
|
|
288
|
-
|
|
289
|
-
|
|
296
|
+
actualPenaltyU = Math.min(amountU, availableU);
|
|
297
|
+
insuranceCoveredU = amountU - actualPenaltyU; // 余额不足部分
|
|
290
298
|
}
|
|
291
|
-
settled.push({ userId: entry.user_id, role: entry.role,
|
|
299
|
+
settled.push({ userId: entry.user_id, role: entry.role, owedU: amountU, actualPenaltyU, insuranceCoveredU });
|
|
292
300
|
}
|
|
293
301
|
// 卖家是否在责任方列表中
|
|
294
302
|
const sellerLiability = liabilityParties.find(p => p.user_id === sellerId);
|
|
295
303
|
db.transaction(() => {
|
|
296
304
|
// ── A. 托管拨付 ──────────────────────────────────────────────
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
// ── B. 责任罚款 → 协议金库 ────────────────────────────────────
|
|
306
|
-
let totalToTreasury = 0;
|
|
305
|
+
applyWalletDelta(db, buyerId, { escrowed: -totalU });
|
|
306
|
+
if (actualRefundU > 0)
|
|
307
|
+
applyWalletDelta(db, buyerId, { balance: actualRefundU });
|
|
308
|
+
if (sellerEscrowShareU > 0)
|
|
309
|
+
applyWalletDelta(db, sellerId, { balance: sellerEscrowShareU });
|
|
310
|
+
// ── B. 责任罚款 → 协议金库(先质押后余额)─────────────────────
|
|
311
|
+
let totalToTreasuryU = 0;
|
|
307
312
|
for (const s of settled) {
|
|
308
|
-
if (s.
|
|
309
|
-
|
|
310
|
-
.get(s.userId);
|
|
311
|
-
if (w.staked >= s.actualPenalty) {
|
|
312
|
-
db.prepare('UPDATE wallets SET staked = staked - ? WHERE user_id = ?').run(s.actualPenalty, s.userId);
|
|
313
|
-
}
|
|
314
|
-
else {
|
|
315
|
-
const fromStake = w.staked;
|
|
316
|
-
const fromBalance = s.actualPenalty - fromStake;
|
|
317
|
-
db.prepare('UPDATE wallets SET staked = 0, balance = balance - ? WHERE user_id = ?').run(fromBalance, s.userId);
|
|
318
|
-
}
|
|
319
|
-
totalToTreasury += s.actualPenalty;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
if (totalToTreasury > 0) {
|
|
323
|
-
db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(totalToTreasury, sysUser.id);
|
|
313
|
+
if (s.actualPenaltyU > 0)
|
|
314
|
+
totalToTreasuryU += debitStakeThenBalance(db, s.userId, s.actualPenaltyU);
|
|
324
315
|
}
|
|
316
|
+
if (totalToTreasuryU > 0)
|
|
317
|
+
applyWalletDelta(db, sysUser.id, { balance: totalToTreasuryU });
|
|
325
318
|
// ── C. 卖家商品质押处理 ───────────────────────────────────────
|
|
326
|
-
if (
|
|
319
|
+
if (stakeAmountU > 0) {
|
|
327
320
|
if (sellerLiability) {
|
|
328
|
-
//
|
|
329
|
-
const
|
|
330
|
-
const
|
|
331
|
-
db
|
|
332
|
-
if (
|
|
333
|
-
db
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(stakeForfeited, sysUser.id);
|
|
337
|
-
}
|
|
321
|
+
// 卖家有责:按责任金额扣罚质押(封顶),剩余返还
|
|
322
|
+
const stakeForfeitedU = Math.min(stakeAmountU, toUnits(sellerLiability.amount));
|
|
323
|
+
const stakeReturnU = stakeAmountU - stakeForfeitedU;
|
|
324
|
+
applyWalletDelta(db, sellerId, { staked: -stakeAmountU });
|
|
325
|
+
if (stakeReturnU > 0)
|
|
326
|
+
applyWalletDelta(db, sellerId, { balance: stakeReturnU });
|
|
327
|
+
if (stakeForfeitedU > 0)
|
|
328
|
+
applyWalletDelta(db, sysUser.id, { balance: stakeForfeitedU });
|
|
338
329
|
}
|
|
339
330
|
else {
|
|
340
331
|
// 卖家无责:全额返还质押
|
|
341
|
-
db
|
|
342
|
-
.run(stakeAmount, stakeAmount, sellerId);
|
|
332
|
+
applyWalletDelta(db, sellerId, { staked: -stakeAmountU, balance: stakeAmountU });
|
|
343
333
|
}
|
|
344
334
|
}
|
|
345
|
-
transition(db, orderId, 'refunded_partial', sysUser.id, [], `争议裁定:责任分配,退款买家 ${
|
|
335
|
+
transition(db, orderId, 'refunded_partial', sysUser.id, [], `争议裁定:责任分配,退款买家 ${toDecimal(actualRefundU)} WAZ`);
|
|
346
336
|
})();
|
|
347
337
|
return {
|
|
348
338
|
success: true,
|
|
349
339
|
detail: {
|
|
350
340
|
ruling: 'liability_split',
|
|
351
|
-
buyer_refund:
|
|
352
|
-
seller_escrow_share:
|
|
341
|
+
buyer_refund: toDecimal(actualRefundU),
|
|
342
|
+
seller_escrow_share: toDecimal(sellerEscrowShareU),
|
|
353
343
|
liability_breakdown: settled.map(s => ({
|
|
354
344
|
userId: s.userId, role: s.role,
|
|
355
|
-
owed: s.
|
|
345
|
+
owed: toDecimal(s.owedU), actualPenalty: toDecimal(s.actualPenaltyU), insuranceCovered: toDecimal(s.insuranceCoveredU)
|
|
356
346
|
})),
|
|
357
347
|
}
|
|
358
348
|
};
|
|
@@ -366,35 +356,27 @@ function executeLiabilitySplit(db, orderId, liabilityParties, buyerRefund) {
|
|
|
366
356
|
* - 先扣质押,质押不足再扣余额
|
|
367
357
|
*/
|
|
368
358
|
function chargeArbitrationFee(db, loserId, orderAmount, arbitratorId, sysUserId) {
|
|
369
|
-
|
|
359
|
+
// RFC-014:整数 base-units。费 = max(1 WAZ, 订单额×1%)
|
|
360
|
+
const feeU = Math.max(toUnits(1), mulRate(toUnits(orderAmount), 0.01));
|
|
370
361
|
const isHumanArbitrator = arbitratorId !== sysUserId;
|
|
371
|
-
const wallet = db.prepare('SELECT balance, staked FROM wallets WHERE user_id = ?')
|
|
362
|
+
const wallet = db.prepare('SELECT COALESCE(balance,0) balance, COALESCE(staked,0) staked FROM wallets WHERE user_id = ?')
|
|
372
363
|
.get(loserId);
|
|
373
|
-
const
|
|
374
|
-
const
|
|
375
|
-
if (
|
|
364
|
+
const availableU = toUnits(wallet?.balance ?? 0) + toUnits(wallet?.staked ?? 0);
|
|
365
|
+
const actualFeeU = Math.min(feeU, availableU);
|
|
366
|
+
if (actualFeeU <= 0)
|
|
376
367
|
return { fee: 0, arbitratorShare: 0, protocolShare: 0 };
|
|
377
368
|
// 扣款:先质押后余额
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
const arbitratorShare = isHumanArbitrator ? Math.round(actualFee * 0.5 * 100) / 100 : 0;
|
|
388
|
-
const protocolShare = actualFee - arbitratorShare;
|
|
389
|
-
if (protocolShare > 0) {
|
|
390
|
-
db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(protocolShare, sysUserId);
|
|
391
|
-
}
|
|
392
|
-
if (arbitratorShare > 0) {
|
|
393
|
-
db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(arbitratorShare, arbitratorId);
|
|
394
|
-
}
|
|
395
|
-
return { fee: actualFee, arbitratorShare, protocolShare };
|
|
369
|
+
debitStakeThenBalance(db, loserId, actualFeeU);
|
|
370
|
+
// 分配:人工仲裁各一半(allocate 精确),自动裁定全归协议
|
|
371
|
+
const arbitratorShareU = isHumanArbitrator ? allocate(actualFeeU, [1, 1])[0] : 0;
|
|
372
|
+
const protocolShareU = actualFeeU - arbitratorShareU;
|
|
373
|
+
if (protocolShareU > 0)
|
|
374
|
+
applyWalletDelta(db, sysUserId, { balance: protocolShareU });
|
|
375
|
+
if (arbitratorShareU > 0)
|
|
376
|
+
applyWalletDelta(db, arbitratorId, { balance: arbitratorShareU });
|
|
377
|
+
return { fee: toDecimal(actualFeeU), arbitratorShare: toDecimal(arbitratorShareU), protocolShare: toDecimal(protocolShareU) };
|
|
396
378
|
}
|
|
397
|
-
function executeSettlement(db, orderId, ruling, refundAmount, liablePartyId) {
|
|
379
|
+
export function executeSettlement(db, orderId, ruling, refundAmount, liablePartyId) {
|
|
398
380
|
const order = db.prepare('SELECT * FROM orders WHERE id = ?').get(orderId);
|
|
399
381
|
if (!order)
|
|
400
382
|
return { success: false, error: '订单不存在' };
|
|
@@ -405,143 +387,131 @@ function executeSettlement(db, orderId, ruling, refundAmount, liablePartyId) {
|
|
|
405
387
|
.get(order.product_id);
|
|
406
388
|
const stakeAmount = product?.stake_amount ?? 0;
|
|
407
389
|
const sysUser = db.prepare("SELECT id FROM users WHERE id = 'sys_protocol'").get();
|
|
390
|
+
// RFC-014:整数 base-units
|
|
391
|
+
const totalU = toUnits(totalAmount);
|
|
392
|
+
const stakeAmountU = toUnits(stakeAmount);
|
|
408
393
|
if (ruling === 'refund_buyer') {
|
|
409
|
-
// ── 买家胜诉:退全款 +
|
|
410
|
-
const
|
|
411
|
-
const stakeReturn = stakeAmount - penalty;
|
|
394
|
+
// ── 买家胜诉:退全款 + 卖家损失一半质押(惩罚,allocate 精确二分)──────────────
|
|
395
|
+
const [penaltyU, stakeReturnU] = stakeAmountU > 0 ? allocate(stakeAmountU, [1, 1]) : [0, 0];
|
|
412
396
|
db.transaction(() => {
|
|
413
397
|
// 退还买家托管资金
|
|
414
|
-
db
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
398
|
+
applyWalletDelta(db, buyerId, { escrowed: -totalU, balance: totalU });
|
|
399
|
+
// 卖家扣押全额质押 → 一半补偿买家,一半退回卖家(净损一半)
|
|
400
|
+
if (stakeAmountU > 0) {
|
|
401
|
+
applyWalletDelta(db, sellerId, { staked: -stakeAmountU });
|
|
402
|
+
if (penaltyU > 0)
|
|
403
|
+
applyWalletDelta(db, buyerId, { balance: penaltyU });
|
|
404
|
+
if (stakeReturnU > 0)
|
|
405
|
+
applyWalletDelta(db, sellerId, { balance: stakeReturnU });
|
|
422
406
|
}
|
|
423
|
-
transition(db, orderId, 'refunded_full', sysUser.id, [], `争议裁定:退款买家,质押惩罚 ${
|
|
407
|
+
transition(db, orderId, 'refunded_full', sysUser.id, [], `争议裁定:退款买家,质押惩罚 ${toDecimal(penaltyU)} WAZ`);
|
|
424
408
|
})();
|
|
425
409
|
return {
|
|
426
410
|
success: true,
|
|
427
411
|
detail: {
|
|
428
412
|
ruling: 'refund_buyer',
|
|
429
|
-
buyer_refund:
|
|
430
|
-
buyer_compensation:
|
|
431
|
-
seller_stake_forfeited:
|
|
432
|
-
seller_stake_returned:
|
|
413
|
+
buyer_refund: toDecimal(totalU),
|
|
414
|
+
buyer_compensation: toDecimal(penaltyU),
|
|
415
|
+
seller_stake_forfeited: toDecimal(stakeAmountU),
|
|
416
|
+
seller_stake_returned: toDecimal(stakeReturnU),
|
|
433
417
|
}
|
|
434
418
|
};
|
|
435
419
|
}
|
|
436
420
|
else if (ruling === 'release_seller') {
|
|
437
421
|
// ── 卖家胜诉:资金释放给卖家(正常结算逻辑)──────────────────
|
|
438
|
-
const
|
|
439
|
-
const
|
|
440
|
-
const
|
|
422
|
+
const protocolFeeU = mulRate(totalU, 0.02);
|
|
423
|
+
const logisticsFeeU = order.logistics_id ? mulRate(totalU, 0.05) : 0;
|
|
424
|
+
const sellerAmountU = totalU - protocolFeeU - logisticsFeeU; // 残值
|
|
441
425
|
db.transaction(() => {
|
|
442
|
-
db
|
|
443
|
-
db
|
|
444
|
-
if (order.logistics_id &&
|
|
445
|
-
db.
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
if (
|
|
449
|
-
db
|
|
450
|
-
.run(stakeAmount, stakeAmount, sellerId);
|
|
451
|
-
}
|
|
426
|
+
applyWalletDelta(db, buyerId, { escrowed: -totalU });
|
|
427
|
+
applyWalletDelta(db, sellerId, { balance: sellerAmountU });
|
|
428
|
+
if (order.logistics_id && logisticsFeeU > 0)
|
|
429
|
+
applyWalletDelta(db, order.logistics_id, { balance: logisticsFeeU });
|
|
430
|
+
if (protocolFeeU > 0)
|
|
431
|
+
applyWalletDelta(db, sysUser.id, { balance: protocolFeeU }); // 协议费入金库
|
|
432
|
+
if (stakeAmountU > 0)
|
|
433
|
+
applyWalletDelta(db, sellerId, { staked: -stakeAmountU, balance: stakeAmountU }); // 返还质押
|
|
452
434
|
transition(db, orderId, 'resolved_for_seller', sysUser.id, [], '争议裁定:卖家胜诉,资金释放完成');
|
|
453
435
|
})();
|
|
454
436
|
return {
|
|
455
437
|
success: true,
|
|
456
438
|
detail: {
|
|
457
439
|
ruling: 'release_seller',
|
|
458
|
-
seller_received:
|
|
459
|
-
logistics_fee:
|
|
460
|
-
protocol_fee:
|
|
461
|
-
seller_stake_returned:
|
|
440
|
+
seller_received: toDecimal(sellerAmountU),
|
|
441
|
+
logistics_fee: toDecimal(logisticsFeeU),
|
|
442
|
+
protocol_fee: toDecimal(protocolFeeU),
|
|
443
|
+
seller_stake_returned: toDecimal(stakeAmountU),
|
|
462
444
|
}
|
|
463
445
|
};
|
|
464
446
|
}
|
|
465
447
|
else if (ruling === 'partial_refund') {
|
|
466
|
-
const
|
|
467
|
-
if (
|
|
468
|
-
return { success: false, error: `退款金额 ${
|
|
448
|
+
const refundU = refundAmount != null ? toUnits(refundAmount) : mulRate(totalU, 0.5);
|
|
449
|
+
if (refundU > totalU)
|
|
450
|
+
return { success: false, error: `退款金额 ${toDecimal(refundU)} 超出订单总额 ${totalAmount}` };
|
|
469
451
|
if (liablePartyId) {
|
|
470
452
|
// ── 第三方责任 partial_refund ────────────────────────────────
|
|
471
453
|
// 卖家全额结算(正常收款),买家赔偿由责任方钱包直接支付
|
|
472
|
-
const
|
|
473
|
-
const
|
|
474
|
-
const
|
|
454
|
+
const protocolFeeU = mulRate(totalU, 0.02);
|
|
455
|
+
const logisticsFeeU = order.logistics_id ? mulRate(totalU, 0.05) : 0;
|
|
456
|
+
const sellerAmountU = totalU - protocolFeeU - logisticsFeeU;
|
|
475
457
|
// 检查责任方余额是否足够
|
|
476
|
-
const liableWallet = db.prepare('SELECT balance, staked FROM wallets WHERE user_id = ?')
|
|
458
|
+
const liableWallet = db.prepare('SELECT COALESCE(balance,0) balance, COALESCE(staked,0) staked FROM wallets WHERE user_id = ?')
|
|
477
459
|
.get(liablePartyId);
|
|
478
|
-
const
|
|
479
|
-
const
|
|
460
|
+
const liableAvailableU = toUnits(liableWallet?.balance ?? 0) + toUnits(liableWallet?.staked ?? 0);
|
|
461
|
+
const actualRefundU = Math.min(refundU, liableAvailableU);
|
|
480
462
|
db.transaction(() => {
|
|
481
463
|
// 1. 释放托管 → 正常结算给卖家
|
|
482
|
-
db
|
|
483
|
-
db
|
|
484
|
-
if (order.logistics_id &&
|
|
485
|
-
db.
|
|
486
|
-
|
|
464
|
+
applyWalletDelta(db, buyerId, { escrowed: -totalU });
|
|
465
|
+
applyWalletDelta(db, sellerId, { balance: sellerAmountU });
|
|
466
|
+
if (order.logistics_id && logisticsFeeU > 0)
|
|
467
|
+
applyWalletDelta(db, order.logistics_id, { balance: logisticsFeeU });
|
|
468
|
+
if (protocolFeeU > 0)
|
|
469
|
+
applyWalletDelta(db, sysUser.id, { balance: protocolFeeU }); // 协议费入金库
|
|
487
470
|
// 2. 返还卖家质押
|
|
488
|
-
if (
|
|
489
|
-
db
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
const liableStaked = liableWallet?.staked ?? 0;
|
|
495
|
-
if (liableStaked >= actualRefund) {
|
|
496
|
-
db.prepare('UPDATE wallets SET staked = staked - ? WHERE user_id = ?').run(actualRefund, liablePartyId);
|
|
497
|
-
}
|
|
498
|
-
else {
|
|
499
|
-
const fromBalance = actualRefund - liableStaked;
|
|
500
|
-
db.prepare('UPDATE wallets SET staked = 0, balance = balance - ? WHERE user_id = ?').run(fromBalance, liablePartyId);
|
|
501
|
-
}
|
|
502
|
-
// 4. 赔偿金给买家
|
|
503
|
-
db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(actualRefund, buyerId);
|
|
471
|
+
if (stakeAmountU > 0)
|
|
472
|
+
applyWalletDelta(db, sellerId, { staked: -stakeAmountU, balance: stakeAmountU });
|
|
473
|
+
// 3. 从责任方钱包扣赔偿(先质押后余额) → 4. 给买家
|
|
474
|
+
if (actualRefundU > 0) {
|
|
475
|
+
debitStakeThenBalance(db, liablePartyId, actualRefundU);
|
|
476
|
+
applyWalletDelta(db, buyerId, { balance: actualRefundU });
|
|
504
477
|
}
|
|
505
|
-
transition(db, orderId, 'refunded_partial', sysUser.id, [], `争议裁定:第三方责任赔偿 ${
|
|
478
|
+
transition(db, orderId, 'refunded_partial', sysUser.id, [], `争议裁定:第三方责任赔偿 ${toDecimal(actualRefundU)} WAZ,卖家全额结算`);
|
|
506
479
|
})();
|
|
507
480
|
return {
|
|
508
481
|
success: true,
|
|
509
482
|
detail: {
|
|
510
483
|
ruling: 'partial_refund',
|
|
511
484
|
liable_party: liablePartyId,
|
|
512
|
-
buyer_compensation:
|
|
513
|
-
seller_received:
|
|
514
|
-
logistics_fee:
|
|
515
|
-
protocol_fee:
|
|
516
|
-
seller_stake_returned:
|
|
485
|
+
buyer_compensation: toDecimal(actualRefundU),
|
|
486
|
+
seller_received: toDecimal(sellerAmountU),
|
|
487
|
+
logistics_fee: toDecimal(logisticsFeeU),
|
|
488
|
+
protocol_fee: toDecimal(protocolFeeU),
|
|
489
|
+
seller_stake_returned: toDecimal(stakeAmountU),
|
|
517
490
|
}
|
|
518
491
|
};
|
|
519
492
|
}
|
|
520
493
|
else {
|
|
521
|
-
// ── 买卖双方协商 partial_refund
|
|
522
|
-
const
|
|
523
|
-
|
|
494
|
+
// ── 买卖双方协商 partial_refund ───────────────────────
|
|
495
|
+
const sellerGetU = totalU - refundU;
|
|
496
|
+
// 政策(option a):协商和解无过错 → 全额退质押,不罚没。
|
|
497
|
+
const stakeReturnU = stakeAmountU;
|
|
524
498
|
db.transaction(() => {
|
|
525
|
-
db
|
|
526
|
-
if (
|
|
527
|
-
db
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
db.prepare('UPDATE wallets SET staked = staked - ?, balance = balance + ? WHERE user_id = ?')
|
|
534
|
-
.run(stakeAmount, stakeReturn, sellerId);
|
|
535
|
-
}
|
|
536
|
-
transition(db, orderId, 'refunded_partial', sysUser.id, [], `争议裁定:部分退款 ${refund} WAZ`);
|
|
499
|
+
applyWalletDelta(db, buyerId, { escrowed: -totalU });
|
|
500
|
+
if (refundU > 0)
|
|
501
|
+
applyWalletDelta(db, buyerId, { balance: refundU });
|
|
502
|
+
if (sellerGetU > 0)
|
|
503
|
+
applyWalletDelta(db, sellerId, { balance: sellerGetU });
|
|
504
|
+
if (stakeAmountU > 0)
|
|
505
|
+
applyWalletDelta(db, sellerId, { staked: -stakeAmountU, balance: stakeReturnU });
|
|
506
|
+
transition(db, orderId, 'refunded_partial', sysUser.id, [], `争议裁定:部分退款 ${toDecimal(refundU)} WAZ`);
|
|
537
507
|
})();
|
|
538
508
|
return {
|
|
539
509
|
success: true,
|
|
540
510
|
detail: {
|
|
541
511
|
ruling: 'partial_refund',
|
|
542
|
-
buyer_refund:
|
|
543
|
-
seller_received:
|
|
544
|
-
seller_stake_returned:
|
|
512
|
+
buyer_refund: toDecimal(refundU),
|
|
513
|
+
seller_received: toDecimal(sellerGetU),
|
|
514
|
+
seller_stake_returned: toDecimal(stakeReturnU),
|
|
545
515
|
}
|
|
546
516
|
};
|
|
547
517
|
}
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
* - 其他技能目前免费(增强曝光,间接提升成交)
|
|
17
17
|
*/
|
|
18
18
|
import { generateId } from '../../layer0-foundation/L0-1-database/schema.js';
|
|
19
|
+
import { toUnits, toDecimal } from '../../money.js';
|
|
20
|
+
import { applyWalletDelta } from '../../ledger.js';
|
|
19
21
|
// ─── Schema ───────────────────────────────────────────────────
|
|
20
22
|
export function initSkillSchema(db) {
|
|
21
23
|
db.exec(`
|
|
@@ -223,15 +225,15 @@ export function recordSkillUsage(db, orderId, orderAmount) {
|
|
|
223
225
|
`).get(order.buyer_id, order.seller_id);
|
|
224
226
|
if (!skillSub)
|
|
225
227
|
return;
|
|
226
|
-
const
|
|
227
|
-
if (
|
|
228
|
+
const feeU = Math.round(toUnits(orderAmount) * 0.005); // 0.5% 推荐佣金(整数 base-units)
|
|
229
|
+
if (feeU <= 0)
|
|
228
230
|
return;
|
|
229
231
|
// 从系统账户(protocol fee 池)拨出佣金给 Skill 发布者
|
|
230
232
|
// 简化实现:直接增加卖家钱包余额(Skill 发布者就是卖家本人)
|
|
231
|
-
db.
|
|
233
|
+
applyWalletDelta(db, skillSub.seller_id, { balance: feeU });
|
|
232
234
|
db.prepare('UPDATE skills SET total_uses = total_uses + 1 WHERE id = ?').run(skillSub.skill_id);
|
|
233
235
|
const id = generateId('sul');
|
|
234
|
-
db.prepare('INSERT INTO skill_usage_log (id, skill_id, user_id, order_id, fee_paid) VALUES (?,?,?,?,?)').run(id, skillSub.skill_id, order.buyer_id, orderId,
|
|
236
|
+
db.prepare('INSERT INTO skill_usage_log (id, skill_id, user_id, order_id, fee_paid) VALUES (?,?,?,?,?)').run(id, skillSub.skill_id, order.buyer_id, orderId, toDecimal(feeU));
|
|
235
237
|
}
|
|
236
238
|
// ─── 自动接单 Skill 触发 ──────────────────────────────────────
|
|
237
239
|
/**
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
* 绝不进入 PV 二元匹配 / 推土机三级佣金 / fund_base —— 保持双引擎解耦、规避 PV 合规问题。
|
|
19
19
|
*/
|
|
20
20
|
import { generateId } from '../../layer0-foundation/L0-1-database/schema.js';
|
|
21
|
+
import { toUnits, toDecimal } from '../../money.js';
|
|
22
|
+
import { applyWalletDelta } from '../../ledger.js';
|
|
21
23
|
export const SKILL_KINDS = ['template', 'prompt', 'guide', 'checklist'];
|
|
22
24
|
export const SKILL_BILLING_MODES = ['free', 'one_time', 'per_use'];
|
|
23
25
|
export const SKILL_KIND_META = {
|
|
@@ -231,18 +233,20 @@ function hasUnlock(db, userId, listingId) {
|
|
|
231
233
|
}
|
|
232
234
|
/** 扣买家、加作者净额、协议费入 sys_protocol;记 skill_orders。feeRate 由路由层从协议参数传入。 */
|
|
233
235
|
function settlePayment(db, listing, buyerId, feeRate) {
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
const
|
|
236
|
+
// RFC-014:整数 base-units(买家付 price = 作者净额 net + 协议费 fee,精确守恒)
|
|
237
|
+
const priceU = toUnits(Number(listing.price));
|
|
238
|
+
const feeU = Math.round(priceU * feeRate);
|
|
239
|
+
const netU = priceU - feeU; // 残值,精确
|
|
240
|
+
const price = toDecimal(priceU), fee = toDecimal(feeU), net = toDecimal(netU);
|
|
237
241
|
const tx = db.transaction(() => {
|
|
238
242
|
const w = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(buyerId);
|
|
239
|
-
if (!w || w.balance <
|
|
243
|
+
if (!w || toUnits(w.balance) < priceU)
|
|
240
244
|
throw new Error('余额不足,请先充值');
|
|
241
|
-
db
|
|
242
|
-
db.
|
|
245
|
+
applyWalletDelta(db, buyerId, { balance: -priceU });
|
|
246
|
+
applyWalletDelta(db, listing.author_id, { balance: netU, earned: netU });
|
|
243
247
|
db.prepare("INSERT OR IGNORE INTO wallets (user_id, balance) VALUES ('sys_protocol', 0)").run();
|
|
244
|
-
if (
|
|
245
|
-
db
|
|
248
|
+
if (feeU > 0)
|
|
249
|
+
applyWalletDelta(db, 'sys_protocol', { balance: feeU });
|
|
246
250
|
db.prepare('UPDATE skill_listings SET total_sales = total_sales + 1, total_revenue = total_revenue + ? WHERE id = ?').run(price, listing.id);
|
|
247
251
|
db.prepare('INSERT INTO skill_orders (id, listing_id, buyer_id, billing_mode, amount_paid, protocol_fee, author_net) VALUES (?,?,?,?,?,?,?)')
|
|
248
252
|
.run(generateId('sko'), listing.id, buyerId, listing.billing_mode, price, fee, net);
|
package/dist/ledger.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { toUnits, toDecimal } from './money.js';
|
|
2
|
+
/** 读某用户钱包当前余额(整数 base-units)。无钱包行 → 全 0。 */
|
|
3
|
+
export function walletUnits(db, userId) {
|
|
4
|
+
const r = db.prepare('SELECT COALESCE(balance,0) balance, COALESCE(staked,0) staked, COALESCE(escrowed,0) escrowed, COALESCE(earned,0) earned FROM wallets WHERE user_id = ?')
|
|
5
|
+
.get(userId);
|
|
6
|
+
return { balance: toUnits(r?.balance ?? 0), staked: toUnits(r?.staked ?? 0), escrowed: toUnits(r?.escrowed ?? 0), earned: toUnits(r?.earned ?? 0) };
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* 对钱包字段施加整数单位 delta,以【绝对值】落库(读当前→加 delta→写 toDecimal)。
|
|
10
|
+
* - 字段白名单固定(balance/staked/escrowed/earned),无 SQL 注入面。
|
|
11
|
+
* - 钱包行不存在 → UPDATE 影响 0 行(与历史相对更新的静默行为一致)。
|
|
12
|
+
* - 必须在调用方的 transaction 内使用(资金路径本就如此)。
|
|
13
|
+
*/
|
|
14
|
+
export function applyWalletDelta(db, userId, deltas) {
|
|
15
|
+
const fields = Object.keys(deltas).filter(f => deltas[f] !== undefined && deltas[f] !== 0);
|
|
16
|
+
if (fields.length === 0)
|
|
17
|
+
return;
|
|
18
|
+
const cur = walletUnits(db, userId);
|
|
19
|
+
const sets = [];
|
|
20
|
+
const vals = [];
|
|
21
|
+
for (const f of fields) {
|
|
22
|
+
sets.push(`${f} = ?`);
|
|
23
|
+
vals.push(toDecimal(cur[f] + deltas[f]));
|
|
24
|
+
}
|
|
25
|
+
vals.push(userId);
|
|
26
|
+
db.prepare(`UPDATE wallets SET ${sets.join(', ')} WHERE user_id = ?`).run(...vals);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 从某用户【先扣 staked,不足再扣 balance】,封顶其可用额(staked+balance,不转负)。返回实扣 units。
|
|
30
|
+
* 争议罚没/仲裁费常用:质押优先承担,质押不够再动自由余额。绝对值落库,无浮点 dust。
|
|
31
|
+
*/
|
|
32
|
+
export function debitStakeThenBalance(db, userId, amountU) {
|
|
33
|
+
const cur = walletUnits(db, userId);
|
|
34
|
+
const avail = Math.max(0, cur.staked) + Math.max(0, cur.balance);
|
|
35
|
+
const actual = Math.min(Math.max(0, amountU), avail);
|
|
36
|
+
if (actual <= 0)
|
|
37
|
+
return 0;
|
|
38
|
+
const fromStaked = Math.min(actual, Math.max(0, cur.staked));
|
|
39
|
+
const fromBalance = actual - fromStaked;
|
|
40
|
+
applyWalletDelta(db, userId, { staked: -fromStaked, balance: -fromBalance });
|
|
41
|
+
return actual;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 通用基金/池表的【绝对值】入账(整数 base-units):读当前→加 delta→写 toDecimal。
|
|
45
|
+
* 用于 global_fund / management_bonus_pool / commission_reserve / charity_fund 等单行池表。
|
|
46
|
+
* table / 列名 / whereClause 均为【代码字面量】(非用户输入)→ 无 SQL 注入面。
|
|
47
|
+
* 行不存在 → UPDATE 影响 0 行(与历史相对更新一致)。
|
|
48
|
+
*/
|
|
49
|
+
export function creditColumns(db, table, whereClause, whereArgs, deltas) {
|
|
50
|
+
const cols = Object.keys(deltas).filter(c => deltas[c] !== 0);
|
|
51
|
+
if (cols.length === 0)
|
|
52
|
+
return;
|
|
53
|
+
const cur = db.prepare(`SELECT ${cols.map(c => `COALESCE(${c},0) AS ${c}`).join(', ')} FROM ${table} WHERE ${whereClause}`)
|
|
54
|
+
.get(...whereArgs);
|
|
55
|
+
const sets = cols.map(c => `${c} = ?`).join(', ');
|
|
56
|
+
const vals = cols.map(c => toDecimal(toUnits(cur?.[c] ?? 0) + deltas[c]));
|
|
57
|
+
db.prepare(`UPDATE ${table} SET ${sets} WHERE ${whereClause}`).run(...vals, ...whereArgs);
|
|
58
|
+
}
|