tips.js
2.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import { randomUUID } from 'node:crypto'
import { createBillingRepo, createLedger } from '@drama/billing'
const TIPS = 'vk_tips'
/**
* 打赏:任何成员可给已结算任务的赢家打赏,从自己的可用积分划转,不可撤回。
* 打赏人扣款与收款人入账在同一事务;余额不足时账本抛 INSUFFICIENT_BALANCE,事务回滚。
*
* @param {{ db: import('knex').Knex }} deps
*/
export function createTips({ db }) {
if (typeof db !== 'function') throw new Error('createTips: db is required')
/**
* @param {object} _ctx
* @param {{
* task_id: string,
* from_agent_id: string,
* from_wallet: string,
* amount: number,
* client_request_key: string,
* onSent(trx: import('knex').Knex, sent: { to_wallet: string }): Promise<void>,
* }} input `onSent` 在同一事务里执行(写事件);重放时不再调用。
* @returns {Promise<{ ok: true, tip_id: string, replayed: boolean } | { ok: false, code: 'state_conflict'|'rule_violation', hint: string }>}
*/
async function send(_ctx, { task_id, from_agent_id, from_wallet, amount, client_request_key, onSent }) {
return db.transaction(async (trx) => {
// 锁打赏人的钱包行:同一打赏人的打赏串行,重放在首个事务提交后读到已有记录。
await trx('users').where({ id: from_wallet }).forUpdate().first('id')
const existing = await trx(TIPS).where({ from_agent_id, client_request_key }).first('id')
if (existing !== undefined) return { ok: true, tip_id: existing.id, replayed: true }
const winner = await trx('vk_escrow_payouts').where({ task_id, kind: 'winner' }).first('recipient_wallet')
if (winner === undefined) return { ok: false, code: 'state_conflict', hint: 'Only settled tasks can be tipped.' }
const to_wallet = winner.recipient_wallet
if (to_wallet === from_wallet) return { ok: false, code: 'rule_violation', hint: 'You cannot tip yourself.' }
const id = `tp_${randomUUID()}`
const ledger = createLedger(createBillingRepo(db, trx))
const base = { amount, source: 'voko', reason: 'vk_tip', ref_type: 'vk_tip', ref_id: id }
const out = await ledger.deduct({ ...base, user_id: from_wallet, idempotency_key: `vk:tip:${id}:out` })
const received = await ledger.grant({ ...base, user_id: to_wallet, credit_kind: 'earned', idempotency_key: `vk:tip:${id}:in` })
await trx(TIPS).insert({ id, task_id, from_agent_id, from_wallet, to_wallet, amount, out_ledger_id: out.ledger_id, in_ledger_id: received.ledger_id, client_request_key, created_at: new Date() })
await onSent(trx, { to_wallet })
return { ok: true, tip_id: id, replayed: false }
})
}
return { send }
}