✨ 新增毛利分析报表及提成设置

This commit is contained in:
caojia
2026-10-14 16:57:44 +08:00
parent c0c9c798b3
commit 0087aa3712
17 changed files with 1671 additions and 0 deletions
@@ -0,0 +1,27 @@
package com.bms.price.domain;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/** 业务员固定提成比例配置。commissionRate 按百分数存储,例如 5 表示 5%。 */
@Data
public class PrcProfitCommissionRate implements Serializable
{
private static final long serialVersionUID = 1L;
private Long rateId;
private String belongSalerCode;
private String belongSaler;
private BigDecimal commissionRate;
private String status;
private String remark;
private String createBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
private String updateBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
}
@@ -0,0 +1,59 @@
package com.bms.price.domain.vo;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import com.bms.common.annotation.Excel;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/** 订单维度毛利报表行。 */
@Data
public class PrcProfitReportOrderRow implements Serializable
{
private static final long serialVersionUID = 1L;
@Excel(name = "订单号", width = 24)
private String orderNo;
@Excel(name = "客户代码", width = 16)
private String customerCode;
@Excel(name = "客户名称", width = 24)
private String customerName;
@Excel(name = "业务员", width = 16)
private String belongSaler;
@Excel(name = "业务员编码", width = 16)
private String belongSalerCode;
@Excel(name = "产品代码", width = 16)
private String productCode;
@Excel(name = "产品名称", width = 24)
private String productName;
@Excel(name = "国家代码", width = 12)
private String countryCode;
@Excel(name = "国家/地区", width = 18)
private String countryName;
@Excel(name = "币种", width = 10)
private String currency;
@Excel(name = "计费时间", dateFormat = "yyyy-MM-dd HH:mm:ss", width = 22)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date bizDate;
@Excel(name = "产品价(目录价)", scale = 4, width = 20)
private BigDecimal costAmount;
@Excel(name = "红线价", scale = 4, width = 16)
private BigDecimal redlineAmount;
@Excel(name = "红线毛利", scale = 4, width = 16)
private BigDecimal redlineGrossProfit;
@Excel(name = "红线毛利率(%)", scale = 4, width = 18)
private BigDecimal redlineGrossMarginRate;
@Excel(name = "销售价", scale = 4, width = 16)
private BigDecimal salesAmount;
@Excel(name = "销售毛利", scale = 4, width = 16)
private BigDecimal salesGrossProfit;
@Excel(name = "销售毛利率(%)", scale = 4, width = 18)
private BigDecimal salesGrossMarginRate;
@Excel(name = "提成比例(%)", scale = 4, width = 16)
private BigDecimal commissionRate;
@Excel(name = "提成金额", scale = 4, width = 16)
private BigDecimal commissionAmount;
@Excel(name = "应收状态", width = 14)
private String arBillStatus;
}
@@ -0,0 +1,46 @@
package com.bms.price.domain.vo;
import java.io.Serializable;
import java.math.BigDecimal;
import com.bms.common.annotation.Excel;
import lombok.Data;
/** 产品+目的地维度毛利汇总行。 */
@Data
public class PrcProfitReportProductRow implements Serializable
{
private static final long serialVersionUID = 1L;
@Excel(name = "产品代码", width = 16)
private String productCode;
@Excel(name = "产品名称", width = 24)
private String productName;
@Excel(name = "国家代码", width = 12)
private String countryCode;
@Excel(name = "国家/地区", width = 18)
private String countryName;
@Excel(name = "币种", width = 10)
private String currency;
@Excel(name = "订单数", width = 12)
private Long orderCount;
@Excel(name = "产品价(目录价)", scale = 4, width = 20)
private BigDecimal costAmount;
@Excel(name = "红线价", scale = 4, width = 16)
private BigDecimal redlineAmount;
@Excel(name = "红线毛利", scale = 4, width = 16)
private BigDecimal redlineGrossProfit;
@Excel(name = "红线毛利率(%)", scale = 4, width = 18)
private BigDecimal redlineGrossMarginRate;
@Excel(name = "销售价", scale = 4, width = 16)
private BigDecimal salesAmount;
@Excel(name = "销售毛利", scale = 4, width = 16)
private BigDecimal salesGrossProfit;
@Excel(name = "销售毛利率(%)", scale = 4, width = 18)
private BigDecimal salesGrossMarginRate;
@Excel(name = "提成金额", scale = 4, width = 16)
private BigDecimal commissionAmount;
@Excel(name = "未计算提成订单数", width = 20)
private Long commissionMissingOrderCount;
@Excel(name = "产品价或价格缺失订单数", width = 22)
private Long incompleteOrderCount;
}
@@ -0,0 +1,38 @@
package com.bms.price.domain.vo;
import java.util.Date;
import java.util.List;
import org.springframework.format.annotation.DateTimeFormat;
import com.bms.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/** 毛利报表查询条件。 */
@Data
@EqualsAndHashCode(callSuper = true)
public class PrcProfitReportQuery extends BaseEntity
{
private static final long serialVersionUID = 1L;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date beginBizDate;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date endBizDate;
private String orderNo;
/** 订单号拆分后的多值查询条件。 */
private List<String> orderNos;
private String customerCode;
/** 客户代码拆分后的多值查询条件。 */
private List<String> customerCodes;
private String customerName;
/** 客户名称拆分后的多值查询条件。 */
private List<String> customerNames;
private String belongSalerCode;
private String productCode;
private String countryCode;
private String currency;
/** 是否按产品维度分组值下钻订单明细,允许分组字段为空。 */
private Boolean productDrilldown;
/** 订单明细弹窗的筛选类型:ALL、COMMISSION_MISSING、INCOMPLETE。 */
private String detailType;
}
@@ -0,0 +1,25 @@
package com.bms.price.domain.vo;
import java.io.Serializable;
import java.math.BigDecimal;
import lombok.Data;
/** 毛利报表汇总指标。 */
@Data
public class PrcProfitReportSummary implements Serializable
{
private static final long serialVersionUID = 1L;
private Long orderCount;
private Long incompleteOrderCount;
private String currency;
private BigDecimal costAmount;
private BigDecimal redlineAmount;
private BigDecimal redlineGrossProfit;
private BigDecimal redlineGrossMarginRate;
private BigDecimal salesAmount;
private BigDecimal salesGrossProfit;
private BigDecimal salesGrossMarginRate;
private BigDecimal commissionAmount;
private Long commissionMissingOrderCount;
}
@@ -0,0 +1,13 @@
package com.bms.price.mapper;
import java.util.List;
import com.bms.price.domain.PrcProfitCommissionRate;
public interface PrcProfitCommissionRateMapper
{
List<PrcProfitCommissionRate> selectAll();
int upsert(PrcProfitCommissionRate rate);
int deleteById(Long rateId);
}
@@ -0,0 +1,17 @@
package com.bms.price.mapper;
import java.util.List;
import com.bms.price.domain.vo.PrcProfitReportOrderRow;
import com.bms.price.domain.vo.PrcProfitReportProductRow;
import com.bms.price.domain.vo.PrcProfitReportQuery;
import com.bms.price.domain.vo.PrcProfitReportSummary;
/** 毛利报表 mapper;金额事实取自 prc_income_bill。 */
public interface PrcProfitReportMapper
{
List<PrcProfitReportOrderRow> selectOrderRows(PrcProfitReportQuery query);
List<PrcProfitReportProductRow> selectProductRows(PrcProfitReportQuery query);
List<PrcProfitReportSummary> selectSummary(PrcProfitReportQuery query);
}
@@ -0,0 +1,24 @@
package com.bms.price.service;
import java.util.List;
import com.bms.price.domain.PrcProfitCommissionRate;
import com.bms.price.domain.vo.PrcProfitReportOrderRow;
import com.bms.price.domain.vo.PrcProfitReportProductRow;
import com.bms.price.domain.vo.PrcProfitReportQuery;
import com.bms.price.domain.vo.PrcProfitReportSummary;
/** 毛利报表服务。 */
public interface IPrcProfitReportService
{
List<PrcProfitReportOrderRow> selectOrderRows(PrcProfitReportQuery query);
List<PrcProfitReportProductRow> selectProductRows(PrcProfitReportQuery query);
List<PrcProfitReportSummary> selectSummary(PrcProfitReportQuery query);
List<PrcProfitCommissionRate> selectCommissionRates();
void saveCommissionRate(PrcProfitCommissionRate rate);
int deleteCommissionRate(Long rateId);
}
@@ -0,0 +1,139 @@
package com.bms.price.service.impl;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.math.BigDecimal;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.bms.common.annotation.DataScope;
import com.bms.common.exception.ServiceException;
import com.bms.common.utils.SecurityUtils;
import com.bms.price.domain.PrcProfitCommissionRate;
import com.bms.price.mapper.PrcProfitCommissionRateMapper;
import com.bms.price.domain.vo.PrcProfitReportOrderRow;
import com.bms.price.domain.vo.PrcProfitReportProductRow;
import com.bms.price.domain.vo.PrcProfitReportQuery;
import com.bms.price.domain.vo.PrcProfitReportSummary;
import com.bms.price.mapper.PrcProfitReportMapper;
import com.bms.price.service.IPrcProfitReportService;
/** 毛利报表:以 prc_income_bill 为金额事实表,不关联应付模块。 */
@Service
public class PrcProfitReportServiceImpl implements IPrcProfitReportService
{
private static final int MULTI_VALUE_FILTER_LIMIT = 2000;
@Autowired
private PrcProfitReportMapper profitReportMapper;
@Autowired
private PrcProfitCommissionRateMapper commissionRateMapper;
@Override
@DataScope(deptAlias = "su", deptField = "dept_id", userAlias = "su", userField = "user_id", permission = "report:profit:list")
public List<PrcProfitReportOrderRow> selectOrderRows(PrcProfitReportQuery query)
{
normalizeMultiValueFilters(query);
return profitReportMapper.selectOrderRows(query);
}
@Override
@DataScope(deptAlias = "su", deptField = "dept_id", userAlias = "su", userField = "user_id", permission = "report:profit:list")
public List<PrcProfitReportProductRow> selectProductRows(PrcProfitReportQuery query)
{
normalizeMultiValueFilters(query);
return profitReportMapper.selectProductRows(query);
}
@Override
@DataScope(deptAlias = "su", deptField = "dept_id", userAlias = "su", userField = "user_id", permission = "report:profit:list")
public List<PrcProfitReportSummary> selectSummary(PrcProfitReportQuery query)
{
normalizeMultiValueFilters(query);
return profitReportMapper.selectSummary(query);
}
/** 将报表的多行输入拆分为去重后的查询条件,支持换行、空格、逗号和分号分隔。 */
private void normalizeMultiValueFilters(PrcProfitReportQuery query)
{
if (query == null)
{
return;
}
query.setOrderNos(splitFilterValues(query.getOrderNo(), "订单号"));
query.setCustomerCodes(splitFilterValues(query.getCustomerCode(), "客户代码"));
query.setCustomerNames(splitFilterValues(query.getCustomerName(), "客户名称"));
}
private List<String> splitFilterValues(String rawValue, String fieldName)
{
LinkedHashSet<String> values = new LinkedHashSet<>();
if (rawValue != null && !rawValue.trim().isEmpty())
{
for (String item : rawValue.split("[\\s,,;;]+"))
{
String value = item.trim();
if (!value.isEmpty())
{
values.add(value);
}
}
}
if (values.size() > MULTI_VALUE_FILTER_LIMIT)
{
throw new ServiceException(fieldName + "最多输入 " + MULTI_VALUE_FILTER_LIMIT + " 个不重复值");
}
return new ArrayList<>(values);
}
@Override
public List<PrcProfitCommissionRate> selectCommissionRates()
{
return commissionRateMapper.selectAll();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void saveCommissionRate(PrcProfitCommissionRate rate)
{
if (rate == null || rate.getBelongSalerCode() == null || rate.getBelongSalerCode().trim().isEmpty())
{
throw new ServiceException("业务员编码不能为空");
}
BigDecimal commissionRate = rate.getCommissionRate();
if (commissionRate == null || commissionRate.compareTo(BigDecimal.ZERO) < 0
|| commissionRate.compareTo(new BigDecimal("100")) > 0)
{
throw new ServiceException("提成比例必须在 0 至 100 之间");
}
String status = rate.getStatus();
if (!"0".equals(status) && !"1".equals(status))
{
throw new ServiceException("提成比例状态不正确");
}
String salerCode = rate.getBelongSalerCode().trim();
String salerName = rate.getBelongSaler() == null ? null : rate.getBelongSaler().trim();
if (salerCode.length() > 64 || (salerName != null && salerName.length() > 64)
|| (rate.getRemark() != null && rate.getRemark().length() > 500))
{
throw new ServiceException("业务员编码、名称或备注超过最大长度");
}
rate.setBelongSalerCode(salerCode);
rate.setBelongSaler(salerName);
rate.setCreateBy(SecurityUtils.getUsername());
commissionRateMapper.upsert(rate);
}
@Override
@Transactional(rollbackFor = Exception.class)
public int deleteCommissionRate(Long rateId)
{
if (rateId == null)
{
throw new ServiceException("提成比例配置ID不能为空");
}
return commissionRateMapper.deleteById(rateId);
}
}