Merge branch 'master' of http://git.lionquexpress.com/wuweihua/BMS
This commit is contained in:
+7
-1
@@ -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;
|
||||
|
||||
|
||||
+7
-1
@@ -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;
|
||||
|
||||
|
||||
+62
-12
@@ -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<CustomerAccount> 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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
+131
@@ -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;
|
||||
}
|
||||
}
|
||||
+45
@@ -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<BmsExchangeRate> 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);
|
||||
}
|
||||
+48
@@ -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<BmsExchangeRate> 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);
|
||||
}
|
||||
+271
@@ -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<BmsExchangeRate> 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 "即期汇率";
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user