diff --git a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/data/BmsExchangeRateController.java b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/data/BmsExchangeRateController.java new file mode 100644 index 00000000..e69be952 --- /dev/null +++ b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/data/BmsExchangeRateController.java @@ -0,0 +1,163 @@ +package com.ruoyi.web.controller.data; + +import java.math.BigDecimal; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.system.domain.BmsExchangeRate; +import com.ruoyi.system.domain.BmsExchangeRateConvertResult; +import com.ruoyi.system.service.IBmsExchangeRateService; + +/** + * 汇率管理 信息操作处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/data/exchangerate") +public class BmsExchangeRateController extends BaseController +{ + @Autowired + private IBmsExchangeRateService exchangeRateService; + + /** + * 获取汇率列表 + */ + @PreAuthorize("@ss.hasPermi('data:exchangerate:list')") + @GetMapping("/list") + public TableDataInfo list(BmsExchangeRate exchangeRate) + { + startPage(); + List list = exchangeRateService.selectExchangeRateList(exchangeRate); + return getDataTable(list); + } + + /** + * 导出汇率列表 + */ + @PreAuthorize("@ss.hasPermi('data:exchangerate:export')") + @Log(title = "汇率管理", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, BmsExchangeRate exchangeRate) + { + List list = exchangeRateService.selectExchangeRateList(exchangeRate); + ExcelUtil util = new ExcelUtil(BmsExchangeRate.class); + util.exportExcel(response, list, "汇率数据"); + } + + /** + * 获取汇率详细信息 + */ + @PreAuthorize("@ss.hasPermi('data:exchangerate:query')") + @GetMapping(value = "/{rateId}") + public AjaxResult getInfo(@PathVariable Long rateId) + { + return success(exchangeRateService.selectExchangeRateById(rateId)); + } + + /** + * 新增汇率 + */ + @PreAuthorize("@ss.hasPermi('data:exchangerate:add')") + @Log(title = "汇率管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody BmsExchangeRate exchangeRate) + { + exchangeRate.setCreateBy(getUsername()); + return toAjax(exchangeRateService.insertExchangeRate(exchangeRate)); + } + + /** + * 修改汇率 + */ + @PreAuthorize("@ss.hasPermi('data:exchangerate:edit')") + @Log(title = "汇率管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody BmsExchangeRate exchangeRate) + { + return toAjax(exchangeRateService.updateExchangeRate(exchangeRate)); + } + + /** + * 删除汇率 + */ + @PreAuthorize("@ss.hasPermi('data:exchangerate:remove')") + @Log(title = "汇率管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{rateIds}") + public AjaxResult remove(@PathVariable Long[] rateIds) + { + return toAjax(exchangeRateService.deleteExchangeRateByIds(rateIds)); + } + + /** + * 汇率换算(业务公共接口,登录即可调用,不单独设权限) + * + * @param amount 源金额 + * @param fromCurrency 源币种 + * @param toCurrency 目标币种 + * @param rateType 汇率类型(0即期 1月初 2月末 3固定),默认即期 + * @param bizDate 业务日期 yyyy-MM-dd,默认当天 + */ + @GetMapping("/convert") + public AjaxResult convert(@RequestParam BigDecimal amount, + @RequestParam String fromCurrency, + @RequestParam String toCurrency, + @RequestParam(required = false) String rateType, + @RequestParam(required = false) String bizDate) + { + Date date = parseDate(bizDate); + BmsExchangeRateConvertResult result = exchangeRateService.convert( + amount, fromCurrency, toCurrency, rateType, date); + return success(result); + } + + /** + * 本位币(CNY)折算:任意币种金额折算为人民币 + */ + @GetMapping("/toBase") + public AjaxResult toBase(@RequestParam BigDecimal amount, + @RequestParam String currency, + @RequestParam(required = false) String rateType, + @RequestParam(required = false) String bizDate) + { + Date date = parseDate(bizDate); + BmsExchangeRateConvertResult result = exchangeRateService.convert( + amount, currency, BmsExchangeRate.BASE_CURRENCY, rateType, date); + return success(result); + } + + private Date parseDate(String bizDate) + { + if (bizDate == null || bizDate.isEmpty()) + { + return null; + } + try + { + return new SimpleDateFormat("yyyy-MM-dd").parse(bizDate); + } + catch (Exception e) + { + return null; + } + } +} 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/java/com/ruoyi/system/domain/BmsExchangeRate.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/system/domain/BmsExchangeRate.java new file mode 100644 index 00000000..cfa4ee5d --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/system/domain/BmsExchangeRate.java @@ -0,0 +1,178 @@ +package com.ruoyi.system.domain; + +import java.math.BigDecimal; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 汇率管理 sys_exchange_rate + * + * 汇率定义:1 单位源币种(from_currency) = exchange_rate 单位目标币种(to_currency) + * + * @author ruoyi + */ +public class BmsExchangeRate extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 系统本位币 */ + public static final String BASE_CURRENCY = "CNY"; + + /** 汇率类型:即期汇率 */ + public static final String RATE_TYPE_SPOT = "0"; + /** 汇率类型:月初汇率 */ + public static final String RATE_TYPE_MONTH_BEGIN = "1"; + /** 汇率类型:月末汇率 */ + public static final String RATE_TYPE_MONTH_END = "2"; + /** 汇率类型:固定汇率 */ + public static final String RATE_TYPE_FIXED = "3"; + + /** 汇率ID */ + private Long rateId; + + /** 源币种 */ + @Excel(name = "源币种") + private String fromCurrency; + + /** 目标币种 */ + @Excel(name = "目标币种") + private String toCurrency; + + /** 汇率类型(0即期 1月初 2月末 3固定) */ + @Excel(name = "汇率类型", readConverterExp = "0=即期汇率,1=月初汇率,2=月末汇率,3=固定汇率") + private String rateType; + + /** 汇率值(1源币种=?目标币种) */ + @Excel(name = "汇率值") + private BigDecimal exchangeRate; + + /** 生效日期 */ + @JsonFormat(pattern = "yyyy-MM-dd") + @Excel(name = "生效日期", width = 30, dateFormat = "yyyy-MM-dd") + private Date effectiveDate; + + /** 失效日期 */ + @JsonFormat(pattern = "yyyy-MM-dd") + @Excel(name = "失效日期", width = 30, dateFormat = "yyyy-MM-dd") + private Date expireDate; + + /** 汇率来源 */ + @Excel(name = "汇率来源") + private String source; + + /** 删除标记(0存在 1删除) */ + private String delFlag; + + public Long getRateId() + { + return rateId; + } + + public void setRateId(Long rateId) + { + this.rateId = rateId; + } + + public String getFromCurrency() + { + return fromCurrency; + } + + public void setFromCurrency(String fromCurrency) + { + this.fromCurrency = fromCurrency; + } + + public String getToCurrency() + { + return toCurrency; + } + + public void setToCurrency(String toCurrency) + { + this.toCurrency = toCurrency; + } + + public String getRateType() + { + return rateType; + } + + public void setRateType(String rateType) + { + this.rateType = rateType; + } + + public BigDecimal getExchangeRate() + { + return exchangeRate; + } + + public void setExchangeRate(BigDecimal exchangeRate) + { + this.exchangeRate = exchangeRate; + } + + public Date getEffectiveDate() + { + return effectiveDate; + } + + public void setEffectiveDate(Date effectiveDate) + { + this.effectiveDate = effectiveDate; + } + + public Date getExpireDate() + { + return expireDate; + } + + public void setExpireDate(Date expireDate) + { + this.expireDate = expireDate; + } + + public String getSource() + { + return source; + } + + public void setSource(String source) + { + this.source = source; + } + + public String getDelFlag() + { + return delFlag; + } + + public void setDelFlag(String delFlag) + { + this.delFlag = delFlag; + } + + @Override + public String toString() + { + return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE) + .append("rateId", getRateId()) + .append("fromCurrency", getFromCurrency()) + .append("toCurrency", getToCurrency()) + .append("rateType", getRateType()) + .append("exchangeRate", getExchangeRate()) + .append("effectiveDate", getEffectiveDate()) + .append("expireDate", getExpireDate()) + .append("source", getSource()) + .append("remark", getRemark()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("delFlag", getDelFlag()) + .toString(); + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/system/domain/BmsExchangeRateConvertResult.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/system/domain/BmsExchangeRateConvertResult.java new file mode 100644 index 00000000..561dfbf6 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/system/domain/BmsExchangeRateConvertResult.java @@ -0,0 +1,131 @@ +package com.ruoyi.system.domain; + +import java.io.Serializable; +import java.math.BigDecimal; + +/** + * 汇率换算结果 + * + * @author ruoyi + */ +public class BmsExchangeRateConvertResult implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 源币种 */ + private String fromCurrency; + + /** 目标币种 */ + private String toCurrency; + + /** 汇率类型(0即期 1月初 2月末 3固定) */ + private String rateType; + + /** 业务日期 */ + private String bizDate; + + /** 使用的汇率值(1源币种=?目标币种) */ + private BigDecimal rate; + + /** 源金额 */ + private BigDecimal amount; + + /** 换算结果 */ + private BigDecimal result; + + /** 汇率取数路径:DIRECT 正向配置 / REVERSE 反向取倒数 / CROSS 经本位币交叉换算 / SAME 同币种 */ + private String ratePath; + + /** 汇率来源 */ + private String source; + + public String getFromCurrency() + { + return fromCurrency; + } + + public void setFromCurrency(String fromCurrency) + { + this.fromCurrency = fromCurrency; + } + + public String getToCurrency() + { + return toCurrency; + } + + public void setToCurrency(String toCurrency) + { + this.toCurrency = toCurrency; + } + + public String getRateType() + { + return rateType; + } + + public void setRateType(String rateType) + { + this.rateType = rateType; + } + + public String getBizDate() + { + return bizDate; + } + + public void setBizDate(String bizDate) + { + this.bizDate = bizDate; + } + + public BigDecimal getRate() + { + return rate; + } + + public void setRate(BigDecimal rate) + { + this.rate = rate; + } + + public BigDecimal getAmount() + { + return amount; + } + + public void setAmount(BigDecimal amount) + { + this.amount = amount; + } + + public BigDecimal getResult() + { + return result; + } + + public void setResult(BigDecimal result) + { + this.result = result; + } + + public String getRatePath() + { + return ratePath; + } + + public void setRatePath(String ratePath) + { + this.ratePath = ratePath; + } + + public String getSource() + { + return source; + } + + public void setSource(String source) + { + this.source = source; + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/system/mapper/BmsExchangeRateMapper.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/system/mapper/BmsExchangeRateMapper.java new file mode 100644 index 00000000..43f5bc47 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/system/mapper/BmsExchangeRateMapper.java @@ -0,0 +1,45 @@ +package com.ruoyi.system.mapper; + +import java.util.Date; +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.ruoyi.system.domain.BmsExchangeRate; + +/** + * 汇率管理 Mapper + * + * @author ruoyi + */ +public interface BmsExchangeRateMapper +{ + /** 查询汇率列表 */ + public List selectExchangeRateList(BmsExchangeRate exchangeRate); + + /** 查询汇率详情 */ + public BmsExchangeRate selectExchangeRateById(Long rateId); + + /** 新增汇率 */ + public int insertExchangeRate(BmsExchangeRate exchangeRate); + + /** 修改汇率 */ + public int updateExchangeRate(BmsExchangeRate exchangeRate); + + /** 软删除汇率 */ + public int deleteExchangeRateByIds(Long[] rateIds); + + /** + * 查询某业务日期生效的汇率(取生效日期最新的一条) + * 只做精确正向匹配(from -> to),反向/交叉由 Service 推导 + */ + public BmsExchangeRate selectEffectiveRate(@Param("fromCurrency") String fromCurrency, + @Param("toCurrency") String toCurrency, + @Param("rateType") String rateType, + @Param("bizDate") Date bizDate); + + /** 校验同方向/类型/生效日期是否已存在(编辑时排除自身) */ + public int countDuplicate(@Param("fromCurrency") String fromCurrency, + @Param("toCurrency") String toCurrency, + @Param("rateType") String rateType, + @Param("effectiveDate") Date effectiveDate, + @Param("excludeId") Long excludeId); +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/system/service/IBmsExchangeRateService.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/system/service/IBmsExchangeRateService.java new file mode 100644 index 00000000..ba721653 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/system/service/IBmsExchangeRateService.java @@ -0,0 +1,48 @@ +package com.ruoyi.system.service; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; +import com.ruoyi.system.domain.BmsExchangeRate; +import com.ruoyi.system.domain.BmsExchangeRateConvertResult; + +/** + * 汇率管理 Service + * + * @author ruoyi + */ +public interface IBmsExchangeRateService +{ + /** 查询汇率列表 */ + public List selectExchangeRateList(BmsExchangeRate exchangeRate); + + /** 查询汇率详情 */ + public BmsExchangeRate selectExchangeRateById(Long rateId); + + /** 新增汇率 */ + public int insertExchangeRate(BmsExchangeRate exchangeRate); + + /** 修改汇率 */ + public int updateExchangeRate(BmsExchangeRate exchangeRate); + + /** 软删除汇率 */ + public int deleteExchangeRateByIds(Long[] rateIds); + + /** + * 取换算汇率:1 单位 fromCurrency = ? 单位 toCurrency + * 取数顺序:正向配置 → 反向配置取倒数 → 经本位币(CNY)交叉换算 + * + * @param fromCurrency 源币种 + * @param toCurrency 目标币种 + * @param rateType 汇率类型(0即期 1月初 2月末 3固定),为空按即期 + * @param bizDate 业务日期,为空取当天 + * @return 汇率(不为 null;无配置时抛 ServiceException) + */ + public BmsExchangeRate resolveRate(String fromCurrency, String toCurrency, String rateType, Date bizDate); + + /** + * 金额换算 + */ + public BmsExchangeRateConvertResult convert(BigDecimal amount, String fromCurrency, String toCurrency, + String rateType, Date bizDate); +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/BmsExchangeRateServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/BmsExchangeRateServiceImpl.java new file mode 100644 index 00000000..1fd4ae8a --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/BmsExchangeRateServiceImpl.java @@ -0,0 +1,271 @@ +package com.ruoyi.system.service.impl; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Date; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.domain.BmsExchangeRate; +import com.ruoyi.system.domain.BmsExchangeRateConvertResult; +import com.ruoyi.system.mapper.BmsExchangeRateMapper; +import com.ruoyi.system.service.IBmsExchangeRateService; + +/** + * 汇率管理 Service实现 + * + * @author ruoyi + */ +@Service +public class BmsExchangeRateServiceImpl implements IBmsExchangeRateService +{ + /** 汇率/换算结果统一精度(8位小数) */ + private static final int SCALE = 8; + + @Autowired + private BmsExchangeRateMapper exchangeRateMapper; + + @Override + public List selectExchangeRateList(BmsExchangeRate exchangeRate) + { + return exchangeRateMapper.selectExchangeRateList(exchangeRate); + } + + @Override + public BmsExchangeRate selectExchangeRateById(Long rateId) + { + return exchangeRateMapper.selectExchangeRateById(rateId); + } + + @Override + public int insertExchangeRate(BmsExchangeRate exchangeRate) + { + normalize(exchangeRate); + validate(exchangeRate); + checkDuplicate(exchangeRate); + exchangeRate.setCreateBy(StringUtils.isNotEmpty(exchangeRate.getCreateBy()) + ? exchangeRate.getCreateBy() : ""); + return exchangeRateMapper.insertExchangeRate(exchangeRate); + } + + @Override + public int updateExchangeRate(BmsExchangeRate exchangeRate) + { + if (exchangeRate.getRateId() == null) + { + throw new ServiceException("汇率ID不能为空"); + } + normalize(exchangeRate); + validate(exchangeRate); + checkDuplicate(exchangeRate); + return exchangeRateMapper.updateExchangeRate(exchangeRate); + } + + @Override + public int deleteExchangeRateByIds(Long[] rateIds) + { + return exchangeRateMapper.deleteExchangeRateByIds(rateIds); + } + + /* ============================ 汇率换算引擎 ============================ */ + + @Override + public BmsExchangeRate resolveRate(String fromCurrency, String toCurrency, String rateType, Date bizDate) + { + String from = normalizeCurrency(fromCurrency); + String to = normalizeCurrency(toCurrency); + String type = StringUtils.isNotEmpty(rateType) ? rateType : BmsExchangeRate.RATE_TYPE_SPOT; + Date date = bizDate != null ? bizDate : DateUtils.getNowDate(); + + // 同币种 + if (from.equals(to)) + { + return buildSyntheticRate(from, to, type, BigDecimal.ONE, date, "SAME"); + } + + // 1. 正向配置:from -> to + BmsExchangeRate direct = exchangeRateMapper.selectEffectiveRate(from, to, type, date); + if (direct != null) + { + return buildResult(direct, direct.getExchangeRate(), "DIRECT"); + } + + // 2. 反向配置:to -> from,取倒数 + BmsExchangeRate reverse = exchangeRateMapper.selectEffectiveRate(to, from, type, date); + if (reverse != null && positive(reverse.getExchangeRate())) + { + return buildResult(reverse, + BigDecimal.ONE.divide(reverse.getExchangeRate(), SCALE, RoundingMode.HALF_UP), + "REVERSE"); + } + + // 3. 经本位币 CNY 交叉换算:from -> CNY -> to + BigDecimal toBase = lookupChainRate(from, BmsExchangeRate.BASE_CURRENCY, type, date); + BigDecimal fromBase = lookupChainRate(BmsExchangeRate.BASE_CURRENCY, to, type, date); + if (toBase != null && fromBase != null) + { + BigDecimal crossRate = toBase.multiply(fromBase).setScale(SCALE, RoundingMode.HALF_UP); + BmsExchangeRate synth = buildSyntheticRate(from, to, type, crossRate, date, "CROSS"); + synth.setSource("经CNY交叉换算"); + return synth; + } + + throw new ServiceException("未维护生效日期内【" + from + " → " + to + "】的" + + rateTypeName(type) + ",请先在「数据管理-汇率管理」中配置"); + } + + @Override + public BmsExchangeRateConvertResult convert(BigDecimal amount, String fromCurrency, String toCurrency, + String rateType, Date bizDate) + { + if (amount == null) + { + throw new ServiceException("换算金额不能为空"); + } + BmsExchangeRate rate = resolveRate(fromCurrency, toCurrency, rateType, bizDate); + BmsExchangeRateConvertResult result = new BmsExchangeRateConvertResult(); + result.setFromCurrency(rate.getFromCurrency()); + result.setToCurrency(rate.getToCurrency()); + result.setRateType(rate.getRateType()); + result.setBizDate(DateUtils.parseDateToStr("yyyy-MM-dd", + bizDate != null ? bizDate : DateUtils.getNowDate())); + result.setRate(rate.getExchangeRate()); + result.setAmount(amount); + result.setResult(amount.multiply(rate.getExchangeRate()).setScale(SCALE, RoundingMode.HALF_UP)); + result.setSource(rate.getSource()); + result.setRatePath(rate.getRemark() == null ? null : (rate.getRemark().startsWith("__RATE_PATH__") + ? rate.getRemark().substring("__RATE_PATH__".length()) : "DIRECT")); + if (result.getRatePath() == null) + { + result.setRatePath("DIRECT"); + } + return result; + } + + /* ============================ 私有方法 ============================ */ + + /** + * 查找单跳汇率(先正向,再反向取倒数),查不到返回 null + */ + private BigDecimal lookupChainRate(String from, String to, String type, Date date) + { + BmsExchangeRate direct = exchangeRateMapper.selectEffectiveRate(from, to, type, date); + if (direct != null) + { + return direct.getExchangeRate(); + } + BmsExchangeRate reverse = exchangeRateMapper.selectEffectiveRate(to, from, type, date); + if (reverse != null && positive(reverse.getExchangeRate())) + { + return BigDecimal.ONE.divide(reverse.getExchangeRate(), SCALE, RoundingMode.HALF_UP); + } + return null; + } + + /** 基于真实记录构造返回(保留来源、日期等) */ + private BmsExchangeRate buildResult(BmsExchangeRate origin, BigDecimal rate, String path) + { + BmsExchangeRate r = new BmsExchangeRate(); + r.setRateId(origin.getRateId()); + r.setFromCurrency(origin.getFromCurrency()); + r.setToCurrency(origin.getToCurrency()); + r.setRateType(origin.getRateType()); + r.setExchangeRate(rate); + r.setEffectiveDate(origin.getEffectiveDate()); + r.setExpireDate(origin.getExpireDate()); + r.setSource(origin.getSource()); + r.setRemark("__RATE_PATH__" + path); + return r; + } + + /** 构造合成汇率(同币种/交叉换算用,无主表ID) */ + private BmsExchangeRate buildSyntheticRate(String from, String to, String type, BigDecimal rate, + Date date, String path) + { + BmsExchangeRate r = new BmsExchangeRate(); + r.setFromCurrency(from); + r.setToCurrency(to); + r.setRateType(type); + r.setExchangeRate(rate); + r.setEffectiveDate(date); + r.setSource(path.equals("SAME") ? "系统默认" : "交叉换算"); + r.setRemark("__RATE_PATH__" + path); + return r; + } + + private void normalize(BmsExchangeRate rate) + { + rate.setFromCurrency(normalizeCurrency(rate.getFromCurrency())); + rate.setToCurrency(normalizeCurrency(rate.getToCurrency())); + if (StringUtils.isEmpty(rate.getRateType())) + { + rate.setRateType(BmsExchangeRate.RATE_TYPE_SPOT); + } + if (StringUtils.isEmpty(rate.getSource())) + { + rate.setSource("手工录入"); + } + } + + private void validate(BmsExchangeRate rate) + { + if (StringUtils.isEmpty(rate.getFromCurrency()) || StringUtils.isEmpty(rate.getToCurrency())) + { + throw new ServiceException("源币种和目标币种不能为空"); + } + if (rate.getFromCurrency().equals(rate.getToCurrency())) + { + throw new ServiceException("源币种与目标币种不能相同"); + } + if (rate.getExchangeRate() == null || !positive(rate.getExchangeRate())) + { + throw new ServiceException("汇率值必须大于0"); + } + if (rate.getEffectiveDate() == null) + { + throw new ServiceException("生效日期不能为空"); + } + } + + private void checkDuplicate(BmsExchangeRate rate) + { + int count = exchangeRateMapper.countDuplicate(rate.getFromCurrency(), rate.getToCurrency(), + rate.getRateType(), rate.getEffectiveDate(), rate.getRateId()); + if (count > 0) + { + throw new ServiceException("已存在相同币种方向、类型、生效日期(" + + rate.getFromCurrency() + " → " + rate.getToCurrency() + " / " + + rateTypeName(rate.getRateType()) + " / " + + DateUtils.parseDateToStr("yyyy-MM-dd", rate.getEffectiveDate()) + ")的汇率配置"); + } + } + + private String normalizeCurrency(String currency) + { + if (StringUtils.isEmpty(currency)) + { + throw new ServiceException("币种不能为空"); + } + return currency.trim().toUpperCase(); + } + + private boolean positive(BigDecimal value) + { + return value != null && value.compareTo(BigDecimal.ZERO) > 0; + } + + private String rateTypeName(String type) + { + switch (type) + { + case BmsExchangeRate.RATE_TYPE_MONTH_BEGIN: return "月初汇率"; + case BmsExchangeRate.RATE_TYPE_MONTH_END: return "月末汇率"; + case BmsExchangeRate.RATE_TYPE_FIXED: return "固定汇率"; + case BmsExchangeRate.RATE_TYPE_SPOT: + default: return "即期汇率"; + } + } +} diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/customer/BmsCustomerMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/customer/BmsCustomerMapper.xml index 669cb861..f4d4c91a 100644 --- a/bms-back/ruoyi-system/src/main/resources/mapper/customer/BmsCustomerMapper.xml +++ b/bms-back/ruoyi-system/src/main/resources/mapper/customer/BmsCustomerMapper.xml @@ -37,6 +37,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + select c.customer_id, c.customer_code, c.customer_name, c.customer_code_dw, c.credit_code, c.customer_type, c.is_enable, c.sales_user_id, c.sales_user, c.ed, c.zq, c.settlement_user_id, c.settlement_user, @@ -45,7 +46,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" where ca.customer_id = c.customer_id and ca.currency = 'CNY' and ca.del_flag = '0') as account_balance, (select coalesce(sum(ca.balance), 0) from customer_account ca where ca.customer_id = c.customer_id and ca.currency != 'CNY' and ca.del_flag = '0') as foreign_currency_balance, - c.weight_threshold, c.throw_ratio, c.credit_ed, c.product_ids, c.remark, + c.weight_threshold, c.throw_ratio, + + (select coalesce(sum(ca.total_credit+ca.extra_credit-ca.used_credit), 0) from customer_account ca + where ca.customer_id = c.customer_id and ca.del_flag = '0') as credit_ed, + + c.product_ids, c.remark, c.create_by, c.create_time, c.update_by, c.update_time, c.del_flag from bms_customer c 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/ruoyi-system/src/main/resources/mapper/data/BmsExchangeRateMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/data/BmsExchangeRateMapper.xml new file mode 100644 index 00000000..1063087b --- /dev/null +++ b/bms-back/ruoyi-system/src/main/resources/mapper/data/BmsExchangeRateMapper.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + select rate_id, from_currency, to_currency, rate_type, exchange_rate, effective_date, expire_date, + source, remark, create_by, create_time, del_flag + from sys_exchange_rate + + + + + + + + + + + + insert into sys_exchange_rate ( + from_currency, to_currency, rate_type, exchange_rate, effective_date, expire_date, + source, remark, create_by, create_time, del_flag + ) values ( + #{fromCurrency}, #{toCurrency}, #{rateType}, #{exchangeRate}, #{effectiveDate}, #{expireDate}, + #{source}, #{remark}, #{createBy}, sysdate(), '0' + ) + + + + update sys_exchange_rate + + from_currency = #{fromCurrency}, + to_currency = #{toCurrency}, + rate_type = #{rateType}, + exchange_rate = #{exchangeRate}, + effective_date = #{effectiveDate}, + expire_date = #{expireDate}, + source = #{source}, + remark = #{remark}, + + where rate_id = #{rateId} + + + + update sys_exchange_rate set del_flag = '1' where rate_id in + + #{rateId} + + + + 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-back/sql/data/sys_exchange_rate.sql b/bms-back/sql/data/sys_exchange_rate.sql new file mode 100644 index 00000000..b358642b --- /dev/null +++ b/bms-back/sql/data/sys_exchange_rate.sql @@ -0,0 +1,37 @@ +-- ---------------------------- +-- 汇率管理表 sys_exchange_rate +-- 汇率定义:1 单位源币种(from_currency) = exchange_rate 单位目标币种(to_currency) +-- 例:USD -> CNY, exchange_rate = 7.10000000 表示 1 USD = 7.1 CNY +-- ---------------------------- +DROP TABLE IF EXISTS `sys_exchange_rate`; +CREATE TABLE `sys_exchange_rate` ( + `rate_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '汇率ID', + `from_currency` varchar(10) NOT NULL COMMENT '源币种', + `to_currency` varchar(10) NOT NULL COMMENT '目标币种', + `rate_type` char(1) DEFAULT '0' COMMENT '汇率类型(0即期汇率 1月初汇率 2月末汇率 3固定汇率)', + `exchange_rate` decimal(18,8) NOT NULL COMMENT '汇率值(1源币种=?目标币种)', + `effective_date` date NOT NULL COMMENT '生效日期', + `expire_date` date DEFAULT NULL COMMENT '失效日期', + `source` varchar(50) DEFAULT '' COMMENT '汇率来源(中国银行/央行/手工录入)', + `remark` varchar(500) DEFAULT NULL COMMENT '备注', + `create_by` varchar(64) DEFAULT '' COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `del_flag` char(1) DEFAULT '0' COMMENT '删除标记(0存在 1删除)', + PRIMARY KEY (`rate_id`), + KEY `idx_currency_date` (`from_currency`, `to_currency`, `effective_date`), + KEY `idx_rate_type` (`rate_type`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='汇率表'; + +-- ---------------------------- +-- 初始常用汇率(即期,2026-09-14 生效,长期有效;请按实际行情维护) +-- ---------------------------- +INSERT INTO `sys_exchange_rate` +(`from_currency`, `to_currency`, `rate_type`, `exchange_rate`, `effective_date`, `expire_date`, `source`, `remark`, `create_by`, `create_time`) +VALUES +('USD', 'CNY', '0', 7.10000000, '2026-09-14', NULL, '中国银行', '美元兑人民币即期汇率', 'admin', NOW()), +('EUR', 'CNY', '0', 7.85000000, '2026-09-14', NULL, '中国银行', '欧元兑人民币即期汇率', 'admin', NOW()), +('GBP', 'CNY', '0', 9.05000000, '2026-09-14', NULL, '中国银行', '英镑兑人民币即期汇率', 'admin', NOW()), +('HKD', 'CNY', '0', 0.91000000, '2026-09-14', NULL, '中国银行', '港币兑人民币即期汇率', 'admin', NOW()), +('JPY', 'CNY', '0', 0.04800000, '2026-09-14', NULL, '中国银行', '日元兑人民币即期汇率(1日元)', 'admin', NOW()), +('CNY', 'USD', '0', 0.14084507, '2026-09-14', NULL, '手工录入', '人民币兑美元(USD/CNY倒数)', 'admin', NOW()), +('CNY', 'EUR', '0', 0.12738854, '2026-09-14', NULL, '手工录入', '人民币兑欧元(EUR/CNY倒数)', 'admin', NOW()); diff --git a/bms-front/src/api/data/exchangeRate.js b/bms-front/src/api/data/exchangeRate.js new file mode 100644 index 00000000..6fc97e3a --- /dev/null +++ b/bms-front/src/api/data/exchangeRate.js @@ -0,0 +1,67 @@ +import request from '@/utils/request' + +// 查询汇率列表 +export function listExchangeRate(query) { + return request({ + url: '/data/exchangerate/list', + method: 'get', + params: query + }) +} + +// 查询汇率详细 +export function getExchangeRate(rateId) { + return request({ + url: '/data/exchangerate/' + rateId, + method: 'get' + }) +} + +// 新增汇率 +export function addExchangeRate(data) { + return request({ + url: '/data/exchangerate', + method: 'post', + data: data + }) +} + +// 修改汇率 +export function updateExchangeRate(data) { + return request({ + url: '/data/exchangerate', + method: 'put', + data: data + }) +} + +// 删除汇率 +export function delExchangeRate(rateIds) { + return request({ + url: '/data/exchangerate/' + rateIds, + method: 'delete' + }) +} + +/** + * 汇率换算 + * @param {Object} params amount/fromCurrency/toCurrency/rateType/bizDate + */ +export function convertExchangeRate(params) { + return request({ + url: '/data/exchangerate/convert', + method: 'get', + params: params + }) +} + +/** + * 任意币种金额折算本位币(CNY) + */ +export function convertToBase(params) { + return request({ + url: '/data/exchangerate/toBase', + method: 'get', + params: params + }) +} diff --git a/bms-front/src/views/customersuppliermanagement/customerinfo/customermoney/index.vue b/bms-front/src/views/customersuppliermanagement/customerinfo/customermoney/index.vue index 0c8e5831..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,7 +19,7 @@ -
账户余额
+
账户余额(CNY)
{{ formatMoney(currentAccount.balance) }}
@@ -101,12 +98,20 @@ {{ formatOption(receiptTypeOptions, scope.row.receiptType) }} - + - + + + + + + + @@ -189,16 +194,23 @@ 支出 - + + + + + + + - @@ -232,8 +244,8 @@ - - + + @@ -246,13 +258,33 @@ - - - - + + + + + +
1 {{ receiptDialog.form.currency || 'CNY' }} = {{ formatRate(receiptDialog.form.exchangeRate) }} CNY,默认取汇率表,可手工修改
+
+
+ + + + + + + @@ -321,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) }} @@ -368,6 +404,7 @@ import axios from 'axios' import { saveAs } from 'file-saver' import { getCustomer } from "@/api/customersuppliermanagement/customerinfo/customer.js" import { listAccount, listReceipt, getReceipt, addReceipt, confirmReceipt, cancelReceipt, listFlow } from "@/api/customersuppliermanagement/customermoney/customermoney.js" +import { convertToBase } from "@/api/data/exchangeRate" import { getToken } from "@/utils/auth" import { useRoute } from 'vue-router' @@ -406,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' } +] @@ -427,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') || {} }) /** 可用额度 = 总额度 + 额外额度 - 已用额度 */ @@ -462,10 +513,6 @@ 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 - } }) } @@ -542,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: '', @@ -561,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("登记成功,等待审核") @@ -729,7 +834,6 @@ function initPage() { // 重置页面状态,避免上一个客户的数据残留 customer.value = {} accountList.value = [] - currentCurrency.value = 'CNY' activeTab.value = 'receipt' receiptDialog.visible = false viewDialog.visible = false @@ -815,6 +919,12 @@ 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); } +.rate-tip { + margin-top: 4px; + font-size: 12px; + color: var(--el-text-color-secondary); + 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 5474fd43..c74857c1 100644 --- a/bms-front/src/views/customersuppliermanagement/customerinfo/index.vue +++ b/bms-front/src/views/customersuppliermanagement/customerinfo/index.vue @@ -185,12 +185,9 @@ - + - - - @@ -315,6 +312,20 @@ + + + + + + + + + + @@ -364,6 +375,7 @@ + @@ -376,13 +388,6 @@ - - - - - - - diff --git a/bms-front/src/views/data/exchangerate/index.vue b/bms-front/src/views/data/exchangerate/index.vue new file mode 100644 index 00000000..f9396e62 --- /dev/null +++ b/bms-front/src/views/data/exchangerate/index.vue @@ -0,0 +1,430 @@ + + + + +