'use strict'; const dayjs = require('dayjs'); const BALANCE_DOC_ID = 'e35392d069f2104f003790b271c67c6d'; const MERCHANT_OPTIONS = [ { accNo: 'zg000002466577', label: '广西金久信电子商务有限公司' }, { accNo: 'zg000002460927', label: '南宁亿利通钢管有限公司' }, { accNo: 'zg000002494918', label: '广西冀旺贸易有限公司' }, { accNo: 'zg000002459804', label: '广西金久信电子商务有限公司' } ]; const MERCHANT_MAP = MERCHANT_OPTIONS.reduce((map, item) => { map[item.accNo] = item.label; return map; }, {}); function normalizeAmount(value) { const amount = Number(value || 0); if (!Number.isFinite(amount)) { throw new Error('金额格式不正确'); } return Number(amount.toFixed(2)); } function normalizeFenToYuan(value) { const amount = Number(value || 0); if (!Number.isFinite(amount)) { throw new Error('金额格式不正确'); } return Number((amount / 100).toFixed(2)); } function normalizeType(type) { if (!['income', 'expense'].includes(type)) { throw new Error('类型不正确'); } return type; } function normalizeTime(value) { const time = Number(value || Date.now()); if (!Number.isFinite(time) || time <= 0) { throw new Error('时间格式不正确'); } return time; } function buildBalanceChange(log = {}) { const type = normalizeType(log.type); const amount = normalizeAmount(log.amount); return type === 'income' ? amount : Number((-amount).toFixed(2)); } function getMerchantLabel(accNo) { return MERCHANT_MAP[accNo] || ''; } function buildBalanceInfoMap(source = {}) { const rawSource = source && typeof source === 'object' ? source : {}; const balanceInfoMap = {}; MERCHANT_OPTIONS.forEach(item => { const current = rawSource[item.accNo] || {}; balanceInfoMap[item.accNo] = { accNo: item.accNo, merchantName: current.merchantName || current.label || item.label, balance: normalizeAmount(current.balance || 0), updateTime: current.updateTime || '' }; }); return balanceInfoMap; } function getBalanceInfoList(source = {}) { const balanceInfoMap = buildBalanceInfoMap(source); return MERCHANT_OPTIONS.map(item => balanceInfoMap[item.accNo]); } function sumBalanceInfoMap(source = {}) { const balanceInfoMap = buildBalanceInfoMap(source); return Number( Object.keys(balanceInfoMap).reduce((sum, accNo) => { return sum + Number(balanceInfoMap[accNo].balance || 0); }, 0).toFixed(2) ); } function getRemoteResponseObjects(res = {}) { return { root: res && typeof res === 'object' ? res : {}, data: res && res.data && typeof res.data === 'object' ? res.data : {}, info: res && res.info && typeof res.info === 'object' ? res.info : {}, content: res && res.content && typeof res.content === 'object' ? res.content : {} }; } function pickFirstDefined(values = []) { return values.find(value => value !== undefined && value !== null && value !== ''); } function getRemoteMerchantBalance(res = {}) { const { root, data, info, content } = getRemoteResponseObjects(res); const canPayAmt = pickFirstDefined([ root.canPayAmt, data.canPayAmt, info.canPayAmt, content.canPayAmt ]); if (canPayAmt !== undefined) { return normalizeFenToYuan(canPayAmt); } const balanceValue = pickFirstDefined([ data.balance, info.balance, content.balance, data.availableBalance, info.availableBalance, content.availableBalance, data.amount, info.amount, content.amount ]); if (balanceValue !== undefined) { return normalizeAmount(balanceValue); } return null; } async function requestMerchantBalance({ vk, accNo }) { return await vk.request({ url: 'http://jjx-union-pay.gxwebsoft.com/api/union-pay-jjx/getInfo', method: 'POST', dataType: 'json', header: { 'content-type': 'application/json; charset=utf-8' }, data: { accNo }, timeout: 60000 }); } async function requestActionLog({ vk, accNo, reqDate, reqJournalNo }) { return await vk.request({ url: 'http://jjx-union-pay.gxwebsoft.com/api/union-pay-jjx/getActionLog', method: 'POST', dataType: 'json', header: { 'content-type': 'application/json; charset=utf-8' }, data: { accNo, reqDate, reqJournalNo }, timeout: 60000 }); } function pickOrderSet(res = {}) { const candidates = [ res && res.orderSet, res && res.data && res.data.orderSet, res && res.info && res.info.orderSet, res && res.content && res.content.orderSet, res && res.data && Array.isArray(res.data) && res.data[0] && res.data[0].orderSet, res && res.list && Array.isArray(res.list) && res.list[0] && res.list[0].orderSet, res && res.rows && Array.isArray(res.rows) && res.rows[0] && res.rows[0].orderSet, res && res.data && res.data.list && Array.isArray(res.data.list) && res.data.list[0] && res.data.list[0].orderSet, res && res.data && res.data.rows && Array.isArray(res.data.rows) && res.data.rows[0] && res.data.rows[0].orderSet ]; for (let i = 0; i < candidates.length; i++) { if (typeof candidates[i] === 'string' && candidates[i]) { return candidates[i]; } } return ''; } function parseOrderSet(res = {}) { const orderSet = pickOrderSet(res); if (!orderSet) { return []; } try { const orderList = JSON.parse(orderSet); return Array.isArray(orderList) ? orderList : []; } catch (err) { console.error('parseOrderSet error', err, orderSet); return []; } } function getActionStatus(res = {}) { const orderList = parseOrderSet(res); if (!orderList.length) { return null; } const firstOrder = orderList[0] || {}; const status = Number(firstOrder.status); return Number.isFinite(status) ? status : null; } function getActionStatusTextByStatus(status) { const statusMap = { 1: '处理中', 2: '成功', 3: '处理失败', 5: '退票', 6: '退票重提' }; return statusMap[Number(status)] || ''; } async function syncMerchantBalance({ vk, accNo, merchantBalance, updateTime = dayjs().format('YYYY-MM-DD HH:mm:ss') }) { const balanceRecord = await ensureBalanceRecord({ vk }); const balanceInfoMap = buildBalanceInfoMap(balanceRecord.balanceInfo || {}); const currentMerchantInfo = balanceInfoMap[accNo] || { accNo, merchantName: getMerchantLabel(accNo), balance: 0, updateTime: '' }; const currentMerchantBalance = normalizeAmount(currentMerchantInfo.balance || 0); const nextMerchantBalance = normalizeAmount(merchantBalance); const nextBalanceInfoMap = { ...balanceInfoMap, [accNo]: { accNo, merchantName: currentMerchantInfo.merchantName || getMerchantLabel(accNo), balance: nextMerchantBalance, updateTime } }; const nextTotalBalance = Number( (Number(balanceRecord.balance || 0) - currentMerchantBalance + nextMerchantBalance).toFixed(2) ); await vk.baseDao.updateById({ dbName: 'jjx-balance', id: balanceRecord._id, dataJson: { balance: nextTotalBalance, balanceInfo: nextBalanceInfoMap, updateTime } }); return { balance: nextTotalBalance, merchantBalance: nextMerchantBalance, updateTime, balanceInfoMap: nextBalanceInfoMap }; } async function ensureBalanceRecord({ transaction, vk }) { let balanceInfo = await vk.baseDao.findByWhereJson({ db: transaction, dbName: 'jjx-balance', whereJson: { _id: BALANCE_DOC_ID } }); if (!balanceInfo) { throw new Error(`jjx-balance余额记录不存在: ${BALANCE_DOC_ID}`); } return balanceInfo; } async function recalculateBalance({ transaction, vk, logId }) { const res = await vk.baseDao.select({ db: transaction, dbName: 'jjx-balance-log', whereJson: {}, sortArr: [{ name: '_add_time', type: 'desc' }], pageIndex: 1, pageSize: 5000 }); const balance = Number(res.rows.reduce((sum, item) => sum + buildBalanceChange(item), 0).toFixed(2)); const balanceInfo = await ensureBalanceRecord({ transaction, vk }); await vk.baseDao.updateById({ db: transaction, dbName: 'jjx-balance', id: balanceInfo._id, dataJson: { balance, lastLogId: logId || balanceInfo.lastLogId || '' } }); return balance; } async function checkBalanceAvailable({ transaction, vk, changeAmount, excludeLogId = '' }) { const balanceInfo = await ensureBalanceRecord({ transaction, vk }); let currentBalance = Number(balanceInfo.balance || 0); if (excludeLogId) { const oldLog = await vk.baseDao.findByWhereJson({ db: transaction, dbName: 'jjx-balance-log', whereJson: { _id: excludeLogId } }); if (!oldLog) { throw new Error('原始流水不存在'); } currentBalance = Number((currentBalance - buildBalanceChange(oldLog)).toFixed(2)); } const nextBalance = Number((currentBalance + changeAmount).toFixed(2)); if (nextBalance < 0) { throw new Error('余额不足'); } return { currentBalance, nextBalance }; } async function updateBalanceWithDelta({ transaction, vk, delta, lastLogId = '' }) { const balanceInfo = await ensureBalanceRecord({ transaction, vk }); const nextBalance = Number((Number(balanceInfo.balance || 0) + Number(delta || 0)).toFixed(2)); await vk.baseDao.updateById({ db: transaction, dbName: 'jjx-balance', id: balanceInfo._id, dataJson: { balance: nextBalance, lastLogId: lastLogId || balanceInfo.lastLogId || '' } }); return nextBalance; } async function rollbackTransaction({ transaction, err }) { try { await transaction.rollback(); } catch (rollbackErr) { console.error(rollbackErr); } return { code: -1, msg: err.message || '操作失败' }; } module.exports = { BALANCE_DOC_ID, MERCHANT_OPTIONS, MERCHANT_MAP, normalizeAmount, normalizeFenToYuan, normalizeType, normalizeTime, buildBalanceChange, getMerchantLabel, buildBalanceInfoMap, getBalanceInfoList, sumBalanceInfoMap, getRemoteResponseObjects, pickFirstDefined, getRemoteMerchantBalance, requestMerchantBalance, requestActionLog, pickOrderSet, parseOrderSet, getActionStatus, getActionStatusTextByStatus, syncMerchantBalance, ensureBalanceRecord, recalculateBalance, checkBalanceAvailable, updateBalanceWithDelta, rollbackTransaction };