This commit is contained in:
wuweihua
2026-09-22 10:15:22 +08:00
30 changed files with 1089 additions and 122 deletions
@@ -1,12 +1,17 @@
package com.ruoyi.web.controller.ar;
import java.util.List;
import java.util.Date;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.validation.annotation.Validated;
import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@@ -31,6 +36,7 @@ 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.exception.ServiceException;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.uuid.IdUtils;
@@ -49,12 +55,23 @@ public class ArBillController extends BaseController {
@Autowired
private IArBillService billService;
/**
* 查询参数中的日期必须严格按 yyyy-MM-dd 解析,禁止非法日期自动滚动到其他日期。
*/
@InitBinder
public void initDateBinder(WebDataBinder binder) {
java.text.SimpleDateFormat formatter = new java.text.SimpleDateFormat("yyyy-MM-dd");
formatter.setLenient(false);
binder.registerCustomEditor(Date.class, new CustomDateEditor(formatter, true));
}
/**
* 查询账单列表;分页参数由 BaseController.startPage 交给 MyBatis PageHelper 处理。
*/
@PreAuthorize("@ss.hasPermi('ar:bill:list')")
@GetMapping("/list")
public TableDataInfo list(ArBillQuery query) {
public TableDataInfo list(ArBillQuery query, HttpServletRequest request) {
rejectMultiValueParameter(request, "customerId");
startPage();
List<ArBill> list = billService.selectArBillList(query);
return getDataTable(list);
@@ -104,7 +121,8 @@ public class ArBillController extends BaseController {
*/
@PreAuthorize("@ss.hasPermi('ar:bill:generate')")
@GetMapping("/generate/preview")
public AjaxResult preview(ArBillGenerateQuery query) {
public AjaxResult preview(ArBillGenerateQuery query, HttpServletRequest request) {
rejectMultiValueParameter(request, "customerId");
startPage();
List<PrcOrderBill> list = billService.previewBill(query);
TableDataInfo pageData = getDataTable(list);
@@ -252,4 +270,17 @@ public class ArBillController extends BaseController {
if (StringUtils.isNotEmpty(headerRequestId)) return headerRequestId;
return IdUtils.fastSimpleUUID();
}
/**
* 禁止把 customerId 以数组参数或重复参数提交,避免参数绑定失败后悄悄退化为无客户条件查询。
*/
private void rejectMultiValueParameter(HttpServletRequest request, String parameterName) {
if (request.getParameterMap().containsKey(parameterName + "[]")) {
throw new ServiceException("查询参数[" + parameterName + "]格式不正确,请传入单个客户ID");
}
String[] values = request.getParameterValues(parameterName);
if (values != null && values.length > 1) {
throw new ServiceException("查询参数[" + parameterName + "]只能传入一个值");
}
}
}
@@ -2,6 +2,8 @@ package com.ruoyi.web.controller.customer.customermoney;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
@@ -17,6 +19,7 @@ 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.customer.customermoney.api.ICustomerAccountApi;
import com.ruoyi.customer.customermoney.domain.CustomerBalanceFlow;
import com.ruoyi.customer.customermoney.domain.CustomerCreditApply;
@@ -25,6 +28,9 @@ import com.ruoyi.customer.customermoney.domain.CustomerReceipt;
import com.ruoyi.customer.customermoney.service.ICreditQuotaService;
import com.ruoyi.customer.customermoney.service.ICustomerMoneyService;
import com.ruoyi.customer.customermoney.vo.CustomerAccountStatusVO;
import com.ruoyi.customer.customermoney.vo.CustomerBalanceFlowExportVO;
import com.ruoyi.customer.customermoney.vo.CustomerCreditFlowExportVO;
import com.ruoyi.customer.customermoney.vo.CustomerReceiptExportVO;
import com.ruoyi.common.utils.SecurityUtils;
/**
@@ -89,6 +95,24 @@ public class CustomerMoneyController extends BaseController
List<CustomerReceipt> list = customerMoneyService.selectReceiptList(query);
return getDataTable(list);
}
/**
* 导出收款记录(按当前筛选条件,全量导出不分页)
*/
@PreAuthorize("@ss.hasPermi('customersupplier:customerinfo:export')")
@Log(title = "客户收款记录", businessType = BusinessType.EXPORT)
@PostMapping("/receipt/export")
public void receiptExport(HttpServletResponse response, CustomerReceipt query)
{
List<CustomerReceiptExportVO> list = Collections.emptyList();
if (query != null && query.getCustomerId() != null && query.getCustomerId() != 0L)
{
list = customerMoneyService.selectReceiptList(query).stream()
.map(CustomerReceiptExportVO::from).collect(Collectors.toList());
}
ExcelUtil<CustomerReceiptExportVO> util = new ExcelUtil<>(CustomerReceiptExportVO.class);
util.exportExcel(response, list, "收款记录");
}
/**
* 获取收款单详情(含收款附件)
*/
@@ -149,6 +173,24 @@ public class CustomerMoneyController extends BaseController
return getDataTable(list);
}
/**
* 导出余额流水(按当前筛选条件,全量导出不分页)
*/
@PreAuthorize("@ss.hasPermi('customersupplier:customerinfo:export')")
@Log(title = "客户余额流水", businessType = BusinessType.EXPORT)
@PostMapping("/flow/export")
public void flowExport(HttpServletResponse response, CustomerBalanceFlow query)
{
List<CustomerBalanceFlowExportVO> list = Collections.emptyList();
if (query != null && query.getCustomerId() != null && query.getCustomerId() != 0L)
{
list = customerMoneyService.selectFlowList(query).stream()
.map(CustomerBalanceFlowExportVO::from).collect(Collectors.toList());
}
ExcelUtil<CustomerBalanceFlowExportVO> util = new ExcelUtil<>(CustomerBalanceFlowExportVO.class);
util.exportExcel(response, list, "余额流水");
}
/**
* 查询额度流水列表
*/
@@ -164,6 +206,24 @@ public class CustomerMoneyController extends BaseController
return getDataTable(list);
}
/**
* 导出额度流水(按当前筛选条件,全量导出不分页)
*/
@PreAuthorize("@ss.hasPermi('customersupplier:customerinfo:export')")
@Log(title = "客户额度流水", businessType = BusinessType.EXPORT)
@PostMapping("/credit/flow/export")
public void creditFlowExport(HttpServletResponse response, CustomerCreditFlow query)
{
List<CustomerCreditFlowExportVO> list = Collections.emptyList();
if (query != null && query.getCustomerId() != null && query.getCustomerId() != 0L)
{
list = customerMoneyService.selectCreditFlowList(query).stream()
.map(CustomerCreditFlowExportVO::from).collect(Collectors.toList());
}
ExcelUtil<CustomerCreditFlowExportVO> util = new ExcelUtil<>(CustomerCreditFlowExportVO.class);
util.exportExcel(response, list, "额度流水");
}
/**
* 提交额度调整申请(信用额度/临时额度),申请待审批通过后才真正调整额度
*/
@@ -0,0 +1,49 @@
package com.ruoyi.common.config.serializer;
import java.io.IOException;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
import java.util.Date;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
/**
* 严格解析 yyyy-MM-dd 日期,避免 SimpleDateFormat 的宽松解析把非法日期自动滚动成其他日期。
*/
public class StrictDateDeserializer extends JsonDeserializer<Date>
{
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd")
.withResolverStyle(ResolverStyle.STRICT);
@Override
public Date deserialize(JsonParser parser, DeserializationContext context) throws IOException
{
if (!parser.hasToken(JsonToken.VALUE_STRING))
{
throw InvalidFormatException.from(parser, "日期必须使用 yyyy-MM-dd 格式", parser.getValueAsString(), Date.class);
}
String value = parser.getText();
if (value == null || value.trim().isEmpty())
{
return null;
}
try
{
LocalDate date = LocalDate.parse(value.trim(), FORMATTER);
return Date.from(date.atStartOfDay(ZoneId.systemDefault()).toInstant());
}
catch (DateTimeParseException ex)
{
throw InvalidFormatException.from(parser, "日期必须是有效的 yyyy-MM-dd 日期", value, Date.class);
}
}
}
@@ -6,6 +6,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.validation.BindException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.bind.annotation.ExceptionHandler;
@@ -13,11 +14,9 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import com.ruoyi.common.constant.HttpStatus;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.exception.DemoModeException;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.html.EscapeUtil;
/**
* 全局异常处理器
@@ -81,13 +80,19 @@ public class GlobalExceptionHandler
public AjaxResult handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request)
{
String requestURI = request.getRequestURI();
String value = Convert.toStr(e.getValue());
if (StringUtils.isNotEmpty(value))
{
value = EscapeUtil.clean(value);
}
log.error("请求参数类型不匹配'{}',发生系统异常.", requestURI, e);
return AjaxResult.error(String.format("请求参数类型不匹配,参数[%s]要求类型为:'%s',但输入值为:'%s'", e.getName(), e.getRequiredType().getName(), value));
return AjaxResult.error(String.format("请求参数[%s]格式不正确,请检查输入内容", e.getName()));
}
/**
* 请求体无法解析时只返回用户可处理的提示,避免把 Jackson 或数据库内部信息直接返回前端。
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
public AjaxResult handleHttpMessageNotReadableException(HttpMessageNotReadableException e,
HttpServletRequest request)
{
log.error("请求地址'{}',请求体格式不正确.", request.getRequestURI(), e);
return AjaxResult.error("请求参数格式不正确,请检查日期、金额等字段");
}
/**
@@ -98,7 +103,7 @@ public class GlobalExceptionHandler
{
String requestURI = request.getRequestURI();
log.error("请求地址'{}',发生未知异常.", requestURI, e);
return AjaxResult.error(e.getMessage());
return AjaxResult.error("系统处理异常,请稍后重试");
}
/**
@@ -109,7 +114,7 @@ public class GlobalExceptionHandler
{
String requestURI = request.getRequestURI();
log.error("请求地址'{}',发生系统异常.", requestURI, e);
return AjaxResult.error(e.getMessage());
return AjaxResult.error("系统处理异常,请稍后重试");
}
/**
@@ -119,7 +124,7 @@ public class GlobalExceptionHandler
public AjaxResult handleBindException(BindException e)
{
log.error(e.getMessage(), e);
String message = e.getAllErrors().get(0).getDefaultMessage();
String message = e.getAllErrors().isEmpty() ? "请求参数校验失败" : e.getAllErrors().get(0).getDefaultMessage();
return AjaxResult.error(message);
}
@@ -130,7 +135,8 @@ public class GlobalExceptionHandler
public Object handleMethodArgumentNotValidException(MethodArgumentNotValidException e)
{
log.error(e.getMessage(), e);
String message = e.getBindingResult().getFieldError().getDefaultMessage();
String message = e.getBindingResult().getFieldError() == null
? "请求参数校验失败" : e.getBindingResult().getFieldError().getDefaultMessage();
return AjaxResult.error(message);
}
@@ -21,6 +21,8 @@ public class ArBillDetail implements Serializable {
private Integer detailNo;
private String orderNo;
private String trackingNo;
/** 转单号快照;账单生成时从来源计费订单复制,保证历史账单可追溯。 */
private String transferNo;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date receiveTime;
private String productCode;
@@ -17,9 +17,9 @@ public class ArBillGenerateQuery
private String currency;
/** 订单号或运单号关键词,按 OR 条件模糊查询。 */
/** 订单号、运单号或转单号关键词,按 OR 条件模糊查询。 */
private String orderOrTrackingNo;
/** 全选模式下,排除弹窗使用的附加订单号或运单号关键词。 */
/** 全选模式下,排除弹窗使用的附加订单号、运单号或转单号关键词。 */
private String excludeOrderOrTrackingNo;
}
@@ -3,8 +3,11 @@ package com.ruoyi.ar.domain.vo;
import java.util.Date;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import com.ruoyi.common.config.serializer.StrictDateDeserializer;
import com.ruoyi.ar.enums.ArBillSelectionMode;
import lombok.Data;
@@ -15,11 +18,13 @@ public class ArBillGenerateRequest
@NotNull(message = "客户不能为空")
private Long customerId;
@NotBlank(message = "账单名称不能为空")
@Size(max = 100, message = "账单名称不能超过100个字符")
private String billName;
private String customerCode;
private String customerName;
@NotNull(message = "账单截止日不能为空")
@JsonFormat(pattern = "yyyy-MM-dd")
@JsonDeserialize(using = StrictDateDeserializer.class)
private Date billPeriodEnd;
/** 手动模式选中的来源订单 ID;全选模式为空。 */
private List<String> sourceBillIds;
@@ -28,13 +33,15 @@ public class ArBillGenerateRequest
/** 全选模式中用户明确排除的来源订单 ID。 */
private List<String> excludedSourceBillIds;
@JsonFormat(pattern = "yyyy-MM-dd")
@JsonDeserialize(using = StrictDateDeserializer.class)
private Date billDate;
private String currency = "CNY";
/** 订单号或运单号关键词,生成时用于再次校验筛选条件。 */
/** 订单号、运单号或转单号关键词,生成时用于再次校验筛选条件。 */
private String orderOrTrackingNo;
private String bankAccountName;
private String bankAccountNo;
private String bankName;
@Size(max = 500, message = "备注不能超过500个字符")
private String remark;
/** 前端幂等号;重复提交同一请求时复用原任务。 */
private String requestId;
@@ -90,6 +90,9 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService {
throw new ServiceException("核销金额必须大于0");
}
amount = amount.setScale(MONEY_SCALE, RoundingMode.HALF_UP);
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new ServiceException("核销金额保留两位小数后必须大于0");
}
// 1. 行锁锁定账单(加锁顺序:账单 -> 账户,与反核销保持一致)
ArBill bill = billMapper.selectArBillByIdForUpdate(cmd.getArBillId());
@@ -984,6 +984,7 @@ public class ArBillServiceImpl implements IArBillService {
detail.setDetailNo(detailNo);
detail.setOrderNo(source.getOrderNo());
detail.setTrackingNo(source.getTrackingNo());
detail.setTransferNo(source.getTransferNo());
detail.setReceiveTime(source.getOrderTime());
detail.setProductCode(source.getProductCode());
detail.setProductName(source.getProductName());
@@ -1631,15 +1632,23 @@ public class ArBillServiceImpl implements IArBillService {
}
private void validatePeriod(ArBillGenerateRequest request) {
if (request == null || request.getCustomerId() == null || StringUtils.isEmpty(request.getBillName())
if (request == null || request.getCustomerId() == null || request.getCustomerId() <= 0
|| StringUtils.isEmpty(request.getBillName())
|| request.getBillName().trim().isEmpty()
|| request.getBillPeriodEnd() == null) {
throw new ServiceException("客户、账单名称和账单截止日不能为空");
}
if (request.getBillName().trim().length() > 100) {
throw new ServiceException("账单名称不能超过100个字符");
}
if (StringUtils.isNotEmpty(request.getRemark()) && request.getRemark().trim().length() > 500) {
throw new ServiceException("备注不能超过500个字符");
}
}
private void validatePreviewQuery(ArBillGenerateQuery query) {
if (query == null || query.getCustomerId() == null || query.getBillPeriodEnd() == null) {
if (query == null || query.getCustomerId() == null || query.getCustomerId() <= 0
|| query.getBillPeriodEnd() == null) {
throw new ServiceException("客户和账单截止日不能为空");
}
}
@@ -376,9 +376,10 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
BigDecimal afterBalance = beforeBalance.subtract(changeAmount);
BigDecimal frozenBalance = nz(account.getFrozenBalance());
account.setBalance(afterBalance);
BigDecimal beforeUsedCredit = nz(account.getUsedCredit());
if (Boolean.TRUE.equals(command.getReleaseUsedCredit()))
{
account.setUsedCredit(nz(account.getUsedCredit()).subtract(changeAmount).max(BigDecimal.ZERO)
account.setUsedCredit(beforeUsedCredit.subtract(changeAmount).max(BigDecimal.ZERO)
.setScale(MONEY_SCALE, RoundingMode.HALF_UP));
}
account.setUpdateBy(username);
@@ -390,6 +391,18 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
CustomerBalanceFlow flow = newBalanceFlow(account, command, flowCurrency, originalAmount, -1,
beforeBalance, afterBalance, frozenBalance, frozenBalance, username, userId);
customerMoneyMapper.insertFlow(flow);
// 核销扣款联动释放已用额度:补写额度流水(RELEASE),保证 used_credit 变动台账完整
BigDecimal afterUsedCredit = nz(account.getUsedCredit());
if (Boolean.TRUE.equals(command.getReleaseUsedCredit())
&& afterUsedCredit.compareTo(beforeUsedCredit) < 0)
{
writeUsedCreditFlow(account, toCreditUsageCommand(command, accountCurrency, null),
CustomerCreditFlow.CHANGE_RELEASE, -1,
beforeUsedCredit, afterUsedCredit,
beforeUsedCredit.subtract(afterUsedCredit),
username, userId);
}
return flow;
}
@@ -412,10 +425,11 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
BigDecimal afterBalance = beforeBalance.add(changeAmount);
BigDecimal frozenBalance = nz(account.getFrozenBalance());
account.setBalance(afterBalance);
BigDecimal beforeUsedCredit = nz(account.getUsedCredit());
if (Boolean.TRUE.equals(command.getReoccupyUsedCredit()))
{
// 反核销回补:账单恢复未清偿,授信占用同步加回(允许出现高于总额度的占用存量)
account.setUsedCredit(nz(account.getUsedCredit()).add(changeAmount)
account.setUsedCredit(beforeUsedCredit.add(changeAmount)
.setScale(MONEY_SCALE, RoundingMode.HALF_UP));
}
account.setUpdateBy(username);
@@ -427,11 +441,39 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
CustomerBalanceFlow flow = newBalanceFlow(account, command, flowCurrency, originalAmount, 1,
beforeBalance, afterBalance, frozenBalance, frozenBalance, username, userId);
customerMoneyMapper.insertFlow(flow);
// 反核销回补联动重新占用已用额度:补写额度流水(OCCUPY),与核销释放流水方向相反、可勾稽
BigDecimal afterUsedCredit = nz(account.getUsedCredit());
if (Boolean.TRUE.equals(command.getReoccupyUsedCredit())
&& afterUsedCredit.compareTo(beforeUsedCredit) > 0)
{
String reoccupyReason = "反核销重新占用授信额度"
+ (StringUtils.hasText(command.getBizNo()) ? ":" + command.getBizNo() : "");
writeUsedCreditFlow(account, toCreditUsageCommand(command, accountCurrency, reoccupyReason),
CustomerCreditFlow.CHANGE_OCCUPY, 1,
beforeUsedCredit, afterUsedCredit,
afterUsedCredit.subtract(beforeUsedCredit),
username, userId);
}
return flow;
}
/* ================= 内部实现 ================= */
/** 余额变动命令 -> 额度占用命令(复用业务单据信息,金额取实际联动额由调用方写流水时传入) */
private CreditUsageCommand toCreditUsageCommand(BalanceChangeCommand source, String accountCurrency, String reason)
{
CreditUsageCommand target = new CreditUsageCommand();
target.setCustomerId(source.getCustomerId());
target.setCurrency(accountCurrency);
target.setAmount(money(source.getAmount()));
target.setBizType(source.getBizType());
target.setBizId(source.getBizId());
target.setBizNo(source.getBizNo());
target.setReason(reason);
return target;
}
/** 额度占用/释放写额度流水(before/after 为已用额度快照) */
private CustomerCreditFlow writeUsedCreditFlow(CustomerAccount account, CreditUsageCommand command,
String changeType, int direction, BigDecimal beforeUsed, BigDecimal afterUsed,
@@ -0,0 +1,91 @@
package com.ruoyi.customer.customermoney.vo;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.customer.customermoney.domain.CustomerBalanceFlow;
import lombok.Data;
/**
* 客户余额流水导出 VO(列与页面余额流水 tab 对齐,枚举导出为中文文案)
*
* @author ruoyi
* @date 2026-09-21
*/
@Data
public class CustomerBalanceFlowExportVO implements Serializable
{
private static final long serialVersionUID = 1L;
@Excel(name = "流水号", width = 22)
private String flowNo;
@Excel(name = "时间", width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
@Excel(name = "业务类型", width = 12)
private String bizTypeLabel;
@Excel(name = "收支", width = 8)
private String directionLabel;
@Excel(name = "原币金额", width = 16)
private String amountText;
@Excel(name = "汇率", width = 12)
private String exchangeRate;
@Excel(name = "本位币变动(CNY)", width = 18)
private BigDecimal baseChangeAmount;
@Excel(name = "变动前余额", width = 14)
private BigDecimal beforeBalance;
@Excel(name = "变动后余额", width = 14)
private BigDecimal afterBalance;
@Excel(name = "关联单号", width = 22)
private String bizNo;
@Excel(name = "操作人", width = 12)
private String createBy;
@Excel(name = "备注", width = 30)
private String remark;
public static CustomerBalanceFlowExportVO from(CustomerBalanceFlow f)
{
CustomerBalanceFlowExportVO vo = new CustomerBalanceFlowExportVO();
vo.setFlowNo(f.getFlowNo());
vo.setCreateTime(f.getCreateTime());
vo.setBizTypeLabel(bizTypeLabel(f.getBizType()));
boolean income = f.getDirection() != null && f.getDirection() == 1;
vo.setDirectionLabel(income ? "收入" : "支出");
String amt = f.getAmount() == null ? "" : f.getAmount().stripTrailingZeros().toPlainString();
vo.setAmountText((f.getCurrency() == null ? "" : f.getCurrency() + " ") + amt);
vo.setExchangeRate(f.getExchangeRate() == null ? null : f.getExchangeRate().stripTrailingZeros().toPlainString());
BigDecimal base = f.getBaseAmount() != null ? f.getBaseAmount() : f.getAmount();
// 支出导出为负数(配合“收支”列阅读,同时保持 Excel 数值类型)
vo.setBaseChangeAmount(base == null ? null : (income ? base : base.negate()));
vo.setBeforeBalance(f.getBeforeBalance());
vo.setAfterBalance(f.getAfterBalance());
vo.setBizNo(f.getBizNo());
vo.setCreateBy(f.getCreateBy());
vo.setRemark(f.getRemark());
return vo;
}
private static String bizTypeLabel(String v)
{
if ("RECEIPT".equals(v)) return "收款";
if ("AR_BILL".equals(v)) return "应收账单";
if ("BILL_PAY".equals(v)) return "账单核销";
if ("ORDER".equals(v)) return "计费订单";
if ("REFUND".equals(v)) return "退款";
if ("ADJUST".equals(v)) return "手工调整";
if ("FREEZE".equals(v)) return "冻结";
if ("UNFREEZE".equals(v)) return "解冻";
return v;
}
}
@@ -0,0 +1,117 @@
package com.ruoyi.customer.customermoney.vo;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.customer.customermoney.domain.CustomerCreditFlow;
import lombok.Data;
/**
* 客户额度流水导出 VO(列与页面额度流水 tab 对齐,枚举导出为中文文案)
*
* @author ruoyi
* @date 2026-09-21
*/
@Data
public class CustomerCreditFlowExportVO implements Serializable
{
private static final long serialVersionUID = 1L;
@Excel(name = "流水号", width = 22)
private String flowNo;
@Excel(name = "时间", width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
@Excel(name = "业务单号", width = 22)
private String bizNo;
@Excel(name = "变动类型", width = 12)
private String changeTypeLabel;
@Excel(name = "变动维度", width = 18)
private String dimensionLabel;
@Excel(name = "调整方式", width = 10)
private String adjustModeLabel;
@Excel(name = "方向", width = 8)
private String directionLabel;
@Excel(name = "变动前", width = 12)
private BigDecimal beforeAmount;
@Excel(name = "变动额", width = 12)
private BigDecimal changeAmount;
@Excel(name = "变动后", width = 12)
private BigDecimal afterAmount;
@Excel(name = "已用额度快照", width = 14)
private BigDecimal usedCredit;
@Excel(name = "生效日期", width = 12, dateFormat = "yyyy-MM-dd")
private Date effectiveDate;
@Excel(name = "失效日期", width = 12)
private String expireDateText;
@Excel(name = "调整原因", width = 30)
private String reason;
@Excel(name = "操作人", width = 12)
private String createBy;
@Excel(name = "备注", width = 30)
private String remark;
public static CustomerCreditFlowExportVO from(CustomerCreditFlow f)
{
CustomerCreditFlowExportVO vo = new CustomerCreditFlowExportVO();
vo.setFlowNo(f.getFlowNo());
vo.setCreateTime(f.getCreateTime());
vo.setBizNo(f.getBizNo());
vo.setChangeTypeLabel(changeTypeLabel(f.getChangeType()));
boolean usageFlow = "OCCUPY".equals(f.getChangeType()) || "RELEASE".equals(f.getChangeType());
vo.setDimensionLabel(usageFlow ? "授信占用(已用额度)" : "授信额度");
vo.setAdjustModeLabel(adjustModeLabel(f.getAdjustMode()));
Integer dir = f.getDirection();
vo.setDirectionLabel(dir == null ? "-" : (dir == 1 ? "增加" : "减少"));
vo.setBeforeAmount(f.getBeforeAmount());
// 减少导出为负数(配合“方向”列阅读,同时保持 Excel 数值类型)
BigDecimal change = f.getChangeAmount();
if (change != null && dir != null && dir == -1)
{
change = change.negate();
}
vo.setChangeAmount(change);
vo.setAfterAmount(f.getAfterAmount());
// 占用/释放流水的已用额度快照在页面上不展示
vo.setUsedCredit(usageFlow ? null : f.getUsedCredit());
vo.setEffectiveDate(f.getEffectiveDate());
vo.setExpireDateText(f.getExpireDate() == null ? "长期" : new java.text.SimpleDateFormat("yyyy-MM-dd").format(f.getExpireDate()));
vo.setReason(f.getReason());
vo.setCreateBy(f.getCreateBy());
vo.setRemark(f.getRemark());
return vo;
}
private static String changeTypeLabel(String v)
{
if ("INIT".equals(v)) return "初始化";
if ("ADJUST".equals(v)) return "手工调整";
if ("EXPIRE".equals(v)) return "到期失效";
if ("OCCUPY".equals(v)) return "授信占用";
if ("RELEASE".equals(v)) return "授信释放";
return v;
}
private static String adjustModeLabel(String v)
{
if ("SET".equals(v)) return "设置为";
if ("ADD".equals(v)) return "增加";
if ("SUB".equals(v)) return "减少";
return v;
}
}
@@ -0,0 +1,116 @@
package com.ruoyi.customer.customermoney.vo;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.customer.customermoney.domain.CustomerReceipt;
import lombok.Data;
/**
* 客户收款记录导出 VO(列与页面收款记录 tab 对齐,枚举导出为中文文案)
*
* @author ruoyi
* @date 2026-09-21
*/
@Data
public class CustomerReceiptExportVO implements Serializable
{
private static final long serialVersionUID = 1L;
@Excel(name = "收款单号", width = 22)
private String receiptNo;
@Excel(name = "收款日期", width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date receivedAt;
@Excel(name = "收款类型", width = 12)
private String receiptTypeLabel;
@Excel(name = "币种", width = 8)
private String currency;
@Excel(name = "原币金额", width = 14)
private BigDecimal amount;
@Excel(name = "汇率", width = 12)
private String exchangeRate;
@Excel(name = "汇率来源", width = 10)
private String rateSourceLabel;
@Excel(name = "本位币金额(CNY)", width = 16)
private BigDecimal baseAmount;
@Excel(name = "收款方式", width = 12)
private String payMethodLabel;
@Excel(name = "客户银行账号", width = 24)
private String bankAccountNo;
@Excel(name = "银行/平台流水号", width = 22)
private String bankTransactionNo;
@Excel(name = "状态", width = 10)
private String statusLabel;
@Excel(name = "登记人", width = 12)
private String createBy;
@Excel(name = "备注", width = 30)
private String remark;
public static CustomerReceiptExportVO from(CustomerReceipt r)
{
CustomerReceiptExportVO vo = new CustomerReceiptExportVO();
vo.setReceiptNo(r.getReceiptNo());
vo.setReceivedAt(r.getReceivedAt());
vo.setReceiptTypeLabel(receiptTypeLabel(r.getReceiptType()));
vo.setCurrency(r.getCurrency());
vo.setAmount(r.getAmount());
vo.setExchangeRate(r.getExchangeRate() == null ? null : r.getExchangeRate().stripTrailingZeros().toPlainString());
vo.setRateSourceLabel(rateSourceLabel(r.getRateSource()));
vo.setBaseAmount(r.getBaseAmount());
vo.setPayMethodLabel(payMethodLabel(r.getPayMethod()));
vo.setBankAccountNo(r.getBankAccountNo());
vo.setBankTransactionNo(r.getBankTransactionNo());
vo.setStatusLabel(statusLabel(r.getStatus()));
vo.setCreateBy(r.getCreateBy());
vo.setRemark(r.getRemark());
return vo;
}
private static String receiptTypeLabel(String v)
{
if ("PREPAY".equals(v)) return "预充值";
if ("REPAY".equals(v)) return "还款核销";
if ("MIXED".equals(v)) return "混合";
return v;
}
private static String statusLabel(String v)
{
if ("PENDING".equals(v)) return "待确认";
if ("CONFIRMED".equals(v)) return "已确认";
if ("CANCELLED".equals(v)) return "已取消";
return v;
}
private static String payMethodLabel(String v)
{
if ("BANK_TRANSFER".equals(v)) return "银行转账";
if ("ALIPAY".equals(v)) return "支付宝";
if ("WECHAT".equals(v)) return "微信";
if ("CASH".equals(v)) return "现金";
if ("OTHER".equals(v)) return "其他";
return v;
}
private static String rateSourceLabel(String v)
{
if ("RATE_TABLE".equals(v)) return "汇率表";
if ("MANUAL".equals(v)) return "手工修改";
if ("SYSTEM".equals(v)) return "系统默认";
return v;
}
}
@@ -11,6 +11,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="detailNo" column="detail_no"/>
<result property="orderNo" column="order_no"/>
<result property="trackingNo" column="tracking_no"/>
<result property="transferNo" column="transfer_no"/>
<result property="receiveTime" column="receive_time"/>
<result property="productCode" column="product_code"/>
<result property="productName" column="product_name"/>
@@ -32,7 +33,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap>
<select id="selectByBillId" parameterType="long" resultMap="ArBillDetailResult">
select detail_id, ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, receive_time,
select detail_id, ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, transfer_no, receive_time,
product_code, product_name, goods_description, country_code, country_name, dest_postcode,
actual_weight, volume_weight, charge_weight, pkg_length, pkg_width, pkg_height,
currency, total_amount, remark, create_by, create_time
@@ -40,7 +41,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</select>
<select id="selectByBillIdAndSourceIds" resultMap="ArBillDetailResult">
select detail_id, ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, receive_time,
select detail_id, ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, transfer_no, receive_time,
product_code, product_name, goods_description, country_code, country_name, dest_postcode,
actual_weight, volume_weight, charge_weight, pkg_length, pkg_width, pkg_height,
currency, total_amount, remark, create_by, create_time
@@ -76,7 +77,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap>
<select id="selectByBillIdWithFees" parameterType="long" resultMap="ArBillDetailWithFeesResult">
select d.detail_id, d.ar_bill_id, d.prc_order_bill_id, d.detail_no, d.order_no, d.tracking_no, d.receive_time,
select d.detail_id, d.ar_bill_id, d.prc_order_bill_id, d.detail_no, d.order_no, d.tracking_no, d.transfer_no, d.receive_time,
d.product_code, d.product_name, d.goods_description, d.country_code, d.country_name, d.dest_postcode,
d.actual_weight, d.volume_weight, d.charge_weight, d.pkg_length, d.pkg_width, d.pkg_height,
d.currency, d.total_amount, d.remark, d.create_by, d.create_time,
@@ -94,7 +95,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</select>
<select id="selectExportPage" resultMap="ArBillDetailResult">
select detail_id, ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, receive_time,
select detail_id, ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, transfer_no, receive_time,
product_code, product_name, goods_description, country_code, country_name, dest_postcode,
actual_weight, volume_weight, charge_weight, pkg_length, pkg_width, pkg_height,
currency, total_amount, remark, create_by, create_time
@@ -105,14 +106,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</select>
<insert id="insert" parameterType="ArBillDetail" useGeneratedKeys="true" keyProperty="detailId">
insert into ar_bill_detail (ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, receive_time,
insert into ar_bill_detail (ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, transfer_no, receive_time,
product_code,
product_name, goods_description, country_code, country_name, dest_postcode,
actual_weight,
volume_weight, charge_weight, pkg_length, pkg_width, pkg_height, currency,
total_amount,
remark, create_by, create_time)
values (#{arBillId}, #{prcOrderBillId}, #{detailNo}, #{orderNo}, #{trackingNo}, #{receiveTime},
values (#{arBillId}, #{prcOrderBillId}, #{detailNo}, #{orderNo}, #{trackingNo}, #{transferNo}, #{receiveTime},
#{productCode}, #{productName}, #{goodsDescription}, #{countryCode}, #{countryName},
#{destPostcode}, #{actualWeight}, #{volumeWeight}, #{chargeWeight}, #{pkgLength}, #{pkgWidth},
#{pkgHeight}, #{currency}, #{totalAmount}, #{remark}, #{createBy}, sysdate())
@@ -120,14 +121,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<insert id="insertBatch" parameterType="java.util.List" useGeneratedKeys="true" keyProperty="detailId">
insert into ar_bill_detail (
ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, receive_time, product_code,
ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, transfer_no, receive_time, product_code,
product_name, goods_description, country_code, country_name, dest_postcode, actual_weight,
volume_weight, charge_weight, pkg_length, pkg_width, pkg_height, currency, total_amount,
remark, create_by, create_time
) values
<foreach collection="list" item="item" separator=",">
(#{item.arBillId}, #{item.prcOrderBillId}, #{item.detailNo}, #{item.orderNo}, #{item.trackingNo},
#{item.receiveTime}, #{item.productCode}, #{item.productName}, #{item.goodsDescription},
#{item.transferNo}, #{item.receiveTime}, #{item.productCode}, #{item.productName}, #{item.goodsDescription},
#{item.countryCode}, #{item.countryName}, #{item.destPostcode}, #{item.actualWeight},
#{item.volumeWeight}, #{item.chargeWeight}, #{item.pkgLength}, #{item.pkgWidth}, #{item.pkgHeight},
#{item.currency}, #{item.totalAmount}, #{item.remark}, #{item.createBy}, sysdate())
@@ -225,7 +225,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</select>
<!-- rename_bill_tables.sql 已将需求文档中的 prc_bill 映射为本项目的 prc_order_bill。 -->
<!-- 生成弹窗预览:按客户、截止日、币种和订单/运单号 OR 条件分页查询未入账订单。 -->
<!-- 生成弹窗预览:按客户、截止日、币种和订单号/运单号/转单号 OR 条件分页查询未入账订单。 -->
<select id="selectPrcOrderBillByCustomerPeriod" resultMap="PrcOrderBillResult">
<include refid="selectPrcOrderBillVo"/>
where customer_id = #{customerId}
@@ -234,11 +234,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</if>
<if test="orderOrTrackingNo != null and orderOrTrackingNo != ''">
and (order_no like concat('%', #{orderOrTrackingNo}, '%')
or tracking_no like concat('%', #{orderOrTrackingNo}, '%'))
or tracking_no like concat('%', #{orderOrTrackingNo}, '%')
or transfer_no like concat('%', #{orderOrTrackingNo}, '%'))
</if>
<if test="excludeOrderOrTrackingNo != null and excludeOrderOrTrackingNo != ''">
and (order_no like concat('%', #{excludeOrderOrTrackingNo}, '%')
or tracking_no like concat('%', #{excludeOrderOrTrackingNo}, '%'))
or tracking_no like concat('%', #{excludeOrderOrTrackingNo}, '%')
or transfer_no like concat('%', #{excludeOrderOrTrackingNo}, '%'))
</if>
and biz_date &lt;= #{cutoffDate}
and ar_bill_status = #{arBillStatus}
@@ -263,11 +265,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</if>
<if test="orderOrTrackingNo != null and orderOrTrackingNo != ''">
and (order_no like concat('%', #{orderOrTrackingNo}, '%')
or tracking_no like concat('%', #{orderOrTrackingNo}, '%'))
or tracking_no like concat('%', #{orderOrTrackingNo}, '%')
or transfer_no like concat('%', #{orderOrTrackingNo}, '%'))
</if>
<if test="excludeOrderOrTrackingNo != null and excludeOrderOrTrackingNo != ''">
and (order_no like concat('%', #{excludeOrderOrTrackingNo}, '%')
or tracking_no like concat('%', #{excludeOrderOrTrackingNo}, '%'))
or tracking_no like concat('%', #{excludeOrderOrTrackingNo}, '%')
or transfer_no like concat('%', #{excludeOrderOrTrackingNo}, '%'))
</if>
and biz_date &lt;= #{cutoffDate}
and ar_bill_status = #{arBillStatus}
@@ -283,7 +287,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</if>
<if test="orderOrTrackingNo != null and orderOrTrackingNo != ''">
and (order_no like concat('%', #{orderOrTrackingNo}, '%')
or tracking_no like concat('%', #{orderOrTrackingNo}, '%'))
or tracking_no like concat('%', #{orderOrTrackingNo}, '%')
or transfer_no like concat('%', #{orderOrTrackingNo}, '%'))
</if>
and biz_date &lt;= #{cutoffDate}
and ar_bill_status = #{arBillStatus}
@@ -339,7 +344,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</if>
<if test="orderOrTrackingNo != null and orderOrTrackingNo != ''">
and (order_no like concat('%', #{orderOrTrackingNo}, '%')
or tracking_no like concat('%', #{orderOrTrackingNo}, '%'))
or tracking_no like concat('%', #{orderOrTrackingNo}, '%')
or transfer_no like concat('%', #{orderOrTrackingNo}, '%'))
</if>
<if test="currency != null and currency != ''">
and currency = #{currency}
+2
View File
@@ -76,6 +76,7 @@ CREATE TABLE IF NOT EXISTS ar_bill_detail (
detail_no INT(11) NOT NULL COMMENT '账单内顺序号',
order_no VARCHAR(64) NOT NULL COMMENT '订单号快照',
tracking_no VARCHAR(64) DEFAULT NULL COMMENT '运单号快照',
transfer_no VARCHAR(64) DEFAULT NULL COMMENT '转单号快照',
receive_time DATETIME DEFAULT NULL COMMENT '收件或入库时间',
product_code VARCHAR(50) NOT NULL COMMENT '产品代码快照',
product_name VARCHAR(100) DEFAULT NULL COMMENT '产品名称快照',
@@ -98,6 +99,7 @@ CREATE TABLE IF NOT EXISTS ar_bill_detail (
UNIQUE KEY uk_ar_bill_detail_source (ar_bill_id, prc_order_bill_id),
UNIQUE KEY uk_ar_bill_detail_no (ar_bill_id, detail_no),
KEY idx_ar_bill_detail_source (prc_order_bill_id),
KEY idx_ar_bill_detail_bill_transfer_no (ar_bill_id, transfer_no),
CONSTRAINT fk_ar_bill_detail_bill FOREIGN KEY (ar_bill_id) REFERENCES ar_bill (ar_bill_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='应收账单订单快照';
@@ -0,0 +1,49 @@
-- 应收账单转单号快照迁移
-- 适用于已经执行过 ar/ar_bill.sql 的存量数据库。
-- 本脚本可重复执行,不删除账单明细,不覆盖已经存在的转单号快照。
SET NAMES utf8mb4;
SET @db_name := DATABASE();
-- 1. 账单明细保存生成账单时的转单号快照。
SET @has_transfer_no := (
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db_name
AND TABLE_NAME = 'ar_bill_detail'
AND COLUMN_NAME = 'transfer_no'
);
SET @sql := IF(@has_transfer_no = 0,
'ALTER TABLE ar_bill_detail ADD COLUMN transfer_no VARCHAR(64) DEFAULT NULL COMMENT ''转单号快照'' AFTER tracking_no',
'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 2. 详情按账单查询转单号时使用组合索引。
SET @has_transfer_index := (
SELECT COUNT(*)
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db_name
AND TABLE_NAME = 'ar_bill_detail'
AND INDEX_NAME = 'idx_ar_bill_detail_bill_transfer_no'
);
SET @sql := IF(@has_transfer_index = 0,
'ALTER TABLE ar_bill_detail ADD KEY idx_ar_bill_detail_bill_transfer_no (ar_bill_id, transfer_no)',
'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 3. 历史数据尽力回填,仅填充空值,不覆盖账单生成时已经保存的快照。
-- 如果来源订单后续修改过转单号,回填值仅代表当前来源表值,不能视为严格历史值。
UPDATE ar_bill_detail d
JOIN prc_order_bill p ON p.bill_id = d.prc_order_bill_id
SET d.transfer_no = p.transfer_no
WHERE d.transfer_no IS NULL
AND p.transfer_no IS NOT NULL;
-- 4. 执行后核验字段和回填数量。
SELECT
COUNT(*) AS detail_count,
SUM(CASE WHEN transfer_no IS NOT NULL AND TRIM(transfer_no) <> '' THEN 1 ELSE 0 END) AS transfer_no_count
FROM ar_bill_detail;
@@ -0,0 +1,99 @@
-- ================================================================
-- BMS 应收账单导出模板恢复脚本
--
-- 用途:恢复应收账单导出功能使用的内置默认模板配置。
-- 特点:
-- 1. 可重复执行,不删除现有模板数据。
-- 2. 模板表不存在时自动创建;模板记录已存在时不会重复插入。
-- 3. storage_key 必须与后端 resources 下的模板路径一致。
-- ================================================================
SET NAMES utf8mb4;
-- 1. 创建导出模板表(表已存在时不会重复创建)
CREATE TABLE IF NOT EXISTS ar_export_template (
template_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '导出模板ID',
template_code VARCHAR(50) NOT NULL COMMENT '模板编码,例如 AR_CUSTOMER_BILL',
template_name VARCHAR(100) NOT NULL COMMENT '模板名称',
business_type VARCHAR(50) NOT NULL COMMENT '业务类型,例如 AR_BILL',
template_version VARCHAR(50) NOT NULL COMMENT '模板版本,已启用版本不可覆盖',
storage_type VARCHAR(20) NOT NULL DEFAULT 'CLASSPATH' COMMENT '存储类型:CLASSPATH=应用内置资源,FILE=服务器文件,OSS=对象存储,MINIO=MinIO对象存储',
storage_key VARCHAR(500) NOT NULL COMMENT '模板文件存储Key或classpath资源路径',
file_hash VARCHAR(128) DEFAULT NULL COMMENT '模板文件SHA-256校验值',
status VARCHAR(20) NOT NULL DEFAULT 'ENABLED' COMMENT '状态:ENABLED=启用,DISABLED=停用',
is_default CHAR(1) NOT NULL DEFAULT '0' COMMENT '是否默认模板:1是0否',
effective_start DATE DEFAULT NULL COMMENT '生效开始日期',
effective_end DATE DEFAULT NULL COMMENT '生效结束日期',
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
create_by VARCHAR(64) NOT NULL COMMENT '创建人',
create_time DATETIME NOT NULL COMMENT '创建时间',
update_by VARCHAR(64) DEFAULT NULL COMMENT '更新人',
update_time DATETIME DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (template_id),
UNIQUE KEY uk_ar_export_template_code_version (business_type, template_code, template_version),
KEY idx_ar_export_template_default (business_type, status, is_default, effective_start, effective_end)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='应收账单导出模板管理';
-- 2. 恢复系统内置的应收账单默认模板
-- 模板文件位置:
-- bms-back/ruoyi-system/src/main/resources/templates/ar/ar_bill_template.xlsx
INSERT INTO ar_export_template (
template_code,
template_name,
business_type,
template_version,
storage_type,
storage_key,
file_hash,
status,
is_default,
effective_start,
effective_end,
remark,
create_by,
create_time,
update_by,
update_time
)
SELECT
'AR_CUSTOMER_BILL',
'应收账单默认模板',
'AR_BILL',
'1.0',
'CLASSPATH',
'templates/ar/ar_bill_template.xlsx',
'735002ac3561e0579b724c0612a2c5ce2d6725bc7fd39c450b0aeff70911a45e',
'ENABLED',
'1',
NULL,
NULL,
'系统内置默认模板',
'system',
NOW(),
'system',
NOW()
FROM DUAL
WHERE NOT EXISTS (
SELECT 1
FROM ar_export_template
WHERE business_type = 'AR_BILL'
AND template_code = 'AR_CUSTOMER_BILL'
AND template_version = '1.0'
);
-- 3. 执行后核验:应返回 1 条启用的默认模板记录
SELECT
template_id,
template_code,
template_name,
business_type,
template_version,
storage_type,
storage_key,
status,
is_default,
file_hash
FROM ar_export_template
WHERE business_type = 'AR_BILL'
AND template_code = 'AR_CUSTOMER_BILL'
AND template_version = '1.0';