diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/domain/CustomerBalanceFlow.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/domain/CustomerBalanceFlow.java index c0b3a952..6de7c97a 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/domain/CustomerBalanceFlow.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/domain/CustomerBalanceFlow.java @@ -42,9 +42,15 @@ public class CustomerBalanceFlow extends BaseEntity /** 客户账户ID */ private Long accountId; - /** 币种 */ + /** 币种(业务原币) */ private String currency; + /** 对CNY汇率(1原币=?CNY,CNY为1) */ + private BigDecimal exchangeRate; + + /** 本位币金额(CNY,实际参与余额变动的金额) */ + private BigDecimal baseAmount; + /** 业务类型:RECEIPT/BILL_PAY/REFUND/ADJUST/FREEZE/UNFREEZE */ private String bizType; diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/domain/CustomerReceipt.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/domain/CustomerReceipt.java index 99ddc9e7..1d126239 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/domain/CustomerReceipt.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/domain/CustomerReceipt.java @@ -59,9 +59,15 @@ public class CustomerReceipt extends BaseEntity /** 待核销金额 */ private BigDecimal unallocatedAmount; - /** 币种 */ + /** 币种(收款原币) */ private String currency; + /** 对CNY汇率(1原币=?CNY,CNY为1;默认取汇率表,可手工修改) */ + private BigDecimal exchangeRate; + + /** 本位币金额(CNY,原币金额×汇率) */ + private BigDecimal baseAmount; + /** 收款方式:BANK_TRANSFER/ALIPAY/WECHAT/CASH/OTHER */ private String payMethod; diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/service/impl/CustomerMoneyServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/service/impl/CustomerMoneyServiceImpl.java index df105d25..f461a19c 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/service/impl/CustomerMoneyServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/service/impl/CustomerMoneyServiceImpl.java @@ -1,6 +1,7 @@ package com.ruoyi.customer.customermoney.service.impl; import java.math.BigDecimal; +import java.math.RoundingMode; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Date; @@ -20,6 +21,8 @@ import com.ruoyi.customer.customermoney.domain.CustomerBalanceFlow; import com.ruoyi.customer.customermoney.domain.CustomerReceipt; import com.ruoyi.customer.customermoney.mapper.CustomerMoneyMapper; import com.ruoyi.customer.customermoney.service.ICustomerMoneyService; +import com.ruoyi.system.domain.BmsExchangeRate; +import com.ruoyi.system.service.IBmsExchangeRateService; /** * 客户余额/收款/流水 Service实现 @@ -32,12 +35,20 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService { private static final DateTimeFormatter NO_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"); + /** 本位币币种 */ + private static final String BASE_CURRENCY = BmsExchangeRate.BASE_CURRENCY; + /** 金额保留位数 */ + private static final int MONEY_SCALE = 2; + @Autowired private CustomerMoneyMapper customerMoneyMapper; @Autowired private BmsCustomerMapper bmsCustomerMapper; + @Autowired + private IBmsExchangeRateService exchangeRateService; + @Override public List selectAccountList(Long customerId) { @@ -81,12 +92,41 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService } if (!StringUtils.hasText(receipt.getCurrency())) { - receipt.setCurrency("CNY"); + receipt.setCurrency(BASE_CURRENCY); } + else + { + receipt.setCurrency(receipt.getCurrency().trim().toUpperCase()); + } + // 汇率与本位币金额:CNY 固定 1:1;外币默认取汇率表即期汇率,也允许页面手工修改后传入 + BigDecimal rate = receipt.getExchangeRate(); + if (BASE_CURRENCY.equals(receipt.getCurrency())) + { + rate = BigDecimal.ONE; + } + else + { + if (rate == null || rate.compareTo(BigDecimal.ZERO) <= 0) + { + Date bizDate = receipt.getReceivedAt() != null ? receipt.getReceivedAt() : new Date(); + BmsExchangeRate rateConfig = exchangeRateService.resolveRate( + receipt.getCurrency(), BASE_CURRENCY, BmsExchangeRate.RATE_TYPE_SPOT, bizDate); + rate = rateConfig.getExchangeRate(); + } + if (rate.compareTo(BigDecimal.ZERO) <= 0) + { + throw new ServiceException("汇率必须大于0,请检查录入的汇率"); + } + } + receipt.setExchangeRate(rate.setScale(8, RoundingMode.HALF_UP)); + BigDecimal baseAmount = receipt.getAmount().multiply(rate).setScale(MONEY_SCALE, RoundingMode.HALF_UP); + receipt.setBaseAmount(baseAmount); + receipt.setReceiptNo(generateNo("SK")); receipt.setStatus(CustomerReceipt.STATUS_PENDING); receipt.setAllocatedAmount(BigDecimal.ZERO); - receipt.setUnallocatedAmount(receipt.getAmount()); + // 核销口径统一为本位币 CNY + receipt.setUnallocatedAmount(baseAmount); if (!StringUtils.hasText(receipt.getInvoiceStatus())) { receipt.setInvoiceStatus("NONE"); @@ -113,17 +153,26 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService throw new ServiceException("仅待确认的收款单可以审核"); } - String currency = StringUtils.hasText(receipt.getCurrency()) ? receipt.getCurrency() : "CNY"; + String currency = StringUtils.hasText(receipt.getCurrency()) ? receipt.getCurrency().trim().toUpperCase() + : BASE_CURRENCY; String username = SecurityUtils.getUsername(); Long userId = SecurityUtils.getUserId(); - // 1. 行锁锁定客户币种账户,不存在则自动初始化 - CustomerAccount account = customerMoneyMapper.selectAccountForUpdate(receipt.getCustomerId(), currency); + // 本位币入账金额(兼容历史数据:未折算时按原金额/汇率1处理) + BigDecimal rate = receipt.getExchangeRate() != null && receipt.getExchangeRate().compareTo(BigDecimal.ZERO) > 0 + ? receipt.getExchangeRate() : BigDecimal.ONE; + BigDecimal originalAmount = nz(receipt.getAmount()); + BigDecimal baseAmount = receipt.getBaseAmount() != null + ? receipt.getBaseAmount() + : originalAmount.multiply(rate).setScale(MONEY_SCALE, RoundingMode.HALF_UP); + + // 1. 行锁锁定客户本位币(CNY)账户,不存在则自动初始化。无论收什么币种,一律折算 CNY 入账 + CustomerAccount account = customerMoneyMapper.selectAccountForUpdate(receipt.getCustomerId(), BASE_CURRENCY); if (account == null) { account = new CustomerAccount(); account.setCustomerId(receipt.getCustomerId()); - account.setCurrency(currency); + account.setCurrency(BASE_CURRENCY); account.setBalance(BigDecimal.ZERO); account.setFrozenBalance(BigDecimal.ZERO); account.setTotalCredit(BigDecimal.ZERO); @@ -135,16 +184,15 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService customerMoneyMapper.insertAccount(account); } - // 2. 变动余额(预充值/混合增加余额;还款核销释放已用额度) - BigDecimal amount = nz(receipt.getAmount()); + // 2. 本位币余额变动(预充值/混合增加余额;还款核销释放已用额度) BigDecimal beforeBalance = nz(account.getBalance()); - BigDecimal afterBalance = beforeBalance.add(amount); + BigDecimal afterBalance = beforeBalance.add(baseAmount); BigDecimal frozenBalance = nz(account.getFrozenBalance()); account.setBalance(afterBalance); if (CustomerReceipt.TYPE_REPAY.equals(receipt.getReceiptType())) { - account.setUsedCredit(nz(account.getUsedCredit()).subtract(amount).max(BigDecimal.ZERO)); + account.setUsedCredit(nz(account.getUsedCredit()).subtract(baseAmount).max(BigDecimal.ZERO)); } account.setUpdateBy(username); @@ -154,17 +202,19 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService throw new ServiceException("账户余额已被其他操作变动,请刷新后重试"); } - // 3. 写余额流水 + // 3. 写余额流水:原币金额留痕,本位币金额参与余额变动 CustomerBalanceFlow flow = new CustomerBalanceFlow(); flow.setFlowNo(generateNo("LS")); flow.setCustomerId(receipt.getCustomerId()); flow.setAccountId(account.getId()); flow.setCurrency(currency); + flow.setExchangeRate(rate); + flow.setBaseAmount(baseAmount); flow.setBizType(CustomerBalanceFlow.BIZ_RECEIPT); flow.setBizId(receipt.getId()); flow.setBizNo(receipt.getReceiptNo()); flow.setDirection(1); - flow.setAmount(amount); + flow.setAmount(originalAmount); flow.setBeforeBalance(beforeBalance); flow.setAfterBalance(afterBalance); flow.setBeforeFrozenBalance(frozenBalance); diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/customer/CustomerMoneyMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/customer/CustomerMoneyMapper.xml index 4645e31b..1041fe83 100644 --- a/bms-back/ruoyi-system/src/main/resources/mapper/customer/CustomerMoneyMapper.xml +++ b/bms-back/ruoyi-system/src/main/resources/mapper/customer/CustomerMoneyMapper.xml @@ -101,6 +101,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + @@ -120,7 +122,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" select r.id, r.receipt_no, r.customer_id, r.account_id, r.receipt_type, r.amount, r.allocated_amount, - r.unallocated_amount, r.currency, r.pay_method, r.bank_transaction_no, r.received_at, r.status, + r.unallocated_amount, r.currency, r.exchange_rate, r.base_amount, r.pay_method, r.bank_transaction_no, + r.received_at, r.status, r.confirmed_at, r.confirmed_by, r.invoice_status, r.version, r.create_by, r.create_time, r.update_by, r.update_time, r.remark, r.del_flag, (select count(1) from bms_customer_attachment a @@ -170,6 +173,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" allocated_amount, unallocated_amount, currency, + exchange_rate, + base_amount, pay_method, bank_transaction_no, received_at, @@ -191,6 +196,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" #{allocatedAmount}, #{unallocatedAmount}, #{currency}, + #{exchangeRate}, + #{baseAmount}, #{payMethod}, #{bankTransactionNo}, #{receivedAt}, @@ -231,6 +238,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + @@ -250,7 +259,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - select id, flow_no, customer_id, account_id, currency, biz_type, biz_id, biz_no, direction, amount, + select id, flow_no, customer_id, account_id, currency, exchange_rate, base_amount, biz_type, biz_id, biz_no, + direction, amount, before_balance, after_balance, before_frozen_balance, after_frozen_balance, operator_id, create_by, create_time, update_by, update_time, remark, del_flag from customer_balance_flow @@ -285,6 +295,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" customer_id, account_id, currency, + exchange_rate, + base_amount, biz_type, biz_id, biz_no, @@ -304,6 +316,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" #{customerId}, #{accountId}, #{currency}, + #{exchangeRate}, + #{baseAmount}, #{bizType}, #{bizId}, #{bizNo}, diff --git a/bms-back/sql/data/customer_receipt_base_currency.sql b/bms-back/sql/data/customer_receipt_base_currency.sql new file mode 100644 index 00000000..899f95ef --- /dev/null +++ b/bms-back/sql/data/customer_receipt_base_currency.sql @@ -0,0 +1,20 @@ +-- ---------------------------- +-- 收款/流水 增加「对CNY汇率」「本位币金额(CNY)」 +-- 统一本位币核算:原币登记,审核后一律折算为 CNY 入客户本位币账户 +-- 汇率定义:1 单位原币 = exchange_rate 单位 CNY(CNY 本币汇率固定为 1) +-- ---------------------------- + +ALTER TABLE `customer_receipt` + ADD COLUMN `exchange_rate` decimal(18,8) DEFAULT 1.00000000 COMMENT '对CNY汇率(1原币=?CNY,CNY为1)' AFTER `currency`, + ADD COLUMN `base_amount` decimal(18,2) DEFAULT 0.00 COMMENT '本位币金额(CNY,原币金额×汇率)' AFTER `exchange_rate`; + +-- 历史数据(均为CNY收款):汇率1,本位币金额=原金额 +UPDATE `customer_receipt` SET `exchange_rate` = 1, `base_amount` = `amount` +WHERE `exchange_rate` = 1 AND (`base_amount` = 0 OR `base_amount` IS NULL); + +ALTER TABLE `customer_balance_flow` + ADD COLUMN `exchange_rate` decimal(18,8) DEFAULT 1.00000000 COMMENT '对CNY汇率(1原币=?CNY,CNY为1)' AFTER `currency`, + ADD COLUMN `base_amount` decimal(18,2) DEFAULT 0.00 COMMENT '本位币金额(CNY,变动额折算)' AFTER `exchange_rate`; + +UPDATE `customer_balance_flow` SET `exchange_rate` = 1, `base_amount` = `amount` +WHERE `exchange_rate` = 1 AND (`base_amount` = 0 OR `base_amount` IS NULL); diff --git a/bms-front/src/views/customersuppliermanagement/customerinfo/customermoney/index.vue b/bms-front/src/views/customersuppliermanagement/customerinfo/customermoney/index.vue index d5f700e6..d6d7e799 100644 --- a/bms-front/src/views/customersuppliermanagement/customerinfo/customermoney/index.vue +++ b/bms-front/src/views/customersuppliermanagement/customerinfo/customermoney/index.vue @@ -10,10 +10,7 @@
客户编码:{{ customer.customerCode || route.query.customerCode || '-' }}
- 币种 - - - + 本位币账户 CNY
@@ -22,14 +19,8 @@ -
账户余额
+
账户余额(CNY)
{{ formatMoney(currentAccount.balance) }}
-
- ≈ CNY {{ baseAmountText }} - - - -
@@ -107,12 +98,20 @@ {{ formatOption(receiptTypeOptions, scope.row.receiptType) }} - + - + + + + + + + @@ -195,16 +194,23 @@ 支出 - + + + + + + + - @@ -238,8 +244,8 @@ - - + + @@ -252,13 +258,33 @@ - - - - + + + + + +
1 {{ receiptDialog.form.currency || 'CNY' }} = {{ formatRate(receiptDialog.form.exchangeRate) }} CNY,默认取汇率表,可手工修改
+
+
+ + + + + + + @@ -327,10 +353,14 @@ {{ formatOption(receiptTypeOptions, viewDialog.data.receiptType) }} - + {{ viewDialog.data.currency }} {{ formatMoney(viewDialog.data.amount) }} - {{ formatMoney(viewDialog.data.unallocatedAmount) }} + {{ formatRate(viewDialog.data.exchangeRate) }} + + CNY {{ formatMoney(viewDialog.data.baseAmount) }} + + CNY {{ formatMoney(viewDialog.data.unallocatedAmount) }} {{ formatOption(payMethodOptions, viewDialog.data.payMethod) }} {{ viewDialog.data.bankTransactionNo || '-' }} {{ formatDateTime(viewDialog.data.confirmedAt) }} @@ -413,6 +443,15 @@ const bizTypeOptions = [ { value: 'FREEZE', label: '冻结' }, { value: 'UNFREEZE', label: '解冻' } ] +/** 登记收款可选币种(汇率需在 数据管理-汇率管理 维护对CNY汇率) */ +const currencyOptions = [ + { value: 'CNY', label: '人民币 CNY' }, + { value: 'USD', label: '美元 USD' }, + { value: 'EUR', label: '欧元 EUR' }, + { value: 'GBP', label: '英镑 GBP' }, + { value: 'HKD', label: '港币 HKD' }, + { value: 'JPY', label: '日元 JPY' } +] @@ -434,17 +473,22 @@ function formatMoney(val) { const num = Number(val) return isNaN(num) ? '0.00' : num.toFixed(2) } +/** 汇率展示:去尾零,最多8位小数(7.10000000 → 7.1) */ +function formatRate(val) { + if (val === null || val === undefined || val === '') return '1' + const num = Number(val) + return isNaN(num) ? String(val) : String(num) +} function formatDateTime(val) { if (!val) return '-' return String(val).length > 16 ? String(val).substring(0, 16) : val } -/* ================= 客户信息 & 账户 ================= */ +/* ================= 客户信息 & 账户(统一本位币 CNY) ================= */ const accountList = ref([]) -const currentCurrency = ref('CNY') const currentAccount = computed(() => { - return accountList.value.find(a => a.currency === currentCurrency.value) || {} + return accountList.value.find(a => a.currency === 'CNY') || {} }) /** 可用额度 = 总额度 + 额外额度 - 已用额度 */ @@ -469,46 +513,9 @@ function loadAccounts() { if (!customerId) return listAccount(customerId).then(res => { accountList.value = res.data || [] - if (accountList.value.length > 0) { - const exists = accountList.value.some(a => a.currency === currentCurrency.value) - if (!exists) currentCurrency.value = accountList.value[0].currency - } - loadBaseAmounts() }) } -/* 外币账户按即期汇率折算本位币(CNY) */ -const baseAmountMap = ref({}) -const baseRateMap = ref({}) - -function loadBaseAmounts() { - baseAmountMap.value = {} - baseRateMap.value = {} - accountList.value - .filter(a => a.currency && a.currency !== 'CNY' && Number(a.balance) !== 0) - .forEach(a => { - convertToBase({ amount: a.balance, currency: a.currency, rateType: '0' }) - .then(res => { - baseAmountMap.value[a.currency] = res.data.result - baseRateMap.value[a.currency] = res.data.rate - }) - .catch(() => { - // 未维护汇率时不阻塞页面,折算行不显示 - }) - }) -} - -const baseAmountText = computed(() => { - const v = baseAmountMap.value[currentCurrency.value] - return v === undefined || v === null ? '-' : formatMoney(v) -}) -const baseRateText = computed(() => { - const r = baseRateMap.value[currentCurrency.value] - return r === undefined || r === null - ? '未维护对CNY汇率' - : `按即期汇率折算:1 ${currentCurrency.value} = ${r} CNY` -}) - function goBack() { router.push({ path: '/customersuppliermanagement/customerinfo' }) } @@ -582,12 +589,14 @@ const receiptRules = { } function resetReceiptForm() { + rateEdited.value = false receiptDialog.form = { customerId: route.query.customerId || null, receivedAt: proxy.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}'), amount: '', receiptType: 'PREPAY', currency: 'CNY', + exchangeRate: 1, payMethod: 'BANK_TRANSFER', bankTransactionNo: '', remark: '', @@ -601,9 +610,65 @@ function handleAddReceipt() { receiptDialog.visible = true } +/** 汇率是否被手工修改过(修改后切换币种/日期不再自动覆盖) */ +const rateEdited = ref(false) + +/** 按收款日期从汇率表取该币种对CNY即期汇率 */ +function fetchDefaultRate() { + const cur = receiptDialog.form.currency + if (!cur || cur === 'CNY') { + receiptDialog.form.exchangeRate = 1 + return + } + const bizDate = receiptDialog.form.receivedAt + ? String(receiptDialog.form.receivedAt).substring(0, 10) + : proxy.parseTime(new Date(), '{y}-{m}-{d}') + convertToBase({ amount: 1, currency: cur, rateType: '0', bizDate }) + .then(res => { + if (!rateEdited.value) receiptDialog.form.exchangeRate = Number(res.data.rate) + }) + .catch(() => { + if (!rateEdited.value) { + receiptDialog.form.exchangeRate = undefined + proxy.$modal.msgWarning(`汇率表中未维护 ${cur} 对 CNY 的汇率,请手工录入汇率`) + } + }) +} + +/** 切换币种:重置手工标记并重新取默认汇率 */ +function handleCurrencyChange() { + rateEdited.value = false + fetchDefaultRate() +} + +/** 手工修改汇率 */ +function handleRateInput() { + if (receiptDialog.form.currency !== 'CNY') rateEdited.value = true +} + +/** 本位币金额预览 = 原币金额 × 汇率 */ +const baseAmountPreview = computed(() => { + const amount = Number(receiptDialog.form.amount) || 0 + const rate = Number(receiptDialog.form.exchangeRate) || 0 + return formatMoney(amount * rate) +}) + +// 收款日期变化且汇率未手改时,按新日期重新取汇率表 +watch(() => receiptDialog.form.receivedAt, () => { + if (receiptDialog.visible && receiptDialog.form.currency !== 'CNY' && !rateEdited.value) { + fetchDefaultRate() + } +}) + function submitReceipt() { receiptFormRef.value.validate(valid => { if (!valid) return + const cur = receiptDialog.form.currency + const rate = Number(receiptDialog.form.exchangeRate) + if (cur !== 'CNY' && (!rate || rate <= 0)) { + proxy.$modal.msgError(`请录入 ${cur} 对 CNY 的汇率(汇率表未配置时可手工填写)`) + return + } submitLoading.value = true addReceipt(receiptDialog.form).then(() => { proxy.$modal.msgSuccess("登记成功,等待审核") @@ -769,7 +834,6 @@ function initPage() { // 重置页面状态,避免上一个客户的数据残留 customer.value = {} accountList.value = [] - currentCurrency.value = 'CNY' activeTab.value = 'receipt' receiptDialog.visible = false viewDialog.visible = false @@ -855,18 +919,11 @@ watch(() => route.query.customerId, (newId, oldId) => { .balance-value.success { color: var(--el-color-success); } .balance-value.warning { color: var(--el-color-warning); } .balance-value.danger { color: var(--el-color-danger); } -.balance-base { - margin-top: 6px; +.rate-tip { + margin-top: 4px; font-size: 12px; color: var(--el-text-color-secondary); - display: flex; - align-items: center; - justify-content: center; - gap: 4px; -} -.balance-base-icon { - cursor: help; - font-size: 13px; + line-height: 1.4; } .search-form { margin-bottom: -4px; diff --git a/bms-front/src/views/customersuppliermanagement/customerinfo/index.vue b/bms-front/src/views/customersuppliermanagement/customerinfo/index.vue index 06a36769..c74857c1 100644 --- a/bms-front/src/views/customersuppliermanagement/customerinfo/index.vue +++ b/bms-front/src/views/customersuppliermanagement/customerinfo/index.vue @@ -185,12 +185,9 @@ - + - - - @@ -391,13 +388,6 @@ - - - - - - -