20261008
This commit is contained in:
@@ -0,0 +1,381 @@
|
||||
'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
|
||||
};
|
||||
Reference in New Issue
Block a user