fix: 提现排除处理中金额,选择报价改为重点提示

可提现额度扣除处理中提现,避免重复提现超总额;收到报价选择供应商时展示重点提示而非服务费提示。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-11 00:47:54 +08:00
parent ca7d179d1b
commit b341d6ad97
5 changed files with 258 additions and 93 deletions
+33 -24
View File
@@ -258,8 +258,7 @@
</view>
<view class="mt-15">项目名称:{{ sheet.projectName ? sheet.projectName : '' }}</view>
<view class="mt-15">项目地址:{{ sheet.fullAddress }}</view>
<template
v-if="isLoggedIn && (sheet.isMine || sheet.hasQuote || userInfo.userLevel > 0)">
<template v-if="canViewBuyerInfo">
<view class="mt-15" v-if="sheet.companyName">公司信息:{{
sheet.companyName ? sheet.companyName : ''
}}
@@ -564,6 +563,18 @@ export default {
isLoggedIn() {
return !!vk.getVuex('$user.userInfo._id')
},
canViewBuyerInfo() {
const remainQuoteNum = Number(this.userInfo && this.userInfo.remainQuoteNum) || 0
const quoteExpiredAt = this.userInfo && this.userInfo.quoteExpiredAt
const monthlyQuoteActive = !!(quoteExpiredAt && dayjs(quoteExpiredAt).isAfter(dayjs()))
return this.isLoggedIn && (
this.sheet.isMine ||
this.sheet.hasQuote ||
Number(this.userInfo.userLevel) > 0 ||
remainQuoteNum > 0 ||
monthlyQuoteActive
)
},
batchProductNames() {
if (this.sheet.otherCate) return this.sheet.otherCate
return this.sheet.productList.map(item => item.goodsName + `${item.materialQuality ? `(${item.materialQuality})` : ''}`).join(',')
@@ -603,18 +614,18 @@ export default {
* 打开报价窗口
*/
openQuoteDialog() {
const h = this.$createElement
this.$confirm(h('div', {class: 'dialog-confirm-content'}, [
h('p', '1、请您谨慎点击"选择",选择供应商后,系统将自动关闭您此条询价信息,无法返回,只能再次发布询价信息;'),
h('p', '2、点击"选择"前,您可通过供应商联系方式,仔细商讨:商品明细、数量、单价、执行标准、交货方式、交货期、验收方式、合同条款等细节关键题确认,各种交易细节无误后要求供应商进行最终报价;'),
h('p', '3、您承诺对此次交易的内容用途已经完全了解并知悉经过严格仔细审核后实施,此次交易为合法经营,如违反相关法律规定,您将承担全部责任,与平台无关。您确认以上描述无误后请点击"选择"。')
]), '重点提示', {
confirmButtonText: '同意悉知',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// const h = this.$createElement
// this.$confirm(h('div', {class: 'dialog-confirm-content'}, [
// h('p', '1、请您谨慎点击"选择",选择供应商后,系统将自动关闭您此条询价信息,无法返回,只能再次发布询价信息;'),
// h('p', '2、点击"选择"前,您可通过供应商联系方式,仔细商讨:商品明细、数量、单价、执行标准、交货方式、交货期、验收方式、合同条款等细节关键题确认,各种交易细节无误后要求供应商进行最终报价;'),
// h('p', '3、您承诺对此次交易的内容用途已经完全了解并知悉经过严格仔细审核后实施,此次交易为合法经营,如违反相关法律规定,您将承担全部责任,与平台无关。您确认以上描述无误后请点击"选择"。')
// ]), '重点提示', {
// confirmButtonText: '同意悉知',
// cancelButtonText: '取消',
// type: 'warning'
// }).then(() => {
this.showDialog = true
}).catch(() => {})
// }).catch(() => {})
},
openQuoteServiceFeeDialog() {
const rate = this.sheet.notaryServiceRate || 0.1
@@ -731,12 +742,14 @@ export default {
confirmButtonText: '确定'
})
},
showNotaryServiceFeeTip(item) {
const rate = this.sheet.notaryServiceRate || 0.1
this.$confirm(`提示若您成功签约交易,平台将收取总交易金额 x${rate}%服务费,如您存在利用系统漏洞恶意逃避支付服务费的情况。
平台有将您的失信行为进行公布,情况严重的平台予以劝退。
并有权依法追回平台服务费。您承诺此次交易为合法经营,如违反相关法律法规您将承担全部任与平台无关。您己经完全理解并知悉,您不同意请点击“取消”,取消报价,您同意以上约定请点击“同意并知悉”。`, '温馨提示', {
confirmButtonText: '同意并知悉',
showSelectQuoteTip(item) {
const h = this.$createElement
this.$confirm(h('div', {class: 'dialog-confirm-content'}, [
h('p', '1、请您谨慎点击"选择",选择供应商后,系统将自动关闭您此条询价信息,无法返回,只能再次发布询价信息;'),
h('p', '2、点击"选择"前,您可通过供应商联系方式,仔细商讨:商品明细、数量、单价、执行标准、交货方式、交货期、验收方式、合同条款等细节问题确认,各种交易细节无误后要求供应商进行最终报价;'),
h('p', '3、您承诺对此次交易的内容用途已经完全了解并知悉经过严格仔细审核后实施,此次交易为合法经营,如违反相关法律规定,您将承担全部责任,与平台无关。您确认以上描述无误后请点击"选择"。')
]), '重点提示', {
confirmButtonText: '同意悉知',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
@@ -769,11 +782,7 @@ export default {
this.showNotaryBusinessTip()
return
}
if (this.sheet.tradeMethod === 1) {
this.showNotaryServiceFeeTip(item)
return
}
this.doConfirm(item)
this.showSelectQuoteTip(item)
},
getFileExt(url = "") {
const cleanUrl = String(url).split("?")[0].split("#")[0]
@@ -8,15 +8,17 @@ module.exports = {
* @param {String} accNo 账号
* @param {String} reqDate 请求日期,格式:yyyyMMdd
* @param {String} reqJournalNo 请求流水号
* @param {String} logId 可选,流水ID,传入后会回写状态
*/
main: async (event) => {
let { data = {}, util } = event;
let { vk } = util;
let { accNo, reqDate, reqJournalNo } = data;
let { accNo, reqDate, reqJournalNo, logId } = data;
accNo = String(accNo || '').trim();
reqDate = String(reqDate || '').trim();
reqJournalNo = String(reqJournalNo || '').trim();
logId = String(logId || '').trim();
if (!accNo) {
return {
@@ -46,43 +48,83 @@ module.exports = {
};
}
let res = 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: {
let res = await balanceUtil.requestActionLog({
vk,
accNo,
reqDate,
reqJournalNo
},
timeout: 60000
});
if (res && typeof res === 'object' && typeof res.code === 'undefined') {
res.code = 0;
}
const responseObjects = balanceUtil.getRemoteResponseObjects(res);
const list = balanceUtil.pickFirstDefined([
Array.isArray(res.list) ? res.list : undefined,
Array.isArray(res.rows) ? res.rows : undefined,
Array.isArray(res.data) ? res.data : undefined,
Array.isArray(res.data && res.data.list) ? res.data.list : undefined,
Array.isArray(res.data && res.data.rows) ? res.data.rows : undefined,
Array.isArray(res.info && res.info.list) ? res.info.list : undefined,
Array.isArray(res.info && res.info.rows) ? res.info.rows : undefined,
Array.isArray(res.content && res.content.list) ? res.content.list : undefined,
Array.isArray(res.content && res.content.rows) ? res.content.rows : undefined
]) || [];
const status = balanceUtil.getActionStatus(res);
const statusText = balanceUtil.getActionStatusTextByStatus(status);
if (status !== null) {
let logInfo = null;
if (logId) {
logInfo = await vk.baseDao.findByWhereJson({
dbName: 'jjx-balance-log',
whereJson: {
_id: logId
}
});
}
if (!logInfo) {
logInfo = await vk.baseDao.findByWhereJson({
dbName: 'jjx-balance-log',
whereJson: {
srcReqId: reqJournalNo,
merchantId: accNo
}
});
}
if (logInfo && logInfo._id) {
logId = logInfo._id;
const prevStatus = Number(logInfo.status);
const nextStatus = Number(status);
const updateData = {
status: nextStatus
};
// 新提现单(balanceDeducted === false):成功时再扣减余额缓存
// 旧提现单无该字段:创建时已扣减,这里只更新状态
const needDeductOnSuccess = logInfo.balanceDeducted === false
&& prevStatus === 1
&& nextStatus === 2;
if (needDeductOnSuccess) {
const amount = balanceUtil.normalizeAmount(logInfo.amount);
await balanceUtil.applyMerchantBalanceDelta({
vk,
merchantId: accNo,
delta: -amount,
lastLogId: logId
});
updateData.balanceDeducted = true;
}
await vk.baseDao.updateById({
dbName: 'jjx-balance-log',
id: logId,
dataJson: updateData
});
}
}
const responseObjects = balanceUtil.getRemoteResponseObjects(res);
if (responseObjects.data && typeof responseObjects.data === 'object' && !Array.isArray(responseObjects.data)) {
responseObjects.data.list = list;
responseObjects.data.status = status;
responseObjects.data.statusText = statusText;
responseObjects.data.logId = logId;
} else if (res && typeof res === 'object') {
res.data = {
list
status,
statusText,
logId
};
}
@@ -1,5 +1,5 @@
'use strict';
const { BALANCE_DOC_ID, getBalanceInfoList } = require('./util.js');
const { BALANCE_DOC_ID, getBalanceInfoList, getAvailableWithdrawBalance, normalizeAmount } = require('./util.js');
module.exports = {
/**
@@ -25,6 +25,24 @@ module.exports = {
}
const balanceInfoList = getBalanceInfoList(info.balanceInfo || {});
const enrichedList = [];
for (let i = 0; i < balanceInfoList.length; i++) {
const item = balanceInfoList[i];
const {
pendingAmount,
availableBalance
} = await getAvailableWithdrawBalance({
vk,
merchantId: item.accNo,
merchantBalance: item.balance
});
enrichedList.push({
...item,
balance: normalizeAmount(item.balance),
pendingAmount,
availableBalance
});
}
return {
code: 0,
@@ -32,7 +50,7 @@ module.exports = {
data: {
balance: Number(Number(info.balance || 0).toFixed(2)),
updateTime: info.updateTime || info._update_time_str || info._add_time_str || '',
balanceInfoList
balanceInfoList: enrichedList
}
};
}
@@ -213,6 +213,64 @@ function getActionStatusTextByStatus(status) {
return statusMap[Number(status)] || '';
}
async function applyMerchantBalanceDelta({
vk,
transaction,
merchantId,
delta,
lastLogId = '',
updateTime = dayjs().format('YYYY-MM-DD HH:mm:ss')
}) {
const accNo = String(merchantId || '').trim();
if (!accNo) {
throw new Error('提现账号不能为空');
}
const changeAmount = normalizeAmount(delta);
const balanceRecord = await ensureBalanceRecord({ transaction, 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(currentMerchantBalance + changeAmount);
if (nextMerchantBalance < 0) {
throw new Error('余额不足');
}
const nextBalanceInfoMap = {
...balanceInfoMap,
[accNo]: {
accNo,
merchantName: currentMerchantInfo.merchantName || getMerchantLabel(accNo),
balance: nextMerchantBalance,
updateTime
}
};
const nextTotalBalance = normalizeAmount(Number(balanceRecord.balance || 0) + changeAmount);
const dataJson = {
balance: nextTotalBalance,
balanceInfo: nextBalanceInfoMap,
updateTime
};
if (lastLogId) {
dataJson.lastLogId = lastLogId;
}
await vk.baseDao.updateById({
db: transaction,
dbName: 'jjx-balance',
id: balanceRecord._id,
dataJson
});
return {
balance: nextTotalBalance,
merchantBalance: nextMerchantBalance,
updateTime,
balanceInfoMap: nextBalanceInfoMap
};
}
async function syncMerchantBalance({
vk,
accNo,
@@ -323,6 +381,49 @@ async function checkBalanceAvailable({ transaction, vk, changeAmount, excludeLog
};
}
/**
* 统计指定商户处理中的提现金额(status=1)
*/
async function getPendingWithdrawAmount({ transaction, vk, merchantId }) {
const accNo = String(merchantId || '').trim();
if (!accNo) {
return 0;
}
const res = await vk.baseDao.select({
db: transaction,
dbName: 'jjx-balance-log',
whereJson: {
merchantId: accNo,
type: 'expense',
source: 'withdraw',
status: 1
},
pageIndex: 1,
pageSize: 1000,
getCount: false
});
const rows = (res && res.rows) || [];
return normalizeAmount(rows.reduce((sum, item) => sum + Number(item.amount || 0), 0));
}
/**
* 可提现余额 = 当前余额 - 处理中提现金额
*/
async function getAvailableWithdrawBalance({ transaction, vk, merchantId, merchantBalance }) {
const pendingAmount = await getPendingWithdrawAmount({
transaction,
vk,
merchantId
});
const balance = normalizeAmount(merchantBalance);
const availableBalance = normalizeAmount(Math.max(0, balance - pendingAmount));
return {
balance,
pendingAmount,
availableBalance
};
}
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));
@@ -372,10 +473,13 @@ module.exports = {
parseOrderSet,
getActionStatus,
getActionStatusTextByStatus,
applyMerchantBalanceDelta,
syncMerchantBalance,
ensureBalanceRecord,
recalculateBalance,
checkBalanceAvailable,
getPendingWithdrawAmount,
getAvailableWithdrawBalance,
updateBalanceWithDelta,
rollbackTransaction
};
@@ -1,5 +1,4 @@
'use strict';
const dayjs = require('dayjs');
const balanceUtil = require('./util.js');
const merchantMap = balanceUtil.MERCHANT_MAP;
@@ -34,11 +33,28 @@ module.exports = {
updateTime: ''
};
const merchantBalance = balanceUtil.normalizeAmount(merchantBalanceInfo.balance || 0);
if (merchantBalance <= 0) {
throw new Error('当前企业余额小于等于0,无法提现');
const {
pendingAmount,
availableBalance
} = await balanceUtil.getAvailableWithdrawBalance({
transaction,
vk,
merchantId,
merchantBalance
});
if (availableBalance <= 0) {
throw new Error(
pendingAmount > 0
? `可提现余额不足(处理中提现¥${pendingAmount.toFixed(2)}已占用)`
: '当前企业余额小于等于0,无法提现'
);
}
if (amount > merchantBalance) {
throw new Error('提现金额不能大于当前企业余额');
if (amount > availableBalance) {
throw new Error(
pendingAmount > 0
? `提现金额不能大于可提现余额¥${availableBalance.toFixed(2)}(已排除处理中提现¥${pendingAmount.toFixed(2)})`
: '提现金额不能大于当前企业余额'
);
}
const description = String(data.description || '').trim() || `余额提现-${merchantMap[merchantId]}`;
@@ -80,52 +96,28 @@ module.exports = {
merchantId,
merchantName: merchantMap[merchantId],
srcReqId: requestRes.srcReqId,
status: 1
status: 1,
// 新逻辑:处理中不扣余额,成功后再扣;用于兼容旧数据(旧单默认已扣)
balanceDeducted: false
};
const changeAmount = balanceUtil.buildBalanceChange(logData);
await balanceUtil.checkBalanceAvailable({
transaction,
vk,
changeAmount
});
const addRes = await vk.baseDao.add({
await vk.baseDao.add({
db: transaction,
dbName: 'jjx-balance-log',
dataJson: logData
});
const updateTime = dayjs().format('YYYY-MM-DD HH:mm:ss');
const nextMerchantBalance = Number((merchantBalance + changeAmount).toFixed(2));
const nextBalance = Number((Number(balanceRecord.balance || 0) + changeAmount).toFixed(2));
await vk.baseDao.updateById({
db: transaction,
dbName: 'jjx-balance',
id: balanceRecord._id,
dataJson: {
balance: nextBalance,
lastLogId: addRes.id,
updateTime,
balanceInfo: {
...balanceInfoMap,
[merchantId]: {
accNo: merchantId,
merchantName: merchantMap[merchantId],
balance: nextMerchantBalance,
updateTime
}
}
}
});
// 处理中提现不立刻扣减余额缓存,避免与远端余额同步互相覆盖;
// 可提现额度通过「余额 - 处理中提现」计算,防止重复提现超总额。
await transaction.commit();
return {
code: 0,
msg: '',
data: {
balance: nextBalance,
merchantBalance: nextMerchantBalance
balance: merchantBalance,
pendingAmount: balanceUtil.normalizeAmount(pendingAmount + amount),
availableBalance: balanceUtil.normalizeAmount(Math.max(0, availableBalance - amount)),
merchantBalance
}
};
} catch (err) {