Merge branch 'master' of http://git.lionquexpress.com/wuweihua/BMS
This commit is contained in:
+163
@@ -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<BmsExchangeRate> 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<BmsExchangeRate> list = exchangeRateService.selectExchangeRateList(exchangeRate);
|
||||
ExcelUtil<BmsExchangeRate> util = new ExcelUtil<BmsExchangeRate>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+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 "即期汇率";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</resultMap>
|
||||
|
||||
<!-- 账户余额/外币余额统一取 customer_account 余额表(CNY 为本币,其余为外币),不取 bms_customer 冗余列 -->
|
||||
<!-- 信用额度=总额度+额外额度-已用额度-->
|
||||
<sql id="selectBmsCustomerVo">
|
||||
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
|
||||
</sql>
|
||||
|
||||
@@ -101,6 +101,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="allocatedAmount" column="allocated_amount" />
|
||||
<result property="unallocatedAmount" column="unallocated_amount" />
|
||||
<result property="currency" column="currency" />
|
||||
<result property="exchangeRate" column="exchange_rate" />
|
||||
<result property="baseAmount" column="base_amount" />
|
||||
<result property="payMethod" column="pay_method" />
|
||||
<result property="bankTransactionNo" column="bank_transaction_no" />
|
||||
<result property="receivedAt" column="received_at" />
|
||||
@@ -120,7 +122,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
|
||||
<sql id="selectCustomerReceiptVo">
|
||||
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"
|
||||
<if test="allocatedAmount != null">allocated_amount,</if>
|
||||
<if test="unallocatedAmount != null">unallocated_amount,</if>
|
||||
<if test="currency != null">currency,</if>
|
||||
<if test="exchangeRate != null">exchange_rate,</if>
|
||||
<if test="baseAmount != null">base_amount,</if>
|
||||
<if test="payMethod != null">pay_method,</if>
|
||||
<if test="bankTransactionNo != null">bank_transaction_no,</if>
|
||||
<if test="receivedAt != null">received_at,</if>
|
||||
@@ -191,6 +196,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="allocatedAmount != null">#{allocatedAmount},</if>
|
||||
<if test="unallocatedAmount != null">#{unallocatedAmount},</if>
|
||||
<if test="currency != null">#{currency},</if>
|
||||
<if test="exchangeRate != null">#{exchangeRate},</if>
|
||||
<if test="baseAmount != null">#{baseAmount},</if>
|
||||
<if test="payMethod != null">#{payMethod},</if>
|
||||
<if test="bankTransactionNo != null">#{bankTransactionNo},</if>
|
||||
<if test="receivedAt != null">#{receivedAt},</if>
|
||||
@@ -231,6 +238,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="customerId" column="customer_id" />
|
||||
<result property="accountId" column="account_id" />
|
||||
<result property="currency" column="currency" />
|
||||
<result property="exchangeRate" column="exchange_rate" />
|
||||
<result property="baseAmount" column="base_amount" />
|
||||
<result property="bizType" column="biz_type" />
|
||||
<result property="bizId" column="biz_id" />
|
||||
<result property="bizNo" column="biz_no" />
|
||||
@@ -250,7 +259,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectCustomerBalanceFlowVo">
|
||||
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"
|
||||
<if test="customerId != null">customer_id,</if>
|
||||
<if test="accountId != null">account_id,</if>
|
||||
<if test="currency != null">currency,</if>
|
||||
<if test="exchangeRate != null">exchange_rate,</if>
|
||||
<if test="baseAmount != null">base_amount,</if>
|
||||
<if test="bizType != null">biz_type,</if>
|
||||
<if test="bizId != null">biz_id,</if>
|
||||
<if test="bizNo != null">biz_no,</if>
|
||||
@@ -304,6 +316,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="customerId != null">#{customerId},</if>
|
||||
<if test="accountId != null">#{accountId},</if>
|
||||
<if test="currency != null">#{currency},</if>
|
||||
<if test="exchangeRate != null">#{exchangeRate},</if>
|
||||
<if test="baseAmount != null">#{baseAmount},</if>
|
||||
<if test="bizType != null">#{bizType},</if>
|
||||
<if test="bizId != null">#{bizId},</if>
|
||||
<if test="bizNo != null">#{bizNo},</if>
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ruoyi.system.mapper.BmsExchangeRateMapper">
|
||||
|
||||
<resultMap type="BmsExchangeRate" id="BmsExchangeRateResult">
|
||||
<id property="rateId" column="rate_id" />
|
||||
<result property="fromCurrency" column="from_currency" />
|
||||
<result property="toCurrency" column="to_currency" />
|
||||
<result property="rateType" column="rate_type" />
|
||||
<result property="exchangeRate" column="exchange_rate" />
|
||||
<result property="effectiveDate" column="effective_date" />
|
||||
<result property="expireDate" column="expire_date" />
|
||||
<result property="source" column="source" />
|
||||
<result property="remark" column="remark" />
|
||||
<result property="createBy" column="create_by" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="delFlag" column="del_flag" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectExchangeRateVo">
|
||||
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
|
||||
</sql>
|
||||
|
||||
<select id="selectExchangeRateList" parameterType="BmsExchangeRate" resultMap="BmsExchangeRateResult">
|
||||
<include refid="selectExchangeRateVo"/>
|
||||
<where>
|
||||
del_flag = '0'
|
||||
<if test="fromCurrency != null and fromCurrency != ''"> and from_currency = #{fromCurrency}</if>
|
||||
<if test="toCurrency != null and toCurrency != ''"> and to_currency = #{toCurrency}</if>
|
||||
<if test="rateType != null and rateType != ''"> and rate_type = #{rateType}</if>
|
||||
<if test="source != null and source != ''"> and source like concat('%', #{source}, '%')</if>
|
||||
<if test="params != null and params.beginTime != null and params.beginTime != ''">
|
||||
and effective_date >= #{params.beginTime}
|
||||
</if>
|
||||
<if test="params != null and params.endTime != null and params.endTime != ''">
|
||||
and effective_date <= #{params.endTime}
|
||||
</if>
|
||||
</where>
|
||||
order by effective_date desc, rate_type asc, rate_id desc
|
||||
</select>
|
||||
|
||||
<select id="selectExchangeRateById" parameterType="Long" resultMap="BmsExchangeRateResult">
|
||||
<include refid="selectExchangeRateVo"/>
|
||||
where rate_id = #{rateId} and del_flag = '0'
|
||||
</select>
|
||||
|
||||
<select id="selectEffectiveRate" resultMap="BmsExchangeRateResult">
|
||||
<include refid="selectExchangeRateVo"/>
|
||||
where del_flag = '0'
|
||||
and from_currency = #{fromCurrency}
|
||||
and to_currency = #{toCurrency}
|
||||
<if test="rateType != null and rateType != ''">and rate_type = #{rateType}</if>
|
||||
and effective_date <= #{bizDate}
|
||||
and (expire_date is null or expire_date >= #{bizDate})
|
||||
order by effective_date desc, rate_id desc
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<select id="countDuplicate" resultType="int">
|
||||
select count(1) from sys_exchange_rate
|
||||
where del_flag = '0'
|
||||
and from_currency = #{fromCurrency}
|
||||
and to_currency = #{toCurrency}
|
||||
and rate_type = #{rateType}
|
||||
and effective_date = #{effectiveDate}
|
||||
<if test="excludeId != null">and rate_id != #{excludeId}</if>
|
||||
</select>
|
||||
|
||||
<insert id="insertExchangeRate" parameterType="BmsExchangeRate" useGeneratedKeys="true" keyProperty="rateId">
|
||||
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'
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="updateExchangeRate" parameterType="BmsExchangeRate">
|
||||
update sys_exchange_rate
|
||||
<set>
|
||||
<if test="fromCurrency != null and fromCurrency != ''">from_currency = #{fromCurrency},</if>
|
||||
<if test="toCurrency != null and toCurrency != ''">to_currency = #{toCurrency},</if>
|
||||
<if test="rateType != null and rateType != ''">rate_type = #{rateType},</if>
|
||||
<if test="exchangeRate != null">exchange_rate = #{exchangeRate},</if>
|
||||
<if test="effectiveDate != null">effective_date = #{effectiveDate},</if>
|
||||
expire_date = #{expireDate},
|
||||
<if test="source != null">source = #{source},</if>
|
||||
<if test="remark != null">remark = #{remark},</if>
|
||||
</set>
|
||||
where rate_id = #{rateId}
|
||||
</update>
|
||||
|
||||
<update id="deleteExchangeRateByIds" parameterType="Long">
|
||||
update sys_exchange_rate set del_flag = '1' where rate_id in
|
||||
<foreach item="rateId" collection="array" open="(" separator="," close=")">
|
||||
#{rateId}
|
||||
</foreach>
|
||||
</update>
|
||||
|
||||
</mapper>
|
||||
@@ -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);
|
||||
@@ -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());
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
+137
-27
@@ -10,10 +10,7 @@
|
||||
</div>
|
||||
<div class="head-code">客户编码:{{ customer.customerCode || route.query.customerCode || '-' }}</div>
|
||||
<div class="head-right">
|
||||
<span style="color:#909399;font-size:13px;">币种</span>
|
||||
<el-select v-model="currentCurrency" style="width: 110px;" @change="loadAccounts">
|
||||
<el-option v-for="acc in accountList" :key="acc.currency" :label="acc.currency" :value="acc.currency" />
|
||||
</el-select>
|
||||
<el-tag type="success" size="small">本位币账户 CNY</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -22,7 +19,7 @@
|
||||
<el-row :gutter="12" class="balance-row">
|
||||
<el-col :xs="12" :sm="8" :md="4">
|
||||
<el-card shadow="hover" class="balance-card">
|
||||
<div class="balance-label">账户余额</div>
|
||||
<div class="balance-label">账户余额(CNY)</div>
|
||||
<div class="balance-value primary">{{ formatMoney(currentAccount.balance) }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
@@ -101,12 +98,20 @@
|
||||
<el-tag :type="receiptTypeTag(scope.row.receiptType)">{{ formatOption(receiptTypeOptions, scope.row.receiptType) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="收款金额" align="center" width="130">
|
||||
<el-table-column label="原币金额" align="center" width="130">
|
||||
<template #default="scope">
|
||||
<span class="money-in">{{ scope.row.currency }} {{ formatMoney(scope.row.amount) }}</span>
|
||||
<span>{{ scope.row.currency }} {{ formatMoney(scope.row.amount) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="待核销" align="center" prop="unallocatedAmount" width="110" />
|
||||
<el-table-column label="汇率" align="center" width="110">
|
||||
<template #default="scope">{{ formatRate(scope.row.exchangeRate) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="本位币金额(CNY)" align="center" width="140">
|
||||
<template #default="scope">
|
||||
<span class="money-in">{{ formatMoney(scope.row.baseAmount) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="待核销(CNY)" align="center" prop="unallocatedAmount" width="110" />
|
||||
<el-table-column label="收款方式" align="center" prop="payMethod" width="110">
|
||||
<template #default="scope">{{ formatOption(payMethodOptions, scope.row.payMethod) }}</template>
|
||||
</el-table-column>
|
||||
@@ -189,16 +194,23 @@
|
||||
<el-tag v-else type="danger">支出</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="变动金额" align="center" width="130">
|
||||
<el-table-column label="原币金额" align="center" width="130">
|
||||
<template #default="scope">
|
||||
{{ scope.row.currency }} {{ formatMoney(scope.row.amount) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="汇率" align="center" width="100">
|
||||
<template #default="scope">{{ formatRate(scope.row.exchangeRate) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="本位币变动(CNY)" align="center" width="140">
|
||||
<template #default="scope">
|
||||
<span :class="scope.row.direction === 1 ? 'money-in' : 'money-out'">
|
||||
{{ scope.row.direction === 1 ? '+' : '-' }}{{ formatMoney(scope.row.amount) }}
|
||||
{{ scope.row.direction === 1 ? '+' : '-' }}{{ formatMoney(scope.row.baseAmount != null ? scope.row.baseAmount : scope.row.amount) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="变动前余额" align="center" prop="beforeBalance" width="120" />
|
||||
<el-table-column label="变动后余额" align="center" prop="afterBalance" width="120" />
|
||||
<el-table-column label="币种" align="center" prop="currency" width="70" />
|
||||
<el-table-column label="关联单号" align="center" prop="bizNo" width="180" show-overflow-tooltip />
|
||||
<el-table-column label="操作人" align="center" prop="createBy" width="100" />
|
||||
<el-table-column label="备注" align="center" prop="remark" show-overflow-tooltip min-width="160" />
|
||||
@@ -232,8 +244,8 @@
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="收款金额" prop="amount">
|
||||
<el-input v-model="receiptDialog.form.amount" placeholder="请输入金额" type="number">
|
||||
<template #prepend>¥</template>
|
||||
<el-input v-model="receiptDialog.form.amount" placeholder="请输入原币金额" type="number">
|
||||
<template #prepend>{{ receiptDialog.form.currency || 'CNY' }}</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -246,13 +258,33 @@
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="币种" prop="currency">
|
||||
<el-select v-model="receiptDialog.form.currency" placeholder="请选择" style="width: 100%;">
|
||||
<el-option label="人民币 CNY" value="CNY" />
|
||||
<el-option label="美元 USD" value="USD" />
|
||||
<el-option label="欧元 EUR" value="EUR" />
|
||||
<el-select v-model="receiptDialog.form.currency" placeholder="请选择" style="width: 100%;" @change="handleCurrencyChange">
|
||||
<el-option v-for="c in currencyOptions" :key="c.value" :label="c.label" :value="c.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="汇率" prop="exchangeRate">
|
||||
<el-input-number
|
||||
v-model="receiptDialog.form.exchangeRate"
|
||||
:precision="8"
|
||||
:step="0.0001"
|
||||
:min="0"
|
||||
:disabled="receiptDialog.form.currency === 'CNY'"
|
||||
controls-position="right"
|
||||
style="width: 100%;"
|
||||
@input="handleRateInput"
|
||||
/>
|
||||
<div class="rate-tip">1 {{ receiptDialog.form.currency || 'CNY' }} = {{ formatRate(receiptDialog.form.exchangeRate) }} CNY<span v-if="receiptDialog.form.currency !== 'CNY'">,默认取汇率表,可手工修改</span></div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="本位币金额">
|
||||
<el-input :model-value="'CNY ' + baseAmountPreview" disabled>
|
||||
<template #prepend>≈</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="收款方式" prop="payMethod">
|
||||
<el-select v-model="receiptDialog.form.payMethod" placeholder="请选择" style="width: 100%;" clearable>
|
||||
@@ -321,10 +353,14 @@
|
||||
<el-descriptions-item label="收款类型">
|
||||
<el-tag :type="receiptTypeTag(viewDialog.data.receiptType)">{{ formatOption(receiptTypeOptions, viewDialog.data.receiptType) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="收款金额">
|
||||
<el-descriptions-item label="原币金额">
|
||||
<span class="money-in">{{ viewDialog.data.currency }} {{ formatMoney(viewDialog.data.amount) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="待核销金额">{{ formatMoney(viewDialog.data.unallocatedAmount) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="对CNY汇率">{{ formatRate(viewDialog.data.exchangeRate) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="本位币金额">
|
||||
<span class="money-in">CNY {{ formatMoney(viewDialog.data.baseAmount) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="待核销金额">CNY {{ formatMoney(viewDialog.data.unallocatedAmount) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="收款方式">{{ formatOption(payMethodOptions, viewDialog.data.payMethod) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="银行流水号">{{ viewDialog.data.bankTransactionNo || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="确认时间">{{ formatDateTime(viewDialog.data.confirmedAt) }}</el-descriptions-item>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -185,12 +185,9 @@
|
||||
<el-table-column label="客服姓名" align="center" prop="serviceUser" />
|
||||
<el-table-column label="结算方式" align="center" prop="settlementType" />
|
||||
<el-table-column label="临时额度" align="center" prop="tempEd" />
|
||||
<el-table-column label="账户余额" align="center" prop="accountBalance" width="110">
|
||||
<el-table-column label="账户余额(CNY)" align="center" prop="accountBalance" width="130">
|
||||
<template #default="scope">{{ formatBalance(scope.row.accountBalance) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="外币余额" align="center" prop="foreignCurrencyBalance" width="110">
|
||||
<template #default="scope">{{ formatBalance(scope.row.foreignCurrencyBalance) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="重量阈值" align="center" prop="weightThreshold" />
|
||||
<el-table-column label="抛比" align="center" prop="throwRatio" />
|
||||
<el-table-column label="信用额度" align="center" prop="creditEd" />
|
||||
@@ -315,6 +312,20 @@
|
||||
<el-input v-model="form.ed" placeholder="请输入资金额度" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="结算方式" prop="settlementType">
|
||||
<el-select
|
||||
v-model="form.settlementType"
|
||||
placeholder="请选择结算方式"
|
||||
clearable
|
||||
>
|
||||
<el-option label="月结" value="0" />
|
||||
<el-option label="周结" value="1" />
|
||||
<el-option label="日结" value="2" />
|
||||
<el-option label="现结" value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="回款账期" prop="zq">
|
||||
<el-input v-model="form.zq" placeholder="请输入回款账期" />
|
||||
@@ -364,6 +375,7 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="临时额度" prop="tempEd">
|
||||
<el-input v-model="form.tempEd" placeholder="请输入临时额度" />
|
||||
@@ -376,13 +388,6 @@
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="外币余额">
|
||||
<el-input :model-value="formatBalance(form.foreignCurrencyBalance)" disabled placeholder="0.00">
|
||||
<template #append>外币</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="重量阈值" prop="weightThreshold">
|
||||
<el-input v-model="form.weightThreshold" placeholder="请输入重量阈值" />
|
||||
|
||||
@@ -0,0 +1,430 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 汇率试算 -->
|
||||
<el-card shadow="never" class="calc-card">
|
||||
<template #header>
|
||||
<div class="calc-header">
|
||||
<span class="calc-title">汇率换算 / 本位币(CNY)折算</span>
|
||||
<el-tag size="small" type="info">取数:正向配置 → 反向取倒数 → 经CNY交叉换算</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<el-form :inline="true" @submit.prevent>
|
||||
<el-form-item label="金额">
|
||||
<el-input-number v-model="calc.amount" :precision="2" :step="100" :min="0" controls-position="right" style="width: 170px;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="源币种">
|
||||
<el-select v-model="calc.fromCurrency" filterable allow-create style="width: 110px;" placeholder="如 USD">
|
||||
<el-option v-for="c in currencyOptions" :key="c" :label="c" :value="c" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="目标币种">
|
||||
<el-select v-model="calc.toCurrency" filterable allow-create style="width: 110px;" placeholder="如 CNY">
|
||||
<el-option v-for="c in currencyOptions" :key="c" :label="c" :value="c" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="汇率类型">
|
||||
<el-select v-model="calc.rateType" style="width: 110px;">
|
||||
<el-option v-for="d in rateTypeOptions" :key="d.value" :label="d.label" :value="d.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="业务日期">
|
||||
<el-date-picker v-model="calc.bizDate" type="date" value-format="YYYY-MM-DD" placeholder="默认今天" style="width: 150px;" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Money" @click="handleConvert">换 算</el-button>
|
||||
<el-button type="success" plain @click="handleToBase">折本位币(CNY)</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-alert v-if="calcResult" :closable="false" type="success" class="calc-result">
|
||||
<template #title>
|
||||
<span>{{ calcResult.amount }} {{ calcResult.fromCurrency }}</span>
|
||||
<span class="calc-eq">=</span>
|
||||
<span class="calc-amount">{{ calcResult.result }} {{ calcResult.toCurrency }}</span>
|
||||
<el-divider direction="vertical" />
|
||||
<span style="font-size: 12px; color: #909399;">
|
||||
汇率 1 {{ calcResult.fromCurrency }} = {{ calcResult.rate }} {{ calcResult.toCurrency }}
|
||||
({{ ratePathText(calcResult.ratePath) }},{{ rateTypeText(calcResult.rateType) }},{{ calcResult.bizDate }})
|
||||
</span>
|
||||
</template>
|
||||
</el-alert>
|
||||
</el-card>
|
||||
|
||||
<!-- 查询 -->
|
||||
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
|
||||
<el-form-item label="源币种" prop="fromCurrency">
|
||||
<el-select v-model="queryParams.fromCurrency" filterable allow-create clearable style="width: 120px;" placeholder="全部">
|
||||
<el-option v-for="c in currencyOptions" :key="c" :label="c" :value="c" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="目标币种" prop="toCurrency">
|
||||
<el-select v-model="queryParams.toCurrency" filterable allow-create clearable style="width: 120px;" placeholder="全部">
|
||||
<el-option v-for="c in currencyOptions" :key="c" :label="c" :value="c" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="汇率类型" prop="rateType">
|
||||
<el-select v-model="queryParams.rateType" clearable style="width: 120px;" placeholder="全部">
|
||||
<el-option v-for="d in rateTypeOptions" :key="d.value" :label="d.label" :value="d.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="来源" prop="source">
|
||||
<el-input v-model="queryParams.source" placeholder="汇率来源" clearable style="width: 140px;" @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="生效日期">
|
||||
<el-date-picker v-model="dateRange" value-format="YYYY-MM-DD" type="daterange" range-separator="-" start-placeholder="开始" end-placeholder="结束" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['data:exchangerate:add']">新增汇率</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['data:exchangerate:export']">导出</el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<el-table v-loading="loading" :data="rateList" border>
|
||||
<el-table-column label="ID" align="center" prop="rateId" width="70" />
|
||||
<el-table-column label="币种方向" align="center" width="150">
|
||||
<template #default="scope">
|
||||
<el-tag size="small">{{ scope.row.fromCurrency }}</el-tag>
|
||||
<el-icon class="rate-arrow"><Right /></el-icon>
|
||||
<el-tag size="small" type="success">{{ scope.row.toCurrency }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" align="center" prop="rateType" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="rateTypeTag(scope.row.rateType)">{{ rateTypeText(scope.row.rateType) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="汇率值(1源=?目标)" align="center" prop="exchangeRate" width="170">
|
||||
<template #default="scope">
|
||||
<span class="rate-value">{{ formatRate(scope.row.exchangeRate) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="生效日期" align="center" prop="effectiveDate" width="110" />
|
||||
<el-table-column label="失效日期" align="center" prop="expireDate" width="110">
|
||||
<template #default="scope">{{ scope.row.expireDate || '长期' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="来源" align="center" prop="source" width="110" />
|
||||
<el-table-column label="状态" align="center" width="90">
|
||||
<template #default="scope">
|
||||
<el-tag :type="isEffective(scope.row) ? 'success' : 'info'" size="small">
|
||||
{{ isEffective(scope.row) ? '生效中' : '未生效' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" align="center" prop="remark" show-overflow-tooltip min-width="140" />
|
||||
<el-table-column label="操作" align="center" width="140" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['data:exchangerate:edit']">修改</el-button>
|
||||
<el-button link type="danger" size="small" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['data:exchangerate:remove']">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
|
||||
|
||||
<!-- 新增/修改弹窗 -->
|
||||
<el-dialog :title="title" v-model="open" width="600px" append-to-body>
|
||||
<el-form ref="rateRef" :model="form" :rules="rules" label-width="110px">
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="源币种" prop="fromCurrency">
|
||||
<el-select v-model="form.fromCurrency" filterable allow-create style="width: 100%;" placeholder="ISO三位码 如 USD">
|
||||
<el-option v-for="c in currencyOptions" :key="c" :label="c" :value="c" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="目标币种" prop="toCurrency">
|
||||
<el-select v-model="form.toCurrency" filterable allow-create style="width: 100%;" placeholder="ISO三位码 如 CNY">
|
||||
<el-option v-for="c in currencyOptions" :key="c" :label="c" :value="c" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="汇率类型" prop="rateType">
|
||||
<el-select v-model="form.rateType" style="width: 100%;">
|
||||
<el-option v-for="d in rateTypeOptions" :key="d.value" :label="d.label" :value="d.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="汇率值" prop="exchangeRate">
|
||||
<el-input-number v-model="form.exchangeRate" :precision="8" :step="0.0001" :min="0.00000001" controls-position="right" style="width: 100%;" placeholder="1源币种=?目标币种" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="生效日期" prop="effectiveDate">
|
||||
<el-date-picker v-model="form.effectiveDate" type="date" value-format="YYYY-MM-DD" style="width: 100%;" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="失效日期" prop="expireDate">
|
||||
<el-date-picker v-model="form.expireDate" type="date" value-format="YYYY-MM-DD" placeholder="空=长期有效" style="width: 100%;" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="汇率来源" prop="source">
|
||||
<el-input v-model="form.source" placeholder="中国银行/央行/手工录入" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="open = false">取 消</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="ExchangeRate">
|
||||
import { listRegionOptions } from '@/api/data/region'
|
||||
import {
|
||||
listExchangeRate,
|
||||
getExchangeRate,
|
||||
addExchangeRate,
|
||||
updateExchangeRate,
|
||||
delExchangeRate,
|
||||
convertExchangeRate,
|
||||
convertToBase
|
||||
} from '@/api/data/exchangeRate'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
|
||||
const rateList = ref([])
|
||||
const loading = ref(true)
|
||||
const showSearch = ref(true)
|
||||
const total = ref(0)
|
||||
const dateRange = ref([])
|
||||
const title = ref('')
|
||||
const open = ref(false)
|
||||
const calcResult = ref(null)
|
||||
|
||||
const rateTypeOptions = [
|
||||
{ value: '0', label: '即期汇率' },
|
||||
{ value: '1', label: '月初汇率' },
|
||||
{ value: '2', label: '月末汇率' },
|
||||
{ value: '3', label: '固定汇率' }
|
||||
]
|
||||
|
||||
/** 常用币种兜底 + 从国家资料(币种)去重加载 */
|
||||
const currencyOptions = ref(['CNY', 'USD', 'EUR', 'GBP', 'HKD', 'JPY'])
|
||||
function loadCurrencyOptions() {
|
||||
listRegionOptions().then(res => {
|
||||
const codes = (res.data || [])
|
||||
.map(r => r.currencyCode)
|
||||
.filter(c => !!c)
|
||||
currencyOptions.value = [...new Set([...currencyOptions.value, ...codes])]
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
const queryParams = reactive({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
fromCurrency: '',
|
||||
toCurrency: '',
|
||||
rateType: '',
|
||||
source: ''
|
||||
})
|
||||
|
||||
const calc = reactive({
|
||||
amount: 100,
|
||||
fromCurrency: 'USD',
|
||||
toCurrency: 'CNY',
|
||||
rateType: '0',
|
||||
bizDate: ''
|
||||
})
|
||||
|
||||
const form = ref({})
|
||||
const rules = {
|
||||
fromCurrency: [{ required: true, message: '源币种不能为空', trigger: 'change' }],
|
||||
toCurrency: [{ required: true, message: '目标币种不能为空', trigger: 'change' }],
|
||||
rateType: [{ required: true, message: '请选择汇率类型', trigger: 'change' }],
|
||||
exchangeRate: [{ required: true, message: '汇率值不能为空', trigger: 'blur' }],
|
||||
effectiveDate: [{ required: true, message: '生效日期不能为空', trigger: 'change' }]
|
||||
}
|
||||
|
||||
function rateTypeText(v) {
|
||||
const o = rateTypeOptions.find(d => d.value === String(v))
|
||||
return o ? o.label : v
|
||||
}
|
||||
function rateTypeTag(v) {
|
||||
return { '0': 'danger', '1': 'warning', '2': '', '3': 'success' }[v] || 'info'
|
||||
}
|
||||
function formatRate(v) {
|
||||
const num = Number(v)
|
||||
return isNaN(num) ? '-' : num.toFixed(8).replace(/0+$/, '').replace(/\.$/, '')
|
||||
}
|
||||
function ratePathText(v) {
|
||||
return { DIRECT: '正向配置', REVERSE: '反向取倒数', CROSS: '经CNY交叉换算', SAME: '同币种' }[v] || v
|
||||
}
|
||||
function isEffective(row) {
|
||||
const today = proxy.parseTime(new Date(), '{y}-{m}-{d}')
|
||||
return row.effectiveDate <= today && (!row.expireDate || row.expireDate >= today)
|
||||
}
|
||||
|
||||
function getList() {
|
||||
loading.value = true
|
||||
listExchangeRate(proxy.addDateRange({ ...queryParams }, dateRange.value)).then(res => {
|
||||
rateList.value = res.rows
|
||||
total.value = res.total
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function handleQuery() {
|
||||
queryParams.pageNum = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
function resetQuery() {
|
||||
dateRange.value = []
|
||||
proxy.resetForm('queryRef')
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
function reset() {
|
||||
form.value = {
|
||||
rateId: null,
|
||||
fromCurrency: 'USD',
|
||||
toCurrency: 'CNY',
|
||||
rateType: '0',
|
||||
exchangeRate: null,
|
||||
effectiveDate: proxy.parseTime(new Date(), '{y}-{m}-{d}'),
|
||||
expireDate: '',
|
||||
source: '手工录入',
|
||||
remark: ''
|
||||
}
|
||||
proxy.resetForm('rateRef')
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
reset()
|
||||
open.value = true
|
||||
title.value = '新增汇率'
|
||||
}
|
||||
|
||||
function handleUpdate(row) {
|
||||
reset()
|
||||
getExchangeRate(row.rateId).then(res => {
|
||||
form.value = {
|
||||
...res.data,
|
||||
expireDate: res.data.expireDate || ''
|
||||
}
|
||||
open.value = true
|
||||
title.value = '修改汇率'
|
||||
})
|
||||
}
|
||||
|
||||
function submitForm() {
|
||||
proxy.$refs.rateRef.validate(valid => {
|
||||
if (!valid) return
|
||||
const data = { ...form.value }
|
||||
data.fromCurrency = (data.fromCurrency || '').toUpperCase()
|
||||
data.toCurrency = (data.toCurrency || '').toUpperCase()
|
||||
if (!data.expireDate) data.expireDate = null
|
||||
const action = data.rateId ? updateExchangeRate(data) : addExchangeRate(data)
|
||||
action.then(() => {
|
||||
proxy.$modal.msgSuccess(data.rateId ? '修改成功' : '新增成功')
|
||||
open.value = false
|
||||
getList()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function handleDelete(row) {
|
||||
proxy.$modal.confirm('是否确认删除汇率【' + row.fromCurrency + ' → ' + row.toCurrency + ' / ' + rateTypeText(row.rateType) + '】?').then(() => {
|
||||
return delExchangeRate(row.rateId)
|
||||
}).then(() => {
|
||||
getList()
|
||||
proxy.$modal.msgSuccess('删除成功')
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
function handleExport() {
|
||||
proxy.download('/data/exchangerate/export', { ...queryParams }, `exchange_rate_${new Date().getTime()}.xlsx`)
|
||||
}
|
||||
|
||||
function handleConvert() {
|
||||
if (!calc.amount && calc.amount !== 0) {
|
||||
proxy.$modal.msgWarning('请输入金额')
|
||||
return
|
||||
}
|
||||
convertExchangeRate({
|
||||
amount: calc.amount,
|
||||
fromCurrency: calc.fromCurrency,
|
||||
toCurrency: calc.toCurrency,
|
||||
rateType: calc.rateType,
|
||||
bizDate: calc.bizDate || undefined
|
||||
}).then(res => {
|
||||
calcResult.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
function handleToBase() {
|
||||
if (!calc.amount && calc.amount !== 0) {
|
||||
proxy.$modal.msgWarning('请输入金额')
|
||||
return
|
||||
}
|
||||
convertToBase({
|
||||
amount: calc.amount,
|
||||
currency: calc.fromCurrency,
|
||||
rateType: calc.rateType,
|
||||
bizDate: calc.bizDate || undefined
|
||||
}).then(res => {
|
||||
calcResult.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
loadCurrencyOptions()
|
||||
getList()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.calc-card {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.calc-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.calc-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
.calc-result {
|
||||
margin-top: 4px;
|
||||
}
|
||||
.calc-eq {
|
||||
margin: 0 8px;
|
||||
color: #909399;
|
||||
}
|
||||
.calc-amount {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
.rate-arrow {
|
||||
margin: 0 4px;
|
||||
vertical-align: middle;
|
||||
color: #909399;
|
||||
}
|
||||
.rate-value {
|
||||
font-weight: 600;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user