汇率计算合同金额

This commit is contained in:
2025-06-21 15:09:38 +08:00
parent 7a1128f316
commit d16db4f464
9 changed files with 301 additions and 53 deletions

View File

@@ -0,0 +1,14 @@
package com.ruoyi.framework.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

View File

@@ -236,7 +236,6 @@ public class SysOaProjectController extends BaseController {
@RequestParam(required = false) String projectName,
@RequestParam(required = false) String projectNum,
@RequestParam(required = false) String projectStatus,
@RequestParam(required = false) String isDomestic, // 国内外
@RequestParam(required = false) BigDecimal minContractAmount,
@RequestParam(required = false) BigDecimal maxContractAmount,
@RequestParam(required = false) BigDecimal minProfitLoss,
@@ -250,11 +249,12 @@ public class SysOaProjectController extends BaseController {
) {
pageQuery.setOrderByColumn(sortField);
pageQuery.setIsAsc(sortOrder);
System.out.println("收到");
return iSysOaProjectService.getProjectProfitLossList(
projectName, projectNum, projectStatus, isDomestic,
projectName, projectNum, projectStatus,
minContractAmount, maxContractAmount,
minProfitLoss, maxProfitLoss,
beginTimeStart, beginTimeEnd, profitType,sortField, sortOrder, pageQuery);
beginTimeStart, beginTimeEnd, profitType, pageQuery);
}

View File

@@ -12,10 +12,15 @@ public class ProjectProfitLossVO {
private String projectNum;
private Date beginTime;
private String projectStatus;
private BigDecimal contractAmount;
private BigDecimal originalFunds; // 原始合同金额
private String projectRemark; // 项目备注(用于判断币种)
private BigDecimal contractAmountUsd; // 美元合同金额
private Integer isUsd; // 是否美元1是0否
private BigDecimal exchangeRate; // 实时汇率
private BigDecimal contractAmountCny; // 人民币合同金额
private BigDecimal warehouseCost;
private BigDecimal hrCost;
private BigDecimal profitLoss;
private BigDecimal profitLoss; //盈亏金额
// ...其他字段
// getter/setter
}

View File

@@ -25,7 +25,7 @@ import java.util.Map;
* @date 2024-01-11
*/
public interface SysOaProjectMapper extends BaseMapperPlus<SysOaProjectMapper, SysOaProject, SysOaProjectVo> {
Page<ProjectProfitLossVO> selectProfitLossPage(@Param("page") Page<ProjectProfitLossVO> page, @Param("ew") Wrapper<SysOaProject> wrapper);
Page<ProjectProfitLossVO> selectProfitLossPage(@Param("page") Page<ProjectProfitLossVO> page, @Param("ew") Wrapper<SysOaProject> wrapper,@Param("exchangeRate") BigDecimal exchangeRate); // 新增汇率参数
Page<SysOaOutWarehouseListVo> selectPageOutList(@Param("page") Page<SysOaOutWarehouseListVo> page,@Param(Constants.WRAPPER) Wrapper<SysOaProject> queryWrapper);

View File

@@ -0,0 +1,23 @@
package com.ruoyi.oa.service;
import java.math.BigDecimal;
import java.util.Date;
/**
* 汇率服务接口
*/
public interface IExchangeRateService {
/**
* 获取当前日期的美元实时汇率
* @return 美元兑人民币汇率
*/
BigDecimal getCurrentUsdExchangeRate();
/**
* 获取指定日期的美元汇率
* @param date 日期
* @return 美元兑人民币汇率
*/
BigDecimal getUsdExchangeRate(Date date);
}

View File

@@ -24,10 +24,10 @@ public interface ISysOaProjectService {
/**
* 项目盈亏排序
*/
TableDataInfo<ProjectProfitLossVO> getProjectProfitLossList(String projectName,String projectNum,String projectStatus,String isDomestic,
TableDataInfo<ProjectProfitLossVO> getProjectProfitLossList(String projectName,String projectNum,String projectStatus,
BigDecimal minContractAmount,BigDecimal maxContractAmount,BigDecimal minProfitLoss,
BigDecimal maxProfitLoss,String beginTimeStart,String beginTimeEnd,String profitType,
String sortField, String sortOrder, PageQuery pageQuery);
PageQuery pageQuery);
/**
* 查询项目管理
*/

View File

@@ -0,0 +1,132 @@
package com.ruoyi.oa.service.impl;
import com.ruoyi.oa.service.IExchangeRateService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Service
public class ExchangeRateServiceImpl implements IExchangeRateService {
@Autowired
private RestTemplate restTemplate;
// 汇率缓存避免重复调用API
private final ConcurrentHashMap<String, BigDecimal> rateCache = new ConcurrentHashMap<>();
// 缓存过期时间1小时
private static final long CACHE_EXPIRE_TIME = 60 * 60 * 1000;
// 缓存时间记录
private final ConcurrentHashMap<String, Long> cacheTimeMap = new ConcurrentHashMap<>();
@Override
public BigDecimal getCurrentUsdExchangeRate() {
String today = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
return getUsdExchangeRate(new Date());
}
@Override
public BigDecimal getUsdExchangeRate(Date date) {
String dateStr = new SimpleDateFormat("yyyy-MM-dd").format(date);
// 检查缓存
if (isCacheValid(dateStr)) {
return rateCache.get(dateStr);
}
try {
// 调用汇率API
BigDecimal rate = callExchangeRateAPI(dateStr);
// 更新缓存
rateCache.put(dateStr, rate);
cacheTimeMap.put(dateStr, System.currentTimeMillis());
log.info("获取汇率成功,日期:{},汇率:{}", dateStr, rate);
return rate;
} catch (Exception e) {
log.error("获取汇率失败,日期:{},使用默认汇率", dateStr, e);
// 返回默认汇率
return new BigDecimal("7.2");
}
}
/**
* 检查缓存是否有效
*/
private boolean isCacheValid(String dateStr) {
Long cacheTime = cacheTimeMap.get(dateStr);
if (cacheTime == null) {
return false;
}
return (System.currentTimeMillis() - cacheTime) < CACHE_EXPIRE_TIME;
}
/**
* 调用汇率API
*/
private BigDecimal callExchangeRateAPI(String dateStr) {
// 方式1使用免费的汇率API
String url = "https://api.exchangerate-api.com/v4/latest/USD";
try {
String response = restTemplate.getForObject(url, String.class);
JSONObject json = JSON.parseObject(response);
// 解析汇率数据
JSONObject rates = json.getJSONObject("rates");
BigDecimal rate = rates.getBigDecimal("CNY");
if (rate != null) {
return rate;
}
} catch (Exception e) {
log.warn("调用汇率API失败尝试备用API", e);
}
// 备用方案使用其他免费API
try {
String backupUrl = "https://api.frankfurter.app/latest?from=USD&to=CNY";
String response = restTemplate.getForObject(backupUrl, String.class);
JSONObject json = JSON.parseObject(response);
JSONObject rates = json.getJSONObject("rates");
BigDecimal rate = rates.getBigDecimal("CNY");
if (rate != null) {
return rate;
}
} catch (Exception e) {
log.warn("备用API也失败使用默认汇率", e);
}
// 最终备用方案:使用默认汇率
return getDefaultExchangeRate(dateStr);
}
/**
* 获取默认汇率
*/
private BigDecimal getDefaultExchangeRate(String dateStr) {
// 根据当前年份返回不同的默认汇率
if (dateStr.startsWith("2024")) {
return new BigDecimal("7.2");
} else if (dateStr.startsWith("2023")) {
return new BigDecimal("7.1");
} else if (dateStr.startsWith("2022")) {
return new BigDecimal("6.9");
} else {
return new BigDecimal("7.0");
}
}
}

View File

@@ -14,6 +14,7 @@ import com.ruoyi.oa.domain.dto.ProjectActivityDTO;
import com.ruoyi.oa.domain.dto.ProjectDataDTO;
import com.ruoyi.oa.domain.vo.*;
import com.ruoyi.oa.service.CodeGeneratorService;
import com.ruoyi.oa.service.IExchangeRateService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -41,6 +42,8 @@ public class SysOaProjectServiceImpl implements ISysOaProjectService {
@Autowired
private CodeGeneratorService codeGeneratorService;
@Autowired
private IExchangeRateService exchangeRateService;
// 1. 定义常量列表(最好放到某个常量类里)
@@ -49,44 +52,97 @@ public class SysOaProjectServiceImpl implements ISysOaProjectService {
)));
/**
* 查询项目盈亏分页列表
*
* @param projectName 项目名称 (筛选)
* @param projectNum 项目编号 (筛选)
* @param projectStatus 项目状态 (筛选)
* @param minContractAmount 最小合同额 (筛选)
* @param maxContractAmount 最大合同额 (筛选)
* @param minProfitLoss 最小盈亏 (筛选)
* @param maxProfitLoss 最大盈亏 (筛选)
* @param beginTimeStart 项目开始时间范围 (筛选)
* @param beginTimeEnd 项目结束时间范围 (筛选)
* @param profitType 盈亏类型 ("profit" 或 "loss") (筛选)
* @param pageQuery 分页及排序查询参数
* @return 分页结果
*/
@Override
public TableDataInfo<ProjectProfitLossVO> getProjectProfitLossList(
String projectName, String projectNum, String projectStatus, String isDomestic,
String projectName, String projectNum, String projectStatus,
BigDecimal minContractAmount, BigDecimal maxContractAmount,
BigDecimal minProfitLoss, BigDecimal maxProfitLoss,
String beginTimeStart, String beginTimeEnd, String profitType,
String sortField, String sortOrder,
PageQuery pageQuery
) {
Page<ProjectProfitLossVO> page = pageQuery.build();
QueryWrapper<SysOaProject> wrapper = new QueryWrapper<>();
// 1. 获取实时汇率,用于后续计算
BigDecimal currentExchangeRate = exchangeRateService.getCurrentUsdExchangeRate();
// 基础条件
// 2. 构建基础查询条件 (WHERE子句)
QueryWrapper<SysOaProject> wrapper = new QueryWrapper<>();
wrapper.like(StringUtils.isNotBlank(projectName), "p.project_name", projectName);
wrapper.like(StringUtils.isNotBlank(projectNum), "p.project_num", projectNum);
wrapper.eq(StringUtils.isNotBlank(projectStatus), "p.project_status", projectStatus);
wrapper.eq(StringUtils.isNotBlank(isDomestic), "p.is_domestic", isDomestic);
// wrapper.eq(StringUtils.isNotBlank(isDomestic), "p.is_domestic", isDomestic); // 如果需要,可以放开此注释
// 对原始合同金额进行筛选
wrapper.ge(minContractAmount != null, "p.funds", minContractAmount);
wrapper.le(maxContractAmount != null, "p.funds", maxContractAmount);
// 对项目时间进行筛选
wrapper.ge(StringUtils.isNotBlank(beginTimeStart), "p.begin_time", beginTimeStart);
wrapper.le(StringUtils.isNotBlank(beginTimeEnd), "p.begin_time", beginTimeEnd);
// 盈亏区间
// 3. 构建HAVING子句用于筛选计算后的盈亏值
// HAVING子句允许我们在GROUP BY或聚合函数计算后进行条件筛选
StringBuilder havingClause = new StringBuilder();
// 盈亏类型筛选
if ("profit".equals(profitType)) {
havingClause.append("profit_loss > 0");
} else if ("loss".equals(profitType)) {
havingClause.append("profit_loss < 0");
}
// 盈亏区间筛选
if (minProfitLoss != null) {
wrapper.apply("p.funds - COALESCE(wd.total_warehouse_cost,0) - COALESCE(hr.total_hr_cost,0) >= {0}", minProfitLoss);
if (havingClause.length() > 0) {
havingClause.append(" AND ");
}
havingClause.append("profit_loss >= ").append(minProfitLoss);
}
if (maxProfitLoss != null) {
wrapper.apply("p.funds - COALESCE(wd.total_warehouse_cost,0) - COALESCE(hr.total_hr_cost,0) <= {0}", maxProfitLoss);
if (havingClause.length() > 0) {
havingClause.append(" AND ");
}
havingClause.append("profit_loss <= ").append(maxProfitLoss);
}
// 盈利/亏损筛选
if ("profit".equals(profitType)) {
wrapper.apply("p.funds - COALESCE(wd.total_warehouse_cost,0) - COALESCE(hr.total_hr_cost,0) > 0");
} else if ("loss".equals(profitType)) {
wrapper.apply("p.funds - COALESCE(wd.total_warehouse_cost,0) - COALESCE(hr.total_hr_cost,0) < 0");
// 4. 将HAVING子句应用到QueryWrapper
if (havingClause.length() > 0) {
wrapper.having(havingClause.toString());
}
// 5. 构建分页对象 (排序由PageQuery自动处理)
Page<ProjectProfitLossVO> page = pageQuery.build();
// 6. 调用Mapper方法传入汇率让数据库完成计算、筛选和分页
Page<ProjectProfitLossVO> result = baseMapper.selectProfitLossPage(page, wrapper, currentExchangeRate);
// 7. 再次填充VO中的展示字段确保前端获取的数据是完整的
// 虽然数据库已经计算了profit_loss但在这里填充其他相关字段如汇率、转换后的合同额可以让API返回更丰富的信息
for (ProjectProfitLossVO vo : result.getRecords()) {
vo.setExchangeRate(currentExchangeRate); // 填充实时汇率
if (vo.getIsUsd() == 1) {
// 计算人民币合同额用于展示
vo.setContractAmountCny(vo.getOriginalFunds().multiply(currentExchangeRate));
} else {
vo.setContractAmountCny(vo.getOriginalFunds());
}
// profit_loss 已经由数据库计算返回,无需再次计算
}
Page<ProjectProfitLossVO> result = baseMapper.selectProfitLossPage(page, wrapper);
return TableDataInfo.build(result);
}

View File

@@ -813,6 +813,8 @@
ORDER BY
TIMESTAMPDIFF(DAY, NOW(), p.finish_time) ASC
</select>
<!-- SysOaProjectMapper.xml -->
<select id="selectProfitLossPage" resultType="com.ruoyi.oa.domain.vo.ProjectProfitLossVO">
SELECT
p.project_id,
@@ -820,38 +822,54 @@
p.project_num,
p.begin_time,
p.project_status,
p.funds AS contract_amount,
p.funds AS original_funds,
p.remark AS project_remark,
COALESCE(wd.total_warehouse_cost, 0) AS warehouse_cost,
COALESCE(hr.total_hr_cost, 0) AS hr_cost,
(p.funds - COALESCE(wd.total_warehouse_cost, 0) - COALESCE(hr.total_hr_cost, 0)) AS profit_loss
FROM sys_oa_project p
LEFT JOIN (
-- 物料成本子查询
SELECT
project_id,
SUM(amount * sign_price) AS total_warehouse_cost
FROM sys_oa_warehouse_detail
WHERE del_flag = 0
GROUP BY project_id
) wd ON p.project_id = wd.project_id
LEFT JOIN (
-- 人力成本子查询(日薪转小时计算)
SELECT
a.project_id,
SUM(
CASE
-- 当小时数存在时,使用日薪/8计算小时薪资
WHEN a.hour > 0 THEN a.hour * (COALESCE(u.labor_cost, 0) DIV 8)
-- 当日数存在时,使用日薪×天数
ELSE a.day_length * COALESCE(u.labor_cost, 0)
END
) AS total_hr_cost
FROM sys_oa_attendance a
LEFT JOIN sys_user u ON a.user_id = u.user_id
WHERE a.del_flag = 0
GROUP BY a.project_id
) hr ON p.project_id = hr.project_id
CASE
WHEN p.remark LIKE '%美元%' OR p.remark LIKE '%美金%' THEN 1
ELSE 0
END AS is_usd,
(
CASE
WHEN p.remark LIKE '%美元%' OR p.remark LIKE '%美金%'
THEN p.funds * #{exchangeRate}
ELSE p.funds
END
- COALESCE(wd.total_warehouse_cost, 0)
- COALESCE(hr.total_hr_cost, 0)
) AS profit_loss
FROM
sys_oa_project p
LEFT JOIN (
SELECT
project_id,
SUM(amount * sign_price) AS total_warehouse_cost
FROM
sys_oa_warehouse_detail
WHERE
del_flag = 0
GROUP BY
project_id
) wd ON p.project_id = wd.project_id
LEFT JOIN (
SELECT
a.project_id,
SUM(
CASE
WHEN a.hour > 0 THEN a.hour * u.labor_cost
ELSE a.day_length * 8 * u.labor_cost
END
) AS total_hr_cost
FROM
sys_oa_attendance a
JOIN
sys_user u ON a.user_id = u.user_id
WHERE
a.del_flag = 0
GROUP BY
a.project_id
) hr ON p.project_id = hr.project_id
${ew.customSqlSegment}
</select>
</mapper>