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';
@@ -365,6 +365,10 @@ async function getList() {
total.value = res?.total || 0
} catch (e) {
console.error('BasicTable 加载失败', e)
// 查询失败时清空旧结果,避免用户误以为旧列表是本次查询结果。
tableData.value = []
total.value = 0
selectionList.value = []
} finally {
loading.value = false
}
@@ -0,0 +1,74 @@
<template>
<el-popover :width="240" placement="bottom-end" trigger="click" popper-class="column-settings-popper">
<template #reference>
<el-button circle plain icon="Operation" title="列设置" />
</template>
<div class="column-settings">
<div class="column-settings__header">
<span class="column-settings__title">列展示设置</span>
<el-button link type="primary" size="small" @click="emit('reset')">恢复默认</el-button>
</div>
<el-checkbox-group :model-value="checkedKeys" @change="onChange" class="column-settings__body">
<el-checkbox
v-for="c in columns"
:key="c.key"
:value="c.key"
:disabled="c.fixed"
>{{ c.label }}</el-checkbox>
</el-checkbox-group>
</div>
</el-popover>
</template>
<script setup name="ColumnSettings">
/**
* 表格列设置弹窗(与 useColumnSettings 配合):
* <column-settings :columns="columnDefs" :visible-map="colVisible" @toggle="toggleColumn" @reset="resetColumns" />
* 注意:reference 不要嵌套 el-tooltip——两个 popper 组件嵌套会导致引用解析异常。
*/
const props = defineProps({
columns: { type: Array, default: () => [] },
visibleMap: { type: Object, required: true }
})
const emit = defineEmits(['toggle', 'reset'])
const checkedKeys = computed(() =>
props.columns.filter(c => props.visibleMap[c.key] !== false).map(c => c.key)
)
/** el-checkbox-group 回传当前全量勾选值,逐列 diff 后通知父级修改 */
function onChange(keys) {
props.columns.forEach(c => {
if (c.fixed) return
const next = keys.includes(c.key)
if ((props.visibleMap[c.key] !== false) !== next) {
emit('toggle', c.key, next)
}
})
}
</script>
<style scoped>
.column-settings__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid var(--el-border-color-lighter);
}
.column-settings__title {
font-weight: 600;
font-size: 13px;
}
.column-settings__body {
display: flex;
flex-direction: column;
max-height: 320px;
overflow-y: auto;
}
.column-settings__body :deep(.el-checkbox) {
margin-right: 0;
height: 28px;
}
</style>
+3
View File
@@ -57,6 +57,8 @@ import UserName from '@/components/UserName'
import ProductSelect from '@/components/ProductSelect'
// 多值输入组件(一个字段搜索多个值,如订单号批量搜索)
import MultiValueInput from '@/components/MultiValueInput'
// 表格列设置组件(自定义列显隐)
import ColumnSettings from '@/components/ColumnSettings'
const app = createApp(App)
@@ -87,6 +89,7 @@ app.component('UserSelect', UserSelect)
app.component('UserName', UserName)
app.component('ProductSelect', ProductSelect)
app.component('MultiValueInput', MultiValueInput)
app.component('ColumnSettings', ColumnSettings)
app.use(router)
app.use(store)
+15
View File
@@ -200,6 +200,21 @@ export const dynamicRoutes = [
meta: { title: '供应商编辑', activeMenu: '/customersuppliermanagement/supplierinfo' }
}
]
},
{
// 兼容旧入口 /ar/bill/log;正式菜单路由由数据库菜单生成,当前菜单路径为 /ar/log。
path: '/ar/bill/log',
component: Layout,
hidden: true,
permissions: ['ar:log:list'],
children: [
{
path: '',
component: () => import('@/views/ar/bill/log.vue'),
name: 'ArBillLogCompat',
meta: { title: '应收日志', activeMenu: '/ar/log' }
}
]
}
]
+57
View File
@@ -0,0 +1,57 @@
/**
* 表格列显隐偏好(localStorage 持久化)。
*
* @param {string} storageKey 存储键(页面级唯一,如 'customer-list')
* @param {Array<{key:string,label:string,fixed?:boolean,defaultVisible?:boolean}>} columns 列定义
* @returns {{colVisible: object, isColVisible: Function, resetColumns: Function}}
* colVisible 响应式显隐映射,模板中 v-if="isColVisible('xxx')"
* resetColumns 恢复默认(供列设置弹窗调用)
*/
const PREFIX = 'bms:columnSettings:'
export function useColumnSettings(storageKey, columns) {
const defaults = {}
columns.forEach(c => {
defaults[c.key] = c.defaultVisible !== false
})
let saved = {}
try {
saved = JSON.parse(localStorage.getItem(PREFIX + storageKey) || '{}') || {}
} catch (e) {
saved = {}
}
// 仅接受当前列定义中存在的 key(兼容历史存储与列增减)
const colVisible = reactive({ ...defaults })
columns.forEach(c => {
if (Object.prototype.hasOwnProperty.call(saved, c.key)) {
colVisible[c.key] = !!saved[c.key]
}
// 固定列强制可见
if (c.fixed) colVisible[c.key] = true
})
watch(colVisible, val => {
localStorage.setItem(PREFIX + storageKey, JSON.stringify(val))
}, { deep: true })
function isColVisible(key) {
return colVisible[key] !== false
}
/** 单列显隐切换(供 ColumnSettings 组件 @toggle 调用) */
function toggleColumn(key, visible) {
if (Object.prototype.hasOwnProperty.call(colVisible, key) || columns.some(c => c.key === key)) {
colVisible[key] = !!visible
}
}
function resetColumns() {
columns.forEach(c => {
colVisible[c.key] = c.fixed ? true : c.defaultVisible !== false
})
}
return { colVisible, isColVisible, toggleColumn, resetColumns }
}
@@ -1,5 +1,5 @@
<template>
<el-drawer :model-value="modelValue" :title="detail?.billNo || '应收账单详情'" size="1000px" append-to-body destroy-on-close @update:model-value="$emit('update:modelValue', $event)">
<el-drawer class="bill-detail-drawer" :model-value="modelValue" :title="detail?.billNo || '应收账单详情'" size="min(1000px, 100vw)" append-to-body destroy-on-close @update:model-value="$emit('update:modelValue', $event)">
<div v-loading="loading">
<el-empty v-if="!detail" description="暂无详情" />
<template v-else>
@@ -37,6 +37,7 @@
<el-table-column prop="detailNo" label="#" width="55" align="center" />
<el-table-column prop="orderNo" label="订单号" min-width="180" show-overflow-tooltip />
<el-table-column prop="trackingNo" label="运单号" min-width="160" show-overflow-tooltip />
<el-table-column prop="transferNo" label="转单号" min-width="160" show-overflow-tooltip />
<el-table-column label="产品" min-width="150"><template #default="{ row }">{{ row.productName || row.productCode || '-' }}</template></el-table-column>
<el-table-column label="目的地" min-width="120"><template #default="{ row }">{{ row.countryName || row.countryCode || '-' }}</template></el-table-column>
<el-table-column label="重量/尺寸" min-width="170"><template #default="{ row }">{{ row.chargeWeight || '-' }} kg / {{ dimensions(row) }}</template></el-table-column>
@@ -139,6 +140,7 @@ function mask(value) { if (!value) return '-'; return value.length <= 4 ? '****'
</script>
<style scoped>
:deep(.bill-detail-drawer) { max-width: 100vw; }
.summary { margin-bottom: 18px; }
.total { color: #f56c6c; font-size: 16px; font-weight: 600; }
.section-title { display: flex; align-items: center; gap: 12px; margin: 18px 0 10px; font-weight: 600; }
@@ -6,7 +6,7 @@
<el-col :span="8"><el-form-item label="客户" prop="customerId"><customer-select v-model="generateForm.customerId" placeholder="请选择客户" @change="handleCustomerChange" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="币种" prop="currency"><el-select v-model="generateForm.currency" placeholder="请选择币种" style="width: 100%;" @change="handleCurrencyChange"><el-option label="人民币 CNY" value="CNY" /><el-option label="美元 USD" value="USD" /></el-select></el-form-item></el-col>
<el-col :span="8"><el-form-item label="账单截止日" prop="cutoffDate"><el-date-picker v-model="generateForm.cutoffDate" type="date" value-format="YYYY-MM-DD" placeholder="请选择账单截止日" style="width: 100%;" @change="resetPreview" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="订单/运单号"><el-input v-model="generateForm.orderOrTrackingNo" placeholder="请输入订单号或运单号" clearable /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="单号"><el-input v-model="generateForm.orderOrTrackingNo" placeholder="请输入订单号、运单号或转单号" clearable /></el-form-item></el-col>
</el-row>
</el-form>
<div class="payee-card" v-loading="bankAccountLoading">
@@ -60,6 +60,7 @@
<el-table-column type="selection" width="50" fixed="left" :selectable="isMainRowSelectable" />
<el-table-column prop="orderNo" label="订单号" min-width="150" show-overflow-tooltip />
<el-table-column prop="trackingNo" label="运单号" min-width="140" show-overflow-tooltip />
<el-table-column prop="transferNo" label="转单号" min-width="140" show-overflow-tooltip />
<el-table-column prop="customerName" label="客户" min-width="130" show-overflow-tooltip />
<el-table-column prop="bizDate" label="业务日期" width="105" :formatter="formatDateColumn" />
<el-table-column prop="productName" label="产品" min-width="140" show-overflow-tooltip />
@@ -87,13 +88,14 @@
<el-dialog v-model="excludeDialogOpen" title="选择排除订单" width="1100px" append-to-body destroy-on-close @close="handleExcludeDialogClose">
<div class="exclude-toolbar">
<el-input v-model="excludeKeyword" clearable placeholder="请输入订单号或运单号" class="exclude-search-input" />
<el-input v-model="excludeKeyword" clearable placeholder="请输入订单号、运单号或转单号" class="exclude-search-input" />
<el-button type="primary" :loading="excludeLoading" @click="queryExcludeRows">查询</el-button>
</div>
<el-table ref="excludeTableRef" v-loading="excludeLoading" :data="excludeRowsPage" row-key="billId" border stripe max-height="420" empty-text="暂无符合条件的订单" @selection-change="handleExcludeSelectionChange">
<el-table-column type="selection" width="50" fixed="left" />
<el-table-column prop="orderNo" label="订单号" min-width="220" show-overflow-tooltip />
<el-table-column prop="trackingNo" label="运单号" min-width="180" show-overflow-tooltip />
<el-table-column prop="transferNo" label="转单号" min-width="180" show-overflow-tooltip />
<el-table-column prop="customerName" label="客户" min-width="150" show-overflow-tooltip />
<el-table-column prop="bizDate" label="业务日期" width="120" :formatter="formatDateColumn" />
<el-table-column prop="incomeTotal" label="应收金额" width="130" align="right" :formatter="formatNumberColumn" />
@@ -110,6 +110,8 @@
<el-button icon="Refresh" @click="resetReceiptQuery">重置</el-button>
<el-button type="success" icon="Plus" @click="handleAddReceipt"
v-hasPermi="['customersupplier:customerinfo:add']">登记收款</el-button>
<el-button type="warning" icon="Download" @click="handleExportReceipt"
v-hasPermi="['customersupplier:customerinfo:export']">导出</el-button>
</el-form-item>
</el-form>
@@ -220,6 +222,8 @@
<el-form-item>
<el-button type="primary" icon="Search" @click="getFlowList">搜索</el-button>
<el-button icon="Refresh" @click="resetFlowQuery">重置</el-button>
<el-button type="warning" icon="Download" @click="handleExportFlow"
v-hasPermi="['customersupplier:customerinfo:export']">导出</el-button>
</el-form-item>
</el-form>
@@ -296,6 +300,8 @@
<el-form-item>
<el-button type="primary" icon="Search" @click="getCreditFlowList">搜索</el-button>
<el-button icon="Refresh" @click="resetCreditFlowQuery">重置</el-button>
<el-button type="warning" icon="Download" @click="handleExportCreditFlow"
v-hasPermi="['customersupplier:customerinfo:export']">导出</el-button>
</el-form-item>
</el-form>
@@ -918,7 +924,7 @@
</el-dialog>
<!-- ====== 余额核销账单弹窗 ====== -->
<el-dialog title="余额核销账单" v-model="woDialog.visible" width="560px" append-to-body>
<el-dialog title="余额核销账单" v-model="woDialog.visible" width="560px" append-to-body v-loading="woDialog.accountLoading">
<el-descriptions :column="1" border size="small" v-if="woDialog.bill">
<el-descriptions-item label="账单号">{{ woDialog.bill.billNo }}</el-descriptions-item>
<el-descriptions-item label="账单名称">{{ woDialog.bill.billName || '-' }}</el-descriptions-item>
@@ -931,13 +937,12 @@
</el-descriptions>
<el-form :model="woDialog.form" label-width="110px" class="wo-form">
<el-form-item label="本次核销金额" required>
<el-input-number
<el-input
v-model="woDialog.form.writeoffAmount"
:precision="2"
:step="100"
:min="0.01"
controls-position="right"
inputmode="decimal"
clearable
style="width: 220px;"
placeholder="请输入核销金额"
/>
<span class="wo-form-unit">CNY</span>
</el-form-item>
@@ -1356,6 +1361,39 @@ function resetReceiptQuery() {
getReceiptList()
}
/** 导出收款记录(按当前筛选,全量导出) */
function handleExportReceipt() {
const cid = route.query.customerId
if (!cid) return
const params = proxy.addDateRange({ ...receiptQuery }, receiptDateRange.value)
delete params.pageNum
delete params.pageSize
proxy.download('customersupplier/customermoney/receipt/export', params,
`收款记录_${new Date().getTime()}.xlsx`)
}
/** 导出余额流水(按当前筛选,全量导出) */
function handleExportFlow() {
const cid = route.query.customerId
if (!cid) return
const params = proxy.addDateRange({ ...flowQuery }, flowDateRange.value)
delete params.pageNum
delete params.pageSize
proxy.download('customersupplier/customermoney/flow/export', params,
`余额流水_${new Date().getTime()}.xlsx`)
}
/** 导出额度流水(按当前筛选,全量导出) */
function handleExportCreditFlow() {
const cid = route.query.customerId
if (!cid) return
const params = proxy.addDateRange({ ...creditFlowQuery }, creditFlowDateRange.value)
delete params.pageNum
delete params.pageSize
proxy.download('customersupplier/customermoney/credit/flow/export', params,
`额度流水_${new Date().getTime()}.xlsx`)
}
/** 审核通过 */
function handleConfirmReceipt(row) {
proxy.$modal.confirm(`是否确认审核通过收款单「${row.receiptNo}」?审核通过后金额将计入客户余额。`).then(() => {
@@ -1812,7 +1850,8 @@ function loadBillWriteoffData() {
/* ---- 核销弹窗 ---- */
const woSubmitLoading = ref(false)
const woDialog = reactive({ visible: false, bill: null, form: {} })
const woDialog = reactive({ visible: false, bill: null, form: {}, accountLoading: false })
let writeoffAccountRequestSerial = 0
/** 核销后余额预览(不为负则可提交) */
const woAfterBalance = computed(() => {
const balance = Number(currentAccount.value.balance) || 0
@@ -1820,27 +1859,53 @@ const woAfterBalance = computed(() => {
return formatMoney(balance - amount)
})
function openWriteoffDialog(row) {
// 先刷新账户余额,避免用过期余额核销
listAccount(route.query.customerId).then(res => {
accountList.value = res.data || []
})
async function openWriteoffDialog(row) {
const customerId = Number(route.query.customerId)
if (!customerId) {
proxy.$modal.msgError('缺少客户ID,无法发起核销')
return
}
woDialog.bill = row
const balance = Number(currentAccount.value.balance) || 0
const unwritten = Number(row.unwrittenAmount) || 0
woDialog.form = {
arBillId: row.arBillId,
customerId: Number(route.query.customerId),
writeoffAmount: Math.min(unwritten, balance > 0 ? balance : unwritten),
customerId,
// 余额接口返回前先显示 0,避免使用旧余额或在余额为 0 时错误带出整笔未核销金额。
writeoffAmount: 0,
writeoffDate: proxy.parseTime(new Date(), '{y}-{m}-{d}'),
remark: ''
}
woDialog.visible = true
// 必须等待余额刷新完成后再计算默认金额,避免异步请求尚未返回就读取旧余额。
const requestSerial = ++writeoffAccountRequestSerial
woDialog.accountLoading = true
try {
const res = await listAccount(customerId)
if (requestSerial !== writeoffAccountRequestSerial) return
accountList.value = res.data || []
const balance = Math.max(0, Number(currentAccount.value.balance) || 0)
woDialog.form.writeoffAmount = Math.min(unwritten, balance)
} catch (e) {
if (requestSerial !== writeoffAccountRequestSerial) return
woDialog.form.writeoffAmount = 0
proxy.$modal.msgError('账户余额刷新失败,请稍后重试')
} finally {
if (requestSerial === writeoffAccountRequestSerial) woDialog.accountLoading = false
}
}
function submitWriteoff() {
if (woDialog.accountLoading) {
proxy.$modal.msgWarning('正在刷新账户余额,请稍候')
return
}
const bill = woDialog.bill
const amount = Number(woDialog.form.writeoffAmount)
const rawAmount = typeof woDialog.form.writeoffAmount === 'string'
? woDialog.form.writeoffAmount.trim()
: woDialog.form.writeoffAmount
const amount = Number(rawAmount)
if (!(amount > 0)) {
proxy.$modal.msgError('核销金额必须大于0')
return
@@ -115,49 +115,50 @@
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
<column-settings :columns="columnDefs" :visible-map="colVisible" @toggle="toggleColumn" @reset="resetColumns" />
</el-row>
<el-table v-loading="loading" :data="customerList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table v-loading="loading" border :data="customerList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" fixed="left" />
<!-- <el-table-column label="客户ID" align="center" prop="customerId" />-->
<el-table-column label="客户编码" align="center" prop="customerCode" />
<el-table-column label="客户名称" align="center" prop="customerName" />
<el-table-column label="对外代码" align="center" prop="customerCodeDw" />
<el-table-column label="统一社会信用代码" align="center" prop="creditCode" />
<el-table-column label="业务类型" align="center" prop="customerType">
<el-table-column v-if="isColVisible('customerCode')" label="客户编码" align="center" prop="customerCode" />
<el-table-column v-if="isColVisible('customerName')" label="客户名称" align="center" prop="customerName" :show-overflow-tooltip="true" />
<el-table-column v-if="isColVisible('customerCodeDw')" label="对外代码" align="center" prop="customerCodeDw" />
<el-table-column v-if="isColVisible('creditCode')" label="统一社会信用代码" align="center" prop="creditCode" />
<el-table-column v-if="isColVisible('customerType')" label="业务类型" align="center" prop="customerType">
<template #default="scope">
<dict-tag :options="customer_type" :value="scope.row.customerType" />
</template>
</el-table-column>
<el-table-column label="启用状态" align="center" prop="isEnable">
<el-table-column v-if="isColVisible('isEnable')" label="启用状态" align="center" prop="isEnable">
<template #default="scope">
<dict-tag :options="sys_normal_disable" :value="scope.row.isEnable" />
</template>
</el-table-column>
<el-table-column label="业务员" align="center" prop="salesUserId">
<el-table-column v-if="isColVisible('salesUserId')" label="业务员" align="center" prop="salesUserId">
<template #default="scope"><user-name :user-id="scope.row.salesUserId" /></template>
</el-table-column>
<el-table-column label="资金额度" align="center" prop="ed" />
<el-table-column label="回款账期" align="center" prop="zq" />
<el-table-column label="结算员" align="center" prop="settlementUserId">
<el-table-column v-if="isColVisible('ed')" label="资金额度" align="center" prop="ed" />
<el-table-column v-if="isColVisible('zq')" label="回款账期" align="center" prop="zq" />
<el-table-column v-if="isColVisible('settlementUserId')" label="结算员" align="center" prop="settlementUserId">
<template #default="scope"><user-name :user-id="scope.row.settlementUserId" /></template>
</el-table-column>
<el-table-column label="客服" align="center" prop="serviceUserId">
<el-table-column v-if="isColVisible('serviceUserId')" label="客服" align="center" prop="serviceUserId">
<template #default="scope"><user-name :user-id="scope.row.serviceUserId" /></template>
</el-table-column>
<el-table-column label="结算方式" align="center" prop="settlementType">
<el-table-column v-if="isColVisible('settlementType')" label="结算方式" align="center" prop="settlementType">
<template #default="scope">
<dict-tag :options="settlement_type" :value="scope.row.settlementType" />
</template>
</el-table-column>
<el-table-column label="临时额度" align="center" prop="tempEd" />
<el-table-column label="账户余额(CNY)" align="center" prop="accountBalance" width="130">
<el-table-column v-if="isColVisible('tempEd')" label="临时额度" align="center" prop="tempEd" />
<el-table-column v-if="isColVisible('accountBalance')" 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="weightThreshold" />
<el-table-column label="抛比" align="center" prop="throwRatio" />
<el-table-column label="信用额度" align="center" prop="creditEd">
<el-table-column v-if="isColVisible('weightThreshold')" label="重量阈值" align="center" prop="weightThreshold" />
<el-table-column v-if="isColVisible('throwRatio')" label="抛比" align="center" prop="throwRatio" />
<el-table-column v-if="isColVisible('creditEd')" label="信用额度" align="center" prop="creditEd">
<template #default="scope">
<span v-if="Number(scope.row.creditEd) < 0" style="color: var(--el-color-danger); font-weight: 600;"
title="已用额度超过授信总额(如临时额度已到期),客户回款核销后恢复">
@@ -166,7 +167,7 @@
<span v-else>{{ scope.row.creditEd }}</span>
</template>
</el-table-column>
<el-table-column label="绑定产品" align="center" prop="productIds" min-width="180">
<el-table-column v-if="isColVisible('productIds')" label="绑定产品" align="center" prop="productIds" min-width="180">
<template #default="scope">
<span v-if="productNames(scope.row.productIds)" :title="productNames(scope.row.productIds)">
{{ productNames(scope.row.productIds) }}
@@ -174,8 +175,8 @@
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="备注说明" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="240">
<el-table-column v-if="isColVisible('remark')" label="备注说明" align="center" prop="remark" :show-overflow-tooltip="true" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="240" fixed="right">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['customersupplier:customerinfo:edit']">修改</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['customersupplier:customerinfo:remove']">删除</el-button>
@@ -198,9 +199,34 @@
<script setup name="Customer">
import { listCustomer, delCustomer } from "@/api/customersuppliermanagement/customerinfo/customer.js"
import { useProducts, parseProductIds } from '@/components/ProductSelect/useProducts'
import { useColumnSettings } from '@/utils/columnSettings'
const { proxy } = getCurrentInstance()
/** 可自定义列(默认全部可见;选择列固定、操作列固定右侧不参与设置) */
const columnDefs = [
{ key: 'customerCode', label: '客户编码' },
{ key: 'customerName', label: '客户名称' },
{ key: 'customerCodeDw', label: '对外代码' },
{ key: 'creditCode', label: '统一社会信用代码' },
{ key: 'customerType', label: '业务类型' },
{ key: 'isEnable', label: '启用状态' },
{ key: 'salesUserId', label: '业务员' },
{ key: 'ed', label: '资金额度' },
{ key: 'zq', label: '回款账期' },
{ key: 'settlementUserId', label: '结算员' },
{ key: 'serviceUserId', label: '客服' },
{ key: 'settlementType', label: '结算方式' },
{ key: 'tempEd', label: '临时额度' },
{ key: 'accountBalance', label: '账户余额(CNY)' },
{ key: 'weightThreshold', label: '重量阈值' },
{ key: 'throwRatio', label: '抛比' },
{ key: 'creditEd', label: '信用额度' },
{ key: 'productIds', label: '绑定产品' },
{ key: 'remark', label: '备注说明' }
]
const { colVisible, isColVisible, toggleColumn, resetColumns } = useColumnSettings('customer-info-list', columnDefs)
const customerList = ref([])
const loading = ref(true)
const showSearch = ref(true)
@@ -96,53 +96,54 @@
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
<column-settings :columns="columnDefs" :visible-map="colVisible" @toggle="toggleColumn" @reset="resetColumns" />
</el-row>
<el-table v-loading="loading" :data="supplierList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="供应商编码" align="center" prop="supplierCode" />
<el-table-column label="供应商名称" align="center" prop="supplierName" />
<el-table-column label="对外代码" align="center" prop="supplierCodeDw" />
<el-table-column label="统一社会信用代码" align="center" prop="creditCode" />
<el-table-column label="供应商类型" align="center" prop="supplierType">
<el-table v-loading="loading" border :data="supplierList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" fixed="left" />
<el-table-column v-if="isColVisible('supplierCode')" label="供应商编码" align="center" prop="supplierCode" />
<el-table-column v-if="isColVisible('supplierName')" label="供应商名称" align="center" prop="supplierName" :show-overflow-tooltip="true" />
<el-table-column v-if="isColVisible('supplierCodeDw')" label="对外代码" align="center" prop="supplierCodeDw" />
<el-table-column v-if="isColVisible('creditCode')" label="统一社会信用代码" align="center" prop="creditCode" />
<el-table-column v-if="isColVisible('supplierType')" label="供应商类型" align="center" prop="supplierType">
<template #default="scope">{{ formatOption(supplierTypeOptions, scope.row.supplierType) }}</template>
</el-table-column>
<el-table-column label="启用状态" align="center" prop="isEnable">
<el-table-column v-if="isColVisible('isEnable')" label="启用状态" align="center" prop="isEnable">
<template #default="scope">
<dict-tag :options="sys_normal_disable" :value="scope.row.isEnable" />
</template>
</el-table-column>
<el-table-column label="采购员" align="center" prop="purchaserUserId">
<el-table-column v-if="isColVisible('purchaserUserId')" label="采购员" align="center" prop="purchaserUserId">
<template #default="scope"><user-name :user-id="scope.row.purchaserUserId" /></template>
</el-table-column>
<el-table-column label="应付额度" align="center" prop="ed" />
<el-table-column label="应付账期" align="center" prop="zq" />
<el-table-column label="结算员" align="center" prop="settlementUserId">
<el-table-column v-if="isColVisible('ed')" label="应付额度" align="center" prop="ed" />
<el-table-column v-if="isColVisible('zq')" label="应付账期" align="center" prop="zq" />
<el-table-column v-if="isColVisible('settlementUserId')" label="结算员" align="center" prop="settlementUserId">
<template #default="scope"><user-name :user-id="scope.row.settlementUserId" /></template>
</el-table-column>
<el-table-column label="跟单员" align="center" prop="serviceUserId">
<el-table-column v-if="isColVisible('serviceUserId')" label="跟单员" align="center" prop="serviceUserId">
<template #default="scope"><user-name :user-id="scope.row.serviceUserId" /></template>
</el-table-column>
<el-table-column label="结算方式" align="center" prop="settlementType">
<el-table-column v-if="isColVisible('settlementType')" label="结算方式" align="center" prop="settlementType">
<template #default="scope">
<dict-tag :options="settlement_type" :value="scope.row.settlementType" />
</template>
</el-table-column>
<el-table-column label="临时额度" align="center" prop="tempEd" />
<el-table-column label="应付余额" align="center" prop="accountBalance" />
<el-table-column label="外币应付余额" align="center" prop="foreignCurrencyBalance" />
<el-table-column label="信用额度" align="center" prop="creditEd" />
<el-table-column label="重量阈值" align="center" prop="weightThreshold" />
<el-table-column label="抛比" align="center" prop="throwRatio" />
<el-table-column label="合作开始日期" align="center" prop="cooperationStartDate">
<el-table-column v-if="isColVisible('tempEd')" label="临时额度" align="center" prop="tempEd" />
<el-table-column v-if="isColVisible('accountBalance')" label="应付余额" align="center" prop="accountBalance" />
<el-table-column v-if="isColVisible('foreignCurrencyBalance')" label="外币应付余额" align="center" prop="foreignCurrencyBalance" />
<el-table-column v-if="isColVisible('creditEd')" label="信用额度" align="center" prop="creditEd" />
<el-table-column v-if="isColVisible('weightThreshold')" label="重量阈值" align="center" prop="weightThreshold" />
<el-table-column v-if="isColVisible('throwRatio')" label="抛比" align="center" prop="throwRatio" />
<el-table-column v-if="isColVisible('cooperationStartDate')" label="合作开始日期" align="center" prop="cooperationStartDate">
<template #default="scope">{{ formatDateOnly(scope.row.cooperationStartDate) }}</template>
</el-table-column>
<el-table-column label="合作状态" align="center" prop="cooperationStatus">
<el-table-column v-if="isColVisible('cooperationStatus')" label="合作状态" align="center" prop="cooperationStatus">
<template #default="scope">{{ formatOption(cooperationStatusOptions, scope.row.cooperationStatus) }}</template>
</el-table-column>
<el-table-column label="绑定渠道ID" align="center" prop="channelIds" />
<el-table-column label="备注说明" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="180">
<el-table-column v-if="isColVisible('channelIds')" label="绑定渠道ID" align="center" prop="channelIds" />
<el-table-column v-if="isColVisible('remark')" label="备注说明" align="center" prop="remark" :show-overflow-tooltip="true" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="180" fixed="right">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['customersupplier:supplierinfo:edit']">修改</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['customersupplier:supplierinfo:remove']">删除</el-button>
@@ -162,9 +163,37 @@
<script setup name="Supplier">
import { listSupplier, delSupplier } from "@/api/customersuppliermanagement/supplierinfo/supplier.js"
import { useColumnSettings } from '@/utils/columnSettings'
const { proxy } = getCurrentInstance()
/** 可自定义列(默认全部可见;选择列固定、操作列固定右侧不参与设置) */
const columnDefs = [
{ key: 'supplierCode', label: '供应商编码' },
{ key: 'supplierName', label: '供应商名称' },
{ key: 'supplierCodeDw', label: '对外代码' },
{ key: 'creditCode', label: '统一社会信用代码' },
{ key: 'supplierType', label: '供应商类型' },
{ key: 'isEnable', label: '启用状态' },
{ key: 'purchaserUserId', label: '采购员' },
{ key: 'ed', label: '应付额度' },
{ key: 'zq', label: '应付账期' },
{ key: 'settlementUserId', label: '结算员' },
{ key: 'serviceUserId', label: '跟单员' },
{ key: 'settlementType', label: '结算方式' },
{ key: 'tempEd', label: '临时额度' },
{ key: 'accountBalance', label: '应付余额' },
{ key: 'foreignCurrencyBalance', label: '外币应付余额' },
{ key: 'creditEd', label: '信用额度' },
{ key: 'weightThreshold', label: '重量阈值' },
{ key: 'throwRatio', label: '抛比' },
{ key: 'cooperationStartDate', label: '合作开始日期' },
{ key: 'cooperationStatus', label: '合作状态' },
{ key: 'channelIds', label: '绑定渠道ID' },
{ key: 'remark', label: '备注说明' }
]
const { colVisible, isColVisible, toggleColumn, resetColumns } = useColumnSettings('supplier-info-list', columnDefs)
const supplierList = ref([])
const loading = ref(true)
const showSearch = ref(true)
@@ -69,16 +69,16 @@ const schemas = computed(() => [
width: '100%',
props: { maxlength: 50 }
},
// {
// label: '税模式',
// prop: 'taxMode',
// component: 'select',
// placeholder: '请选择',
// dict: 'bms_tax_mode',
// span: 12,
// width: '100%',
// rules: [{ required: true, message: '请选择税模式', trigger: 'change' }]
// },
{
label: '税模式',
prop: 'taxMode',
component: 'select',
placeholder: '请选择',
dict: 'bms_tax_mode',
span: 12,
width: '100%',
rules: [{ required: true, message: '请选择税模式', trigger: 'change' }]
},
{
label: '生效日期',
prop: 'effectiveDate',
@@ -113,7 +113,7 @@ const columns = [
{ prop: 'serviceLevel', label: '时效等级', width: 90, align: 'center' },
{ prop: 'cargoAttr', label: '走货属性', minWidth: 120, slot: 'cargoAttr' },
{ prop: 'destRegion', label: '目的区域', width: 90, align: 'center' },
// { prop: 'taxMode', label: '税模式', width: 90, align: 'center', dict: 'bms_tax_mode' },
{ prop: 'taxMode', label: '税模式', width: 90, align: 'center', dict: 'bms_tax_mode' },
{ prop: 'effectiveDate', label: '生效日期', width: 120, align: 'center' },
{ prop: 'expireDate', label: '失效日期', width: 120, align: 'center' },
{ prop: 'countryCount', label: '国家数', width: 80, align: 'center' },
@@ -125,15 +125,15 @@ const columns = [
const searchColumns = [
{ prop: 'productCode', label: '产品代码', component: 'input', placeholder: '产品代码' },
{ prop: 'productName', label: '产品名称', component: 'input', placeholder: '产品名称' }
// {
// prop: 'taxMode',
// label: '税模式',
// component: 'select',
// dict: 'bms_tax_mode',
// placeholder: '税模式',
// clearable: true
// }
{ prop: 'productName', label: '产品名称', component: 'input', placeholder: '产品名称' },
{
prop: 'taxMode',
label: '税模式',
component: 'select',
dict: 'bms_tax_mode',
placeholder: '税模式',
clearable: true
}
]
function queryList(params) {