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, * }} 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 } }