diff --git a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/customer/customermoney/CustomerBillController.java b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/customer/customermoney/CustomerBillController.java index 708c440a..843d41f5 100644 --- a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/customer/customermoney/CustomerBillController.java +++ b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/customer/customermoney/CustomerBillController.java @@ -13,8 +13,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.ruoyi.ar.domain.ArBalanceWriteoff; import com.ruoyi.ar.service.IArBalanceWriteoffService; -import com.ruoyi.ar.service.IArBillService; -import com.ruoyi.ar.domain.vo.ArBillQuery; import com.ruoyi.common.annotation.Log; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; @@ -22,7 +20,7 @@ import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.enums.BusinessType; /** - * 客户余额核销应收账单 / 账单只读查询 Controller + * 客户余额核销应收账单 Controller(账单查看统一走应收管理-应收账单页面) * * @author ruoyi * @date 2026-09-16 @@ -34,9 +32,6 @@ public class CustomerBillController extends BaseController @Autowired private IArBalanceWriteoffService balanceWriteoffService; - @Autowired - private IArBillService billService; - /** * 查询客户可核销账单(已确认/部分核销,未核销额大于0) */ @@ -87,25 +82,4 @@ public class CustomerBillController extends BaseController balanceWriteoffService.cancelWriteoff(writeoffId, body == null ? null : body.getCancelRemark()); return success(); } - - /** - * 账单查看:分页查询应收账单(只读,供客户侧账单查看页使用) - */ - @PreAuthorize("@ss.hasPermi('customer:bill:list')") - @GetMapping("/list") - public TableDataInfo billList(ArBillQuery query) - { - startPage(); - return getDataTable(billService.selectArBillList(query)); - } - - /** - * 账单查看:账单详情(含订单明细/费用明细) - */ - @PreAuthorize("@ss.hasPermi('customer:bill:query')") - @GetMapping("/{arBillId}") - public AjaxResult billInfo(@PathVariable("arBillId") Long arBillId) - { - return success(billService.selectArBillById(arBillId)); - } } diff --git a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/customer/customermoney/CustomerMoneyController.java b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/customer/customermoney/CustomerMoneyController.java index 26058a3b..e8028dbd 100644 --- a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/customer/customermoney/CustomerMoneyController.java +++ b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/customer/customermoney/CustomerMoneyController.java @@ -17,6 +17,7 @@ import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.enums.BusinessType; import com.ruoyi.customer.customermoney.domain.CustomerBalanceFlow; +import com.ruoyi.customer.customermoney.domain.CustomerCreditApply; import com.ruoyi.customer.customermoney.domain.CustomerCreditFlow; import com.ruoyi.customer.customermoney.domain.CustomerReceipt; import com.ruoyi.customer.customermoney.service.ICustomerMoneyService; @@ -136,15 +137,76 @@ public class CustomerMoneyController extends BaseController } /** - * 调整客户额度(信用额度/临时额度),调整过程留额度流水 + * 提交额度调整申请(信用额度/临时额度),申请待审批通过后才真正调整额度 */ @PreAuthorize("@ss.hasPermi('customer:credit:adjust')") - @Log(title = "客户额度调整", businessType = BusinessType.UPDATE) - @PostMapping("/credit/adjust") - public AjaxResult adjustCredit(@RequestBody CustomerCreditFlow flow) + @Log(title = "客户额度调整申请", businessType = BusinessType.INSERT) + @PostMapping("/credit/apply") + public AjaxResult applyCredit(@RequestBody CustomerCreditApply apply) { - flow.setCreateBy(getUsername()); - flow.setOperatorId(getUserId()); - return toAjax(customerMoneyService.adjustCredit(flow)); + apply.setCreateBy(getUsername()); + return toAjax(customerMoneyService.applyCreditAdjust(apply)); + } + + /** + * 查询额度调整申请列表 + */ + @PreAuthorize("@ss.hasPermi('customersupplier:customerinfo:list')") + @GetMapping("/credit/apply/list") + public TableDataInfo creditApplyList(CustomerCreditApply query) + { + if (query == null || query.getCustomerId() == null || query.getCustomerId() == 0L) { + return getDataTable(Collections.emptyList()); + } + startPage(); + List list = customerMoneyService.selectCreditApplyList(query); + return getDataTable(list); + } + + /** + * 查询额度调整申请详情 + */ + @PreAuthorize("@ss.hasPermi('customersupplier:customerinfo:list')") + @GetMapping("/credit/apply/{applyId}") + public AjaxResult getCreditApply(@PathVariable("applyId") Long applyId) + { + return success(customerMoneyService.selectCreditApplyById(applyId)); + } + + /** + * 审批通过额度调整申请:通过后调整账户额度并生成额度流水 + */ + @PreAuthorize("@ss.hasPermi('customer:credit:approve')") + @Log(title = "客户额度调整审批", businessType = BusinessType.UPDATE) + @PutMapping("/credit/apply/approve/{applyId}") + public AjaxResult approveCreditApply(@PathVariable("applyId") Long applyId, + @RequestBody(required = false) CustomerCreditApply body) + { + String remark = body != null ? body.getApproveRemark() : null; + return toAjax(customerMoneyService.approveCreditApply(applyId, remark)); + } + + /** + * 驳回额度调整申请(驳回原因必填) + */ + @PreAuthorize("@ss.hasPermi('customer:credit:approve')") + @Log(title = "客户额度调整驳回", businessType = BusinessType.UPDATE) + @PutMapping("/credit/apply/reject/{applyId}") + public AjaxResult rejectCreditApply(@PathVariable("applyId") Long applyId, + @RequestBody CustomerCreditApply body) + { + String reason = body != null ? body.getApproveRemark() : null; + return toAjax(customerMoneyService.rejectCreditApply(applyId, reason)); + } + + /** + * 撤销待审批的额度调整申请 + */ + @PreAuthorize("@ss.hasPermi('customer:credit:adjust')") + @Log(title = "客户额度调整撤销", businessType = BusinessType.UPDATE) + @PutMapping("/credit/apply/cancel/{applyId}") + public AjaxResult cancelCreditApply(@PathVariable("applyId") Long applyId) + { + return toAjax(customerMoneyService.cancelCreditApply(applyId)); } } diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/domain/CustomerCreditApply.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/domain/CustomerCreditApply.java new file mode 100644 index 00000000..0ce23ce7 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/domain/CustomerCreditApply.java @@ -0,0 +1,102 @@ +package com.ruoyi.customer.customermoney.domain; + +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.core.domain.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 客户额度调整申请审批 customer_credit_apply + * 信用额度/额外额度(临时额度)调整先申请,审批通过后才真正调整账户额度 + * + * @author ruoyi + * @date 2026-09-16 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class CustomerCreditApply extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 待审批 */ + public static final String STATUS_PENDING = "PENDING"; + /** 已通过 */ + public static final String STATUS_APPROVED = "APPROVED"; + /** 已驳回 */ + public static final String STATUS_REJECTED = "REJECTED"; + /** 已撤销 */ + public static final String STATUS_CANCELLED = "CANCELLED"; + + /** 申请单ID */ + private Long applyId; + + /** 申请单号(EDSQ前缀) */ + private String applyNo; + + /** 客户ID */ + private Long customerId; + + /** 客户账户ID */ + private Long accountId; + + /** 额度类型:CREDIT信用额度 TEMP临时额度(额外额度) */ + private String creditType; + + /** 调整方式:SET设置为 ADD增加 SUB减少 */ + private String adjustMode; + + /** 调整金额(正数) */ + private java.math.BigDecimal changeAmount; + + /** 临时额度生效日期 */ + @JsonFormat(pattern = "yyyy-MM-dd") + private Date effectiveDate; + + /** 临时额度失效日期(空=长期) */ + @JsonFormat(pattern = "yyyy-MM-dd") + private Date expireDate; + + /** 调整原因 */ + private String reason; + + /** 状态:PENDING/APPROVED/REJECTED/CANCELLED */ + private String status; + + /** 申请人 */ + private String applyBy; + + /** 申请人ID */ + private Long applyUserId; + + /** 申请时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date applyTime; + + /** 审批人 */ + private String approveBy; + + /** 审批人ID */ + private Long approveUserId; + + /** 审批时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date approveTime; + + /** 审批意见/驳回原因 */ + private String approveRemark; + + /** 审批通过后生成的额度流水ID */ + private Long flowId; + + /** 删除标志 */ + private String delFlag; + + /* -------- 展示字段 -------- */ + + /** 客户编码 */ + private String customerCode; + + /** 客户名称 */ + private String customerName; +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/mapper/CustomerCreditApplyMapper.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/mapper/CustomerCreditApplyMapper.java new file mode 100644 index 00000000..964b91dc --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/mapper/CustomerCreditApplyMapper.java @@ -0,0 +1,35 @@ +package com.ruoyi.customer.customermoney.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.ruoyi.customer.customermoney.domain.CustomerCreditApply; + +/** + * 客户额度调整申请审批 Mapper + * + * @author ruoyi + * @date 2026-09-16 + */ +public interface CustomerCreditApplyMapper +{ + /** 新增申请 */ + int insertApply(CustomerCreditApply apply); + + /** 按ID查申请(行锁,审批事务使用) */ + CustomerCreditApply selectApplyByIdForUpdate(@Param("applyId") Long applyId); + + /** 按ID查申请 */ + CustomerCreditApply selectApplyById(@Param("applyId") Long applyId); + + /** 查询申请列表(可按客户/状态/类型过滤) */ + List selectApplyList(CustomerCreditApply query); + + /** 审批通过:PENDING -> APPROVED,回写账户ID、流水ID、审批人/时间/意见 */ + int updateApproved(CustomerCreditApply apply); + + /** 审批驳回:PENDING -> REJECTED */ + int updateRejected(CustomerCreditApply apply); + + /** 申请人撤销:PENDING -> CANCELLED */ + int updateCancelled(@Param("applyId") Long applyId, @Param("updateBy") String updateBy); +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/service/ICustomerMoneyService.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/service/ICustomerMoneyService.java index daa468fa..2a955384 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/service/ICustomerMoneyService.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/service/ICustomerMoneyService.java @@ -4,6 +4,7 @@ import java.math.BigDecimal; import java.util.List; import com.ruoyi.customer.customermoney.domain.CustomerAccount; import com.ruoyi.customer.customermoney.domain.CustomerBalanceFlow; +import com.ruoyi.customer.customermoney.domain.CustomerCreditApply; import com.ruoyi.customer.customermoney.domain.CustomerCreditFlow; import com.ruoyi.customer.customermoney.domain.CustomerReceipt; @@ -52,11 +53,28 @@ public interface ICustomerMoneyService List selectCreditFlowList(CustomerCreditFlow query); /** - * 调整客户额度(信用额度/临时额度) - * 事务内:锁定CNY账户(不存在则初始化)-> 校验(不能为负、不能低于已用额度) - * -> 乐观锁更新额度 -> 回写客户档案额度字段 -> 写额度流水 + * 提交额度调整申请(信用额度/临时额度)。申请仅落库为 PENDING,不立即变动账户额度; + * 申请时会按账户当前额度做一次合法性校验(不能为负、不能低于已用额度),审批通过时会再次校验。 */ - int adjustCredit(CustomerCreditFlow flow); + int applyCreditAdjust(CustomerCreditApply apply); + + /** + * 审批通过额度调整申请:PENDING -> APPROVED。 + * 事务内重新锁定CNY账户并按审批时的实际额度计算/校验,通过后调整额度、回写客户档案、写额度流水。 + */ + int approveCreditApply(Long applyId, String approveRemark); + + /** 审批驳回:PENDING -> REJECTED,驳回原因必填 */ + int rejectCreditApply(Long applyId, String rejectReason); + + /** 申请人撤销:PENDING -> CANCELLED */ + int cancelCreditApply(Long applyId); + + /** 查询额度调整申请列表 */ + List selectCreditApplyList(CustomerCreditApply query); + + /** 查询额度调整申请详情 */ + CustomerCreditApply selectCreditApplyById(Long applyId); /** * 账单确认时占用授信额度(used_credit += amount)。该币种账户不存在时跳过。 diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/service/impl/CustomerMoneyServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/service/impl/CustomerMoneyServiceImpl.java index 69ce261c..ad9bb948 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/service/impl/CustomerMoneyServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/service/impl/CustomerMoneyServiceImpl.java @@ -20,8 +20,10 @@ import com.ruoyi.customer.customerinfo.domain.BmsCustomerAttachment; import com.ruoyi.customer.customerinfo.mapper.BmsCustomerMapper; import com.ruoyi.customer.customermoney.domain.CustomerAccount; import com.ruoyi.customer.customermoney.domain.CustomerBalanceFlow; +import com.ruoyi.customer.customermoney.domain.CustomerCreditApply; import com.ruoyi.customer.customermoney.domain.CustomerCreditFlow; import com.ruoyi.customer.customermoney.domain.CustomerReceipt; +import com.ruoyi.customer.customermoney.mapper.CustomerCreditApplyMapper; import com.ruoyi.customer.customermoney.mapper.CustomerMoneyMapper; import com.ruoyi.customer.customermoney.service.ICustomerMoneyService; import com.ruoyi.system.domain.BmsExchangeRate; @@ -48,6 +50,9 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService @Autowired private CustomerMoneyMapper customerMoneyMapper; + @Autowired + private CustomerCreditApplyMapper creditApplyMapper; + @Autowired private BmsCustomerMapper bmsCustomerMapper; @@ -268,53 +273,27 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService return customerMoneyMapper.selectCreditFlowList(query); } - @Override - @Transactional(rollbackFor = Exception.class) - public int adjustCredit(CustomerCreditFlow cmd) + /** + * 执行额度调整(额度申请审批通过后调用)。 + * 锁CNY账户 -> 按审批时实际额度计算并校验 -> 乐观锁更新 -> 回写客户档案 -> 写额度流水。 + * 返回已落库的额度流水(含ID)。 + */ + private CustomerCreditFlow executeAdjustCredit(CustomerCreditFlow cmd) { - if (cmd == null || cmd.getCustomerId() == null) - { - throw new ServiceException("客户ID不能为空"); - } - Long customerId = cmd.getCustomerId(); - String creditType = cmd.getCreditType(); - if (!CustomerCreditFlow.CREDIT_TYPE_CREDIT.equals(creditType) - && !CustomerCreditFlow.CREDIT_TYPE_TEMP.equals(creditType)) - { - throw new ServiceException("额度类型不正确"); - } - String adjustMode = cmd.getAdjustMode(); - if (!CustomerCreditFlow.MODE_SET.equals(adjustMode) - && !CustomerCreditFlow.MODE_ADD.equals(adjustMode) - && !CustomerCreditFlow.MODE_SUB.equals(adjustMode)) - { - throw new ServiceException("调整方式不正确"); - } - BigDecimal input = nz(cmd.getChangeAmount()); - if (input.compareTo(BigDecimal.ZERO) <= 0) - { - throw new ServiceException("调整金额必须大于0"); - } - if (!StringUtils.hasText(cmd.getReason())) - { - throw new ServiceException("请填写调整原因"); - } - String reason = cmd.getReason().trim(); - if (reason.length() > 255) - { - throw new ServiceException("调整原因不能超过255个字"); - } String username = SecurityUtils.getUsername(); Long userId = SecurityUtils.getUserId(); + Long customerId = cmd.getCustomerId(); + String creditType = cmd.getCreditType(); + String adjustMode = cmd.getAdjustMode(); + String reason = cmd.getReason(); boolean isTemp = CustomerCreditFlow.CREDIT_TYPE_TEMP.equals(creditType); - // 1. 行锁锁定CNY账户(不存在则按客户档案额度初始化创建);先处理临时额度过期 - CustomerAccount account = getOrInitCnyAccount(customerId, username); - expireTempCreditIfNeeded(account); + // 1. 参数/日期/结果合法性校验并锁定CNY账户(不存在则按客户档案额度初始化);先处理临时额度过期 + CustomerAccount account = validateAdjustCommand(cmd, username); + BigDecimal input = nz(cmd.getChangeAmount()); // 2. 计算调整结果 BigDecimal before = isTemp ? nz(account.getExtraCredit()) : nz(account.getTotalCredit()); - BigDecimal otherCredit = isTemp ? nz(account.getTotalCredit()) : nz(account.getExtraCredit()); BigDecimal after; switch (adjustMode) { @@ -329,40 +308,18 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService break; } after = after.setScale(MONEY_SCALE, RoundingMode.HALF_UP); - if (after.compareTo(BigDecimal.ZERO) < 0) - { - throw new ServiceException("调整后额度不能为负数"); - } - if (otherCredit.add(after).compareTo(nz(account.getUsedCredit())) < 0) - { - throw new ServiceException("调整后授信总额度不能小于已用额度:" - + nz(account.getUsedCredit()).toPlainString()); - } - if (after.compareTo(before) == 0) - { - throw new ServiceException("额度未发生变化,无需调整"); - } - // 3. 临时额度有效期校验(不允许预约未来生效,失效日必须晚于生效日) + // 3. 临时额度有效期处理(不允许预约未来生效,失效日必须晚于生效日) Date effectiveDate = null; Date expireDate = null; if (isTemp) { LocalDate today = LocalDate.now(); LocalDate eff = cmd.getEffectiveDate() != null ? toLocalDate(cmd.getEffectiveDate()) : today; - if (eff.isAfter(today)) - { - throw new ServiceException("生效日期不能晚于今天,需要未来生效请在生效当天再设置"); - } effectiveDate = toSqlDate(eff); if (cmd.getExpireDate() != null) { - LocalDate exp = toLocalDate(cmd.getExpireDate()); - if (!exp.isAfter(eff)) - { - throw new ServiceException("失效日期必须晚于生效日期"); - } - expireDate = toSqlDate(exp); + expireDate = toSqlDate(toLocalDate(cmd.getExpireDate())); } account.setExtraCredit(after); account.setExtraEffectiveDate(effectiveDate); @@ -408,7 +365,263 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService flow.setRemark(cmd.getRemark()); flow.setDelFlag("0"); customerMoneyMapper.insertCreditFlow(flow); - return 1; + return flow; + } + + /* ================= 额度调整:申请 / 审批 ================= */ + + @Override + public List selectCreditApplyList(CustomerCreditApply query) + { + return creditApplyMapper.selectApplyList(query); + } + + @Override + public CustomerCreditApply selectCreditApplyById(Long applyId) + { + return creditApplyMapper.selectApplyById(applyId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public int applyCreditAdjust(CustomerCreditApply apply) + { + if (apply == null || apply.getCustomerId() == null) + { + throw new ServiceException("客户ID不能为空"); + } + // 构造内部调整命令,复用统一的参数/日期/额度合法性校验(只校验,不写入) + CustomerCreditFlow cmd = new CustomerCreditFlow(); + cmd.setCustomerId(apply.getCustomerId()); + cmd.setCreditType(apply.getCreditType()); + cmd.setAdjustMode(apply.getAdjustMode()); + cmd.setChangeAmount(nz(apply.getChangeAmount())); + cmd.setReason(apply.getReason()); + cmd.setRemark(apply.getRemark()); + cmd.setEffectiveDate(apply.getEffectiveDate()); + cmd.setExpireDate(apply.getExpireDate()); + + String username = SecurityUtils.getUsername(); + Long userId = SecurityUtils.getUserId(); + + // 1. 参数校验 + 日期校验 + 按当前账户额度预演结果,提前拦截不可能通过的申请(不写入账户) + validateAdjustCommand(cmd, username); + apply.setChangeAmount(cmd.getChangeAmount()); + apply.setReason(cmd.getReason()); + + // 2. 待审批申请落库,不动账户额度 + apply.setApplyNo(generateNo("EDSQ")); + apply.setStatus(CustomerCreditApply.STATUS_PENDING); + apply.setApplyBy(username); + apply.setApplyUserId(userId); + apply.setApplyTime(new Date()); + apply.setCreateBy(username); + return creditApplyMapper.insertApply(apply); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public int approveCreditApply(Long applyId, String approveRemark) + { + if (applyId == null) + { + throw new ServiceException("申请单ID不能为空"); + } + CustomerCreditApply apply = creditApplyMapper.selectApplyByIdForUpdate(applyId); + if (apply == null) + { + throw new ServiceException("额度调整申请不存在"); + } + if (!CustomerCreditApply.STATUS_PENDING.equals(apply.getStatus())) + { + throw new ServiceException("该申请已处理,不能重复审批"); + } + String username = SecurityUtils.getUsername(); + Long userId = SecurityUtils.getUserId(); + + // 以审批时账户的实际额度重新计算并调整(申请期间额度可能已变动) + CustomerCreditFlow cmd = new CustomerCreditFlow(); + cmd.setCustomerId(apply.getCustomerId()); + cmd.setCreditType(apply.getCreditType()); + cmd.setAdjustMode(apply.getAdjustMode()); + cmd.setChangeAmount(nz(apply.getChangeAmount())); + cmd.setReason(apply.getReason()); + cmd.setRemark(apply.getRemark()); + cmd.setEffectiveDate(apply.getEffectiveDate()); + cmd.setExpireDate(apply.getExpireDate()); + CustomerCreditFlow flow = executeAdjustCredit(cmd); + + CustomerCreditApply update = new CustomerCreditApply(); + update.setApplyId(applyId); + update.setAccountId(flow.getAccountId()); + update.setFlowId(flow.getId()); + update.setApproveBy(username); + update.setApproveUserId(userId); + update.setApproveRemark(approveRemark); + int rows = creditApplyMapper.updateApproved(update); + if (rows == 0) + { + throw new ServiceException("申请状态已变化,审批失败"); + } + return rows; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public int rejectCreditApply(Long applyId, String rejectReason) + { + if (applyId == null) + { + throw new ServiceException("申请单ID不能为空"); + } + if (!StringUtils.hasText(rejectReason)) + { + throw new ServiceException("请填写驳回原因"); + } + String reason = rejectReason.trim(); + if (reason.length() > 255) + { + throw new ServiceException("驳回原因不能超过255个字"); + } + CustomerCreditApply apply = creditApplyMapper.selectApplyByIdForUpdate(applyId); + if (apply == null) + { + throw new ServiceException("额度调整申请不存在"); + } + if (!CustomerCreditApply.STATUS_PENDING.equals(apply.getStatus())) + { + throw new ServiceException("该申请已处理,不能重复审批"); + } + CustomerCreditApply update = new CustomerCreditApply(); + update.setApplyId(applyId); + update.setApproveBy(SecurityUtils.getUsername()); + update.setApproveUserId(SecurityUtils.getUserId()); + update.setApproveRemark(reason); + int rows = creditApplyMapper.updateRejected(update); + if (rows == 0) + { + throw new ServiceException("申请状态已变化,驳回失败"); + } + return rows; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public int cancelCreditApply(Long applyId) + { + if (applyId == null) + { + throw new ServiceException("申请单ID不能为空"); + } + CustomerCreditApply apply = creditApplyMapper.selectApplyByIdForUpdate(applyId); + if (apply == null) + { + throw new ServiceException("额度调整申请不存在"); + } + if (!CustomerCreditApply.STATUS_PENDING.equals(apply.getStatus())) + { + throw new ServiceException("仅待审批的申请可以撤销"); + } + int rows = creditApplyMapper.updateCancelled(applyId, SecurityUtils.getUsername()); + if (rows == 0) + { + throw new ServiceException("申请状态已变化,撤销失败"); + } + return rows; + } + + /** + * 额度调整参数/日期/结果合法性校验(申请与审批共用)。 + * 锁定CNY账户做预演但不写入,返回当前账户;规整后的金额/原因/日期回写到 cmd。 + */ + private CustomerAccount validateAdjustCommand(CustomerCreditFlow cmd, String username) + { + if (cmd == null || cmd.getCustomerId() == null) + { + throw new ServiceException("客户ID不能为空"); + } + String creditType = cmd.getCreditType(); + if (!CustomerCreditFlow.CREDIT_TYPE_CREDIT.equals(creditType) + && !CustomerCreditFlow.CREDIT_TYPE_TEMP.equals(creditType)) + { + throw new ServiceException("额度类型不正确"); + } + String adjustMode = cmd.getAdjustMode(); + if (!CustomerCreditFlow.MODE_SET.equals(adjustMode) + && !CustomerCreditFlow.MODE_ADD.equals(adjustMode) + && !CustomerCreditFlow.MODE_SUB.equals(adjustMode)) + { + throw new ServiceException("调整方式不正确"); + } + BigDecimal input = nz(cmd.getChangeAmount()); + if (input.compareTo(BigDecimal.ZERO) <= 0) + { + throw new ServiceException("调整金额必须大于0"); + } + if (!StringUtils.hasText(cmd.getReason())) + { + throw new ServiceException("请填写调整原因"); + } + String reason = cmd.getReason().trim(); + if (reason.length() > 255) + { + throw new ServiceException("调整原因不能超过255个字"); + } + cmd.setChangeAmount(input); + cmd.setReason(reason); + boolean isTemp = CustomerCreditFlow.CREDIT_TYPE_TEMP.equals(creditType); + + CustomerAccount account = getOrInitCnyAccount(cmd.getCustomerId(), username); + expireTempCreditIfNeeded(account); + + BigDecimal before = isTemp ? nz(account.getExtraCredit()) : nz(account.getTotalCredit()); + BigDecimal otherCredit = isTemp ? nz(account.getTotalCredit()) : nz(account.getExtraCredit()); + BigDecimal after; + switch (adjustMode) + { + case CustomerCreditFlow.MODE_SET: + after = input; + break; + case CustomerCreditFlow.MODE_ADD: + after = before.add(input); + break; + default: + after = before.subtract(input); + break; + } + after = after.setScale(MONEY_SCALE, RoundingMode.HALF_UP); + if (after.compareTo(BigDecimal.ZERO) < 0) + { + throw new ServiceException("调整后额度不能为负数"); + } + if (otherCredit.add(after).compareTo(nz(account.getUsedCredit())) < 0) + { + throw new ServiceException("调整后授信总额度不能小于已用额度:" + + nz(account.getUsedCredit()).toPlainString()); + } + if (after.compareTo(before) == 0) + { + throw new ServiceException("额度未发生变化,无需调整"); + } + + if (isTemp) + { + LocalDate today = LocalDate.now(); + LocalDate eff = cmd.getEffectiveDate() != null ? toLocalDate(cmd.getEffectiveDate()) : today; + if (eff.isAfter(today)) + { + throw new ServiceException("生效日期不能晚于今天,需要未来生效请在生效当天再提交"); + } + if (cmd.getExpireDate() != null) + { + LocalDate exp = toLocalDate(cmd.getExpireDate()); + if (!exp.isAfter(eff)) + { + throw new ServiceException("失效日期必须晚于生效日期"); + } + } + } + return account; } /* ================= 账单联动:额度占用 / 余额核销 ================= */ diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/customer/CustomerCreditApplyMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/customer/CustomerCreditApplyMapper.xml new file mode 100644 index 00000000..aac36186 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/resources/mapper/customer/CustomerCreditApplyMapper.xml @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select a.apply_id, a.apply_no, a.customer_id, a.account_id, a.credit_type, a.adjust_mode, + a.change_amount, a.effective_date, a.expire_date, a.reason, a.status, + a.apply_by, a.apply_user_id, a.apply_time, + a.approve_by, a.approve_user_id, a.approve_time, a.approve_remark, a.flow_id, + a.create_by, a.create_time, a.update_by, a.update_time, a.remark, a.del_flag, + c.customer_code, c.customer_name + from customer_credit_apply a + left join bms_customer c on c.customer_id = a.customer_id + + + + + + + + + + insert into customer_credit_apply ( + apply_no, customer_id, credit_type, adjust_mode, change_amount, + effective_date, expire_date, reason, status, + apply_by, apply_user_id, apply_time, + create_by, create_time, remark + ) values ( + #{applyNo}, #{customerId}, #{creditType}, #{adjustMode}, #{changeAmount}, + #{effectiveDate}, #{expireDate}, #{reason}, #{status}, + #{applyBy}, #{applyUserId}, #{applyTime}, + #{createBy}, sysdate(), #{remark} + ) + + + + update customer_credit_apply + set status = 'APPROVED', + account_id = #{accountId}, + flow_id = #{flowId}, + approve_by = #{approveBy}, + approve_user_id = #{approveUserId}, + approve_time = sysdate(), + approve_remark = #{approveRemark}, + update_by = #{approveBy}, + update_time = sysdate() + where apply_id = #{applyId} and status = 'PENDING' and del_flag = '0' + + + + update customer_credit_apply + set status = 'REJECTED', + approve_by = #{approveBy}, + approve_user_id = #{approveUserId}, + approve_time = sysdate(), + approve_remark = #{approveRemark}, + update_by = #{approveBy}, + update_time = sysdate() + where apply_id = #{applyId} and status = 'PENDING' and del_flag = '0' + + + + update customer_credit_apply + set status = 'CANCELLED', + update_by = #{updateBy}, + update_time = sysdate() + where apply_id = #{applyId} and status = 'PENDING' and del_flag = '0' + + + diff --git a/bms-back/sql/customer+supplier/customer_credit_apply.sql b/bms-back/sql/customer+supplier/customer_credit_apply.sql new file mode 100644 index 00000000..be1c6546 --- /dev/null +++ b/bms-back/sql/customer+supplier/customer_credit_apply.sql @@ -0,0 +1,72 @@ +-- ============================================================= +-- 客户额度调整申请/审批表 +-- 信用额度、额外额度(临时额度)调整不再直接生效: +-- 先申请(PENDING)-> 审批通过(APPROVED)后才真正调整 customer_account +-- 并生成 customer_credit_flow 额度流水;可驳回(REJECTED)/撤销(CANCELLED) +-- 2026-09-16 +-- ============================================================= +SET NAMES utf8mb4; + +CREATE TABLE IF NOT EXISTS `customer_credit_apply` ( + `apply_id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '申请单ID', + `apply_no` VARCHAR(64) NOT NULL COMMENT '申请单号(EDSQ前缀)', + `customer_id` BIGINT(20) NOT NULL COMMENT '客户ID', + `account_id` BIGINT(20) DEFAULT NULL COMMENT '客户账户ID(审批通过时回写)', + `credit_type` VARCHAR(16) NOT NULL COMMENT '额度类型:CREDIT信用额度 TEMP临时额度(额外额度)', + `adjust_mode` VARCHAR(8) NOT NULL COMMENT '调整方式:SET设置为 ADD增加 SUB减少', + `change_amount` DECIMAL(18,2) NOT NULL COMMENT '调整金额(恒为正数,方式看adjust_mode)', + `effective_date` DATE DEFAULT NULL COMMENT '临时额度生效日期', + `expire_date` DATE DEFAULT NULL COMMENT '临时额度失效日期(空=长期)', + `reason` VARCHAR(255) NOT NULL COMMENT '调整原因', + `status` VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '状态:PENDING待审批 APPROVED已通过 REJECTED已驳回 CANCELLED已撤销', + `apply_by` VARCHAR(64) DEFAULT '' COMMENT '申请人', + `apply_user_id` BIGINT(20) DEFAULT NULL COMMENT '申请人ID', + `apply_time` DATETIME DEFAULT NULL COMMENT '申请时间', + `approve_by` VARCHAR(64) DEFAULT '' COMMENT '审批人', + `approve_user_id` BIGINT(20) DEFAULT NULL COMMENT '审批人ID', + `approve_time` DATETIME DEFAULT NULL COMMENT '审批时间', + `approve_remark` VARCHAR(255) DEFAULT NULL COMMENT '审批意见/驳回原因', + `flow_id` BIGINT(20) DEFAULT NULL COMMENT '审批通过后生成的额度流水ID(customer_credit_flow.id)', + `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT '' COMMENT '创建者', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT '' COMMENT '更新者', + `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` CHAR(1) DEFAULT '0' COMMENT '删除标志(0存在 1删除)', + PRIMARY KEY (`apply_id`), + UNIQUE KEY `uk_apply_no` (`apply_no`), + KEY `idx_customer_status` (`customer_id`, `status`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='客户额度调整申请审批表'; + +-- ============================================================= +-- 菜单:额度审批(挂在 2422 客户余额管理 下,与 2434 额度调整/申请 并列) +-- ============================================================= +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT 2439, N'额度审批', 2422, 2, '', NULL, 1, 0, 'F', '0', '0', 'customer:credit:approve', '#', 'admin', sysdate(), '客户信用/额外额度调整申请审批' +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2439); + +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT 2, 2439 FROM DUAL +WHERE NOT EXISTS (SELECT 1 FROM sys_role_menu WHERE role_id = 2 AND menu_id = 2439); + +-- ============================================================= +-- 下线“客户账单”独立页面(需求2): +-- 余额页“查看账单”改为跳转应收管理-应收账单(/ar/bill,菜单2401)并带客户条件 +-- 2435 客户账单(C) / 2436 账单详情(F) 删除; +-- 2437 余额核销 / 2438 取消余额核销 仍在客户余额页使用,改挂到 2422 客户余额管理 +-- ============================================================= +DELETE FROM sys_role_menu WHERE menu_id IN (2435, 2436); +DELETE FROM sys_menu WHERE menu_id IN (2435, 2436); +UPDATE sys_menu SET parent_id = 2422 WHERE menu_id IN (2437, 2438); + +-- ============================================================= +-- 字典:客户业务类型 customer_type 补齐(原有仅 0直客) +-- 0直客 1同行 2集运 +-- ============================================================= +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark) +SELECT 1, N'同行', '1', 'customer_type', '', 'info', 'N', '0', 'admin', sysdate(), '同行客户' +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'customer_type' AND dict_value = '1'); +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark) +SELECT 2, N'集运', '2', 'customer_type', '', 'warning', 'N', '0', 'admin', sysdate(), '集运客户' +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'customer_type' AND dict_value = '2'); diff --git a/bms-front/src/api/customersuppliermanagement/customermoney/customerbill.js b/bms-front/src/api/customersuppliermanagement/customermoney/customerbill.js index be8ed613..626b1b90 100644 --- a/bms-front/src/api/customersuppliermanagement/customermoney/customerbill.js +++ b/bms-front/src/api/customersuppliermanagement/customermoney/customerbill.js @@ -35,19 +35,3 @@ export function cancelBalanceWriteoff(writeoffId, cancelRemark) { }) } -/** 账单查看:账单列表(只读) */ -export function listCustomerBill(query) { - return request({ - url: '/customersupplier/customerbill/list', - method: 'get', - params: query - }) -} - -/** 账单查看:账单详情(含明细) */ -export function getCustomerBill(arBillId) { - return request({ - url: '/customersupplier/customerbill/' + arBillId, - method: 'get' - }) -} diff --git a/bms-front/src/api/customersuppliermanagement/customermoney/customermoney.js b/bms-front/src/api/customersuppliermanagement/customermoney/customermoney.js index a9847d8e..9fb1bca4 100644 --- a/bms-front/src/api/customersuppliermanagement/customermoney/customermoney.js +++ b/bms-front/src/api/customersuppliermanagement/customermoney/customermoney.js @@ -68,11 +68,47 @@ export function listCreditFlow(query) { }) } -/** 调整客户额度(信用额度/临时额度),调整过程留额度流水 */ -export function adjustCredit(data) { +/** 提交额度调整申请(信用额度/临时额度),审批通过后才调整额度 */ +export function applyCreditAdjust(data) { return request({ - url: '/customersupplier/customermoney/credit/adjust', + url: '/customersupplier/customermoney/credit/apply', method: 'post', data: data }) } + +/** 查询额度调整申请列表 */ +export function listCreditApply(query) { + return request({ + url: '/customersupplier/customermoney/credit/apply/list', + method: 'get', + params: query + }) +} + +/** 审批通过额度调整申请 */ +export function approveCreditApply(applyId, approveRemark) { + return request({ + url: '/customersupplier/customermoney/credit/apply/approve/' + applyId, + method: 'put', + data: { approveRemark } + }) +} + +/** 驳回额度调整申请 */ +export function rejectCreditApply(applyId, approveRemark) { + return request({ + url: '/customersupplier/customermoney/credit/apply/reject/' + applyId, + method: 'put', + data: { approveRemark } + }) +} + +/** 撤销待审批的额度调整申请 */ +export function cancelCreditApply(applyId) { + return request({ + url: '/customersupplier/customermoney/credit/apply/cancel/' + applyId, + method: 'put' + }) +} + diff --git a/bms-front/src/views/ar/bill/index.vue b/bms-front/src/views/ar/bill/index.vue index 8210bd08..dde2a09c 100644 --- a/bms-front/src/views/ar/bill/index.vue +++ b/bms-front/src/views/ar/bill/index.vue @@ -1,6 +1,6 @@ - + @@ -804,7 +895,7 @@ import axios from 'axios' import { saveAs } from 'file-saver' import { getCustomer } from "@/api/customersuppliermanagement/customerinfo/customer.js" -import { listAccount, listReceipt, getReceipt, addReceipt, confirmReceipt, cancelReceipt, listFlow, listCreditFlow, adjustCredit } from "@/api/customersuppliermanagement/customermoney/customermoney.js" +import { listAccount, listReceipt, getReceipt, addReceipt, confirmReceipt, cancelReceipt, listFlow, listCreditFlow, applyCreditAdjust, listCreditApply, approveCreditApply, rejectCreditApply, cancelCreditApply } from "@/api/customersuppliermanagement/customermoney/customermoney.js" import { listUnsettledBill, listBalanceWriteoff, balanceWriteoff, cancelBalanceWriteoff } from "@/api/customersuppliermanagement/customermoney/customerbill.js" import { convertToBase } from "@/api/data/exchangeRate" import { getToken } from "@/utils/auth" @@ -1056,12 +1147,12 @@ function submitAdjustCredit() { payload.expireDate = f.expireDate || null } creditSubmitLoading.value = true - adjustCredit(payload).then(() => { - proxy.$modal.msgSuccess('额度调整成功') + applyCreditAdjust(payload).then(() => { + proxy.$modal.msgSuccess('申请已提交,等待审批') creditDialog.visible = false - loadCustomer() - loadAccounts() - getCreditFlowList() + if (creditApplyLoaded) { + getApplyList() + } }).finally(() => { creditSubmitLoading.value = false }) @@ -1346,6 +1437,88 @@ function resetCreditFlowQuery() { getCreditFlowList() } +/* ================= 额度调整申请/审批 ================= */ +const applyStatusOptions = [ + { value: 'PENDING', label: '待审批' }, + { value: 'APPROVED', label: '已通过' }, + { value: 'REJECTED', label: '已驳回' }, + { value: 'CANCELLED', label: '已撤销' } +] +const applyStatusTagMap = { PENDING: 'warning', APPROVED: 'success', REJECTED: 'danger', CANCELLED: 'info' } +function applyStatusTag(v) { + return applyStatusTagMap[v] || 'info' +} + +const applyList = ref([]) +const applyTotal = ref(0) +const applyLoading = ref(false) +const applyQuery = reactive({ + pageNum: 1, + pageSize: 10, + customerId: route.query.customerId, + creditType: '', + status: '' +}) +let creditApplyLoaded = false + +function getApplyList() { + const customerId = route.query.customerId + if (!customerId) return + applyLoading.value = true + applyQuery.customerId = customerId + listCreditApply({ ...applyQuery }).then(res => { + applyList.value = res.rows || [] + applyTotal.value = res.total || 0 + }).finally(() => { + applyLoading.value = false + }) +} + +function resetApplyQuery() { + applyQuery.creditType = '' + applyQuery.status = '' + applyQuery.pageNum = 1 + getApplyList() +} + +/** 审批通过后刷新账户卡片与额度流水 */ +function afterApplyHandled() { + getApplyList() + loadAccounts() + getCreditFlowList() +} + +function handleApproveApply(row) { + proxy.$modal.confirm(`确认通过额度调整申请「${row.applyNo}」?通过后将立即调整客户额度并写入额度流水。`) + .then(() => approveCreditApply(row.applyId, '审批通过')) + .then(() => { + proxy.$modal.msgSuccess('已审批通过') + afterApplyHandled() + }).catch(() => {}) +} + +function handleRejectApply(row) { + proxy.$prompt('请输入驳回原因', '驳回额度调整申请', { + confirmButtonText: '确认驳回', + cancelButtonText: '取消', + inputType: 'textarea', + inputPattern: /\S+/, + inputErrorMessage: '驳回原因不能为空' + }).then(({ value }) => rejectCreditApply(row.applyId, value)).then(() => { + proxy.$modal.msgSuccess('已驳回') + getApplyList() + }).catch(() => {}) +} + +function handleCancelApply(row) { + proxy.$modal.confirm(`确认撤销待审批申请「${row.applyNo}」?`) + .then(() => cancelCreditApply(row.applyId)) + .then(() => { + proxy.$modal.msgSuccess('已撤销') + getApplyList() + }).catch(() => {}) +} + /* ================= 账单核销 ================= */ const billStatusMap = { DRAFT: { label: '草稿', type: 'info' }, @@ -1366,7 +1539,7 @@ function formatDate(val) { } function goBillView() { router.push({ - path: '/customersuppliermanagement/customerbill', + path: '/ar/bill', query: { customerId: route.query.customerId || '' } }) } @@ -1510,6 +1683,10 @@ function submitCancelWriteoff() { } watch(activeTab, (tab) => { + if (tab === 'creditApply' && !creditApplyLoaded) { + creditApplyLoaded = true + getApplyList() + } if (tab === 'billWriteoff' && !billWriteoffLoaded) { billWriteoffLoaded = true loadBillWriteoffData() @@ -1632,6 +1809,13 @@ function initPage() { flowTotal.value = 0 creditFlowList.value = [] creditFlowTotal.value = 0 + applyList.value = [] + applyTotal.value = 0 + applyQuery.customerId = customerId + applyQuery.creditType = '' + applyQuery.status = '' + applyQuery.pageNum = 1 + creditApplyLoaded = false unsettledList.value = [] writeoffList.value = [] writeoffTotal.value = 0 @@ -1716,6 +1900,9 @@ watch(() => route.query.customerId, (newId, oldId) => { .credit-tip { margin-bottom: 12px; } +.credit-apply-tip { + margin-bottom: 12px; +} .credit-preview { margin-top: 4px; padding: 10px 14px; diff --git a/bms-front/src/views/customersuppliermanagement/customerinfo/index.vue b/bms-front/src/views/customersuppliermanagement/customerinfo/index.vue index e7782250..e8c0a5dd 100644 --- a/bms-front/src/views/customersuppliermanagement/customerinfo/index.vue +++ b/bms-front/src/views/customersuppliermanagement/customerinfo/index.vue @@ -124,7 +124,11 @@ - + + +