Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.ruoyi.oa.domain.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@@ -12,10 +13,20 @@ public class ProjectProfitLossVO {
|
||||
private String projectNum;
|
||||
private Date beginTime;
|
||||
private String projectStatus;
|
||||
private BigDecimal contractAmount;
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||
private BigDecimal originalFunds; // 原始合同金额
|
||||
private String projectRemark; // 项目备注(用于判断币种)
|
||||
private BigDecimal contractAmountUsd; // 美元合同金额
|
||||
private Integer isUsd; // 是否美元(1是,0否)
|
||||
private BigDecimal exchangeRate; // 实时汇率
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||
private BigDecimal contractAmountCny; // 人民币合同金额
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||
private BigDecimal warehouseCost;
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||
private BigDecimal hrCost;
|
||||
private BigDecimal profitLoss;
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||
private BigDecimal profitLoss; //盈亏金额
|
||||
// ...其他字段
|
||||
// getter/setter
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
/**
|
||||
* 查询项目管理
|
||||
*/
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,56 @@
|
||||
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
|
||||
-- 当小时数存在时,使用日薪/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
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user