Merge branch 'master' of http://git.lionquexpress.com/wuweihua/BMS
This commit is contained in:
-106
@@ -1,106 +0,0 @@
|
|||||||
package com.ruoyi.web.controller.ar;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
|
||||||
import org.springframework.validation.annotation.Validated;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.bind.annotation.PutMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
|
||||||
import org.springframework.web.bind.annotation.RequestHeader;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
import com.ruoyi.ar.domain.ArReceipt;
|
|
||||||
import com.ruoyi.ar.domain.ArReceiptWriteoff;
|
|
||||||
import com.ruoyi.ar.domain.vo.ArBillActionRequest;
|
|
||||||
import com.ruoyi.ar.domain.vo.ArReceiptQuery;
|
|
||||||
import com.ruoyi.ar.domain.vo.ArReceiptWriteoffRequest;
|
|
||||||
import com.ruoyi.ar.service.IArReceiptService;
|
|
||||||
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.enums.BusinessType;
|
|
||||||
import com.ruoyi.common.utils.StringUtils;
|
|
||||||
import com.ruoyi.common.utils.uuid.IdUtils;
|
|
||||||
|
|
||||||
/** 应收收款与核销管理。 */
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/ar/receipt")
|
|
||||||
@Validated
|
|
||||||
public class ArReceiptController extends BaseController
|
|
||||||
{
|
|
||||||
@Autowired
|
|
||||||
private IArReceiptService receiptService;
|
|
||||||
|
|
||||||
@PreAuthorize("@ss.hasPermi('ar:receipt:list')")
|
|
||||||
@GetMapping("/list")
|
|
||||||
public TableDataInfo list(ArReceiptQuery query)
|
|
||||||
{
|
|
||||||
startPage();
|
|
||||||
return getDataTable(receiptService.selectReceiptList(query));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreAuthorize("@ss.hasPermi('ar:receipt:list')")
|
|
||||||
@GetMapping("/{receiptId}/writeoffs")
|
|
||||||
public AjaxResult writeoffs(@PathVariable Long receiptId)
|
|
||||||
{
|
|
||||||
return success(receiptService.selectWriteoffs(receiptId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Log(title = "应收收款", businessType = BusinessType.INSERT)
|
|
||||||
@PreAuthorize("@ss.hasPermi('ar:receipt:add')")
|
|
||||||
@PostMapping
|
|
||||||
public AjaxResult add(@Valid @RequestBody ArReceipt receipt,
|
|
||||||
@RequestHeader(value = "X-Request-Id", required = false) String headerRequestId)
|
|
||||||
{
|
|
||||||
String requestId = requestId(headerRequestId);
|
|
||||||
return success(receiptService.registerReceipt(receipt, getUsername(), requestId)).put("request_id", requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Log(title = "应收收款", businessType = BusinessType.UPDATE)
|
|
||||||
@PreAuthorize("@ss.hasPermi('ar:receipt:cancel')")
|
|
||||||
@PutMapping("/{receiptId}/cancel")
|
|
||||||
public AjaxResult cancelReceipt(@PathVariable Long receiptId, @RequestBody(required = false) ArBillActionRequest request,
|
|
||||||
@RequestHeader(value = "X-Request-Id", required = false) String headerRequestId)
|
|
||||||
{
|
|
||||||
String requestId = requestId(request == null ? null : request.getRequestId(), headerRequestId);
|
|
||||||
receiptService.cancelReceipt(receiptId, request == null ? null : request.getRemark(), getUsername(), requestId);
|
|
||||||
return success().put("request_id", requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Log(title = "应收收款核销", businessType = BusinessType.UPDATE)
|
|
||||||
@PreAuthorize("@ss.hasPermi('ar:receipt:writeoff')")
|
|
||||||
@PostMapping("/writeoff")
|
|
||||||
public AjaxResult writeoff(@Valid @RequestBody ArReceiptWriteoffRequest request,
|
|
||||||
@RequestHeader(value = "X-Request-Id", required = false) String headerRequestId)
|
|
||||||
{
|
|
||||||
String requestId = StringUtils.isNotEmpty(request.getRequestId()) ? request.getRequestId() : requestId(headerRequestId);
|
|
||||||
List<ArReceiptWriteoff> rows = receiptService.writeoff(request, getUsername(), requestId);
|
|
||||||
return success(rows).put("request_id", requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Log(title = "应收收款核销", businessType = BusinessType.UPDATE)
|
|
||||||
@PreAuthorize("@ss.hasPermi('ar:receipt:cancel')")
|
|
||||||
@PutMapping("/writeoff/{writeoffId}/cancel")
|
|
||||||
public AjaxResult cancel(@PathVariable Long writeoffId, @RequestBody(required = false) ArBillActionRequest request,
|
|
||||||
@RequestHeader(value = "X-Request-Id", required = false) String headerRequestId)
|
|
||||||
{
|
|
||||||
String requestId = requestId(request == null ? null : request.getRequestId(), headerRequestId);
|
|
||||||
receiptService.cancelWriteoff(writeoffId, request == null ? null : request.getRemark(), getUsername(), requestId);
|
|
||||||
return success().put("request_id", requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String requestId(String headerRequestId)
|
|
||||||
{
|
|
||||||
return StringUtils.isNotEmpty(headerRequestId) ? headerRequestId : IdUtils.fastSimpleUUID();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String requestId(String bodyRequestId, String headerRequestId)
|
|
||||||
{
|
|
||||||
return StringUtils.isNotEmpty(bodyRequestId) ? bodyRequestId : requestId(headerRequestId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-27
@@ -13,8 +13,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import com.ruoyi.ar.domain.ArBalanceWriteoff;
|
import com.ruoyi.ar.domain.ArBalanceWriteoff;
|
||||||
import com.ruoyi.ar.service.IArBalanceWriteoffService;
|
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.annotation.Log;
|
||||||
import com.ruoyi.common.core.controller.BaseController;
|
import com.ruoyi.common.core.controller.BaseController;
|
||||||
import com.ruoyi.common.core.domain.AjaxResult;
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
@@ -22,7 +20,7 @@ import com.ruoyi.common.core.page.TableDataInfo;
|
|||||||
import com.ruoyi.common.enums.BusinessType;
|
import com.ruoyi.common.enums.BusinessType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 客户余额核销应收账单 / 账单只读查询 Controller
|
* 客户余额核销应收账单 Controller(账单查看统一走应收管理-应收账单页面)
|
||||||
*
|
*
|
||||||
* @author ruoyi
|
* @author ruoyi
|
||||||
* @date 2026-09-16
|
* @date 2026-09-16
|
||||||
@@ -34,9 +32,6 @@ public class CustomerBillController extends BaseController
|
|||||||
@Autowired
|
@Autowired
|
||||||
private IArBalanceWriteoffService balanceWriteoffService;
|
private IArBalanceWriteoffService balanceWriteoffService;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private IArBillService billService;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询客户可核销账单(已确认/部分核销,未核销额大于0)
|
* 查询客户可核销账单(已确认/部分核销,未核销额大于0)
|
||||||
*/
|
*/
|
||||||
@@ -87,25 +82,4 @@ public class CustomerBillController extends BaseController
|
|||||||
balanceWriteoffService.cancelWriteoff(writeoffId, body == null ? null : body.getCancelRemark());
|
balanceWriteoffService.cancelWriteoff(writeoffId, body == null ? null : body.getCancelRemark());
|
||||||
return success();
|
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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+69
-7
@@ -17,6 +17,7 @@ import com.ruoyi.common.core.domain.AjaxResult;
|
|||||||
import com.ruoyi.common.core.page.TableDataInfo;
|
import com.ruoyi.common.core.page.TableDataInfo;
|
||||||
import com.ruoyi.common.enums.BusinessType;
|
import com.ruoyi.common.enums.BusinessType;
|
||||||
import com.ruoyi.customer.customermoney.domain.CustomerBalanceFlow;
|
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.CustomerCreditFlow;
|
||||||
import com.ruoyi.customer.customermoney.domain.CustomerReceipt;
|
import com.ruoyi.customer.customermoney.domain.CustomerReceipt;
|
||||||
import com.ruoyi.customer.customermoney.service.ICustomerMoneyService;
|
import com.ruoyi.customer.customermoney.service.ICustomerMoneyService;
|
||||||
@@ -136,15 +137,76 @@ public class CustomerMoneyController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 调整客户额度(信用额度/临时额度),调整过程留额度流水
|
* 提交额度调整申请(信用额度/临时额度),申请待审批通过后才真正调整额度
|
||||||
*/
|
*/
|
||||||
@PreAuthorize("@ss.hasPermi('customer:credit:adjust')")
|
@PreAuthorize("@ss.hasPermi('customer:credit:adjust')")
|
||||||
@Log(title = "客户额度调整", businessType = BusinessType.UPDATE)
|
@Log(title = "客户额度调整申请", businessType = BusinessType.INSERT)
|
||||||
@PostMapping("/credit/adjust")
|
@PostMapping("/credit/apply")
|
||||||
public AjaxResult adjustCredit(@RequestBody CustomerCreditFlow flow)
|
public AjaxResult applyCredit(@RequestBody CustomerCreditApply apply)
|
||||||
{
|
{
|
||||||
flow.setCreateBy(getUsername());
|
apply.setCreateBy(getUsername());
|
||||||
flow.setOperatorId(getUserId());
|
return toAjax(customerMoneyService.applyCreditAdjust(apply));
|
||||||
return toAjax(customerMoneyService.adjustCredit(flow));
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询额度调整申请列表
|
||||||
|
*/
|
||||||
|
@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<CustomerCreditApply> 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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import lombok.EqualsAndHashCode;
|
|||||||
/**
|
/**
|
||||||
* 应收账单余额核销 ar_balance_writeoff
|
* 应收账单余额核销 ar_balance_writeoff
|
||||||
*
|
*
|
||||||
* <p>用客户账户余额核销应收账单,与 ar_receipt_writeoff(收款核销)并列,
|
* <p>用客户账户余额核销应收账单,和客户账户余额流水联动,
|
||||||
* 两者共享 ar_bill.written_off_amount / unwritten_amount 缓存。</p>
|
* 共享 ar_bill.written_off_amount / unwritten_amount 缓存。</p>
|
||||||
*
|
*
|
||||||
* @author ruoyi
|
* @author ruoyi
|
||||||
* @date 2026-09-16
|
* @date 2026-09-16
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
package com.ruoyi.ar.domain;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.Date;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
|
||||||
import com.ruoyi.common.core.domain.BaseEntity;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
|
|
||||||
/** 应收真实收款。 */
|
|
||||||
@Data
|
|
||||||
@EqualsAndHashCode(callSuper = true)
|
|
||||||
public class ArReceipt extends BaseEntity
|
|
||||||
{
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
|
|
||||||
private Long receiptId;
|
|
||||||
private String receiptNo;
|
|
||||||
private Long customerId;
|
|
||||||
/** 客户名称,仅用于列表展示。 */
|
|
||||||
private String customerName;
|
|
||||||
/** 客户编码,仅用于列表展示。 */
|
|
||||||
private String customerCode;
|
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
|
||||||
private Date receiptDate;
|
|
||||||
private String currency;
|
|
||||||
private BigDecimal receiptAmount;
|
|
||||||
private BigDecimal unallocatedAmount;
|
|
||||||
private String paymentMethod;
|
|
||||||
private String bankReference;
|
|
||||||
private String status;
|
|
||||||
private String remark;
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package com.ruoyi.ar.domain;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.Date;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
/** 收款与账单的核销关系。 */
|
|
||||||
@Data
|
|
||||||
public class ArReceiptWriteoff implements Serializable
|
|
||||||
{
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
|
|
||||||
private Long writeoffId;
|
|
||||||
private Long receiptId;
|
|
||||||
private Long arBillId;
|
|
||||||
/** 关联账单号,仅用于核销记录展示。 */
|
|
||||||
private String billNo;
|
|
||||||
/** 关联收款单号,仅用于核销记录展示。 */
|
|
||||||
private String receiptNo;
|
|
||||||
private String writeoffNo;
|
|
||||||
private BigDecimal writeoffAmount;
|
|
||||||
private String currency;
|
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
|
||||||
private Date writeoffDate;
|
|
||||||
private String status;
|
|
||||||
private String remark;
|
|
||||||
private String createBy;
|
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
|
||||||
private Date createTime;
|
|
||||||
private String cancelBy;
|
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
|
||||||
private Date cancelTime;
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package com.ruoyi.ar.domain.vo;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
import com.ruoyi.ar.domain.ArReceipt;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
|
|
||||||
/** 收款查询参数。 */
|
|
||||||
@Data
|
|
||||||
@EqualsAndHashCode(callSuper = true)
|
|
||||||
public class ArReceiptQuery extends ArReceipt
|
|
||||||
{
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
private Date receiptDateStart;
|
|
||||||
private Date receiptDateEnd;
|
|
||||||
}
|
|
||||||
-35
@@ -1,35 +0,0 @@
|
|||||||
package com.ruoyi.ar.domain.vo;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import jakarta.validation.constraints.NotEmpty;
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
/** 批量收款核销参数。 */
|
|
||||||
@Data
|
|
||||||
public class ArReceiptWriteoffRequest
|
|
||||||
{
|
|
||||||
@NotNull(message = "收款ID不能为空")
|
|
||||||
private Long receiptId;
|
|
||||||
@NotEmpty(message = "至少选择一张账单")
|
|
||||||
@Valid
|
|
||||||
private List<Allocation> allocations;
|
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
|
||||||
private Date writeoffDate;
|
|
||||||
private String currency;
|
|
||||||
private String remark;
|
|
||||||
private String requestId;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public static class Allocation
|
|
||||||
{
|
|
||||||
@NotNull(message = "账单ID不能为空")
|
|
||||||
private Long arBillId;
|
|
||||||
@NotNull(message = "核销金额不能为空")
|
|
||||||
private BigDecimal amount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package com.ruoyi.ar.enums;
|
|
||||||
|
|
||||||
/** 应收收款状态。 */
|
|
||||||
public enum ArReceiptStatus
|
|
||||||
{
|
|
||||||
REGISTERED("REGISTERED"),
|
|
||||||
PARTIAL_WRITTEN_OFF("PARTIAL_WRITTEN_OFF"),
|
|
||||||
WRITTEN_OFF("WRITTEN_OFF"),
|
|
||||||
CANCELLED("CANCELLED");
|
|
||||||
|
|
||||||
private final String code;
|
|
||||||
|
|
||||||
ArReceiptStatus(String code)
|
|
||||||
{
|
|
||||||
this.code = code;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCode()
|
|
||||||
{
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package com.ruoyi.ar.enums;
|
|
||||||
|
|
||||||
/** 收款核销状态。 */
|
|
||||||
public enum ArWriteoffStatus
|
|
||||||
{
|
|
||||||
EFFECTIVE("EFFECTIVE"),
|
|
||||||
CANCELLED("CANCELLED");
|
|
||||||
|
|
||||||
private final String code;
|
|
||||||
|
|
||||||
ArWriteoffStatus(String code)
|
|
||||||
{
|
|
||||||
this.code = code;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCode()
|
|
||||||
{
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package com.ruoyi.ar.mapper;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.List;
|
|
||||||
import org.apache.ibatis.annotations.Param;
|
|
||||||
import com.ruoyi.ar.domain.ArReceipt;
|
|
||||||
import com.ruoyi.ar.domain.vo.ArReceiptQuery;
|
|
||||||
|
|
||||||
public interface ArReceiptMapper
|
|
||||||
{
|
|
||||||
List<ArReceipt> selectArReceiptList(ArReceiptQuery query);
|
|
||||||
|
|
||||||
ArReceipt selectArReceiptById(Long receiptId);
|
|
||||||
|
|
||||||
ArReceipt selectArReceiptByIdForUpdate(Long receiptId);
|
|
||||||
|
|
||||||
ArReceipt selectArReceiptByNo(String receiptNo);
|
|
||||||
|
|
||||||
ArReceipt selectArReceiptByReference(@Param("paymentMethod") String paymentMethod,
|
|
||||||
@Param("bankReference") String bankReference);
|
|
||||||
|
|
||||||
int insertArReceipt(ArReceipt receipt);
|
|
||||||
|
|
||||||
int updateBalanceAndStatus(@Param("receiptId") Long receiptId,
|
|
||||||
@Param("unallocatedAmount") BigDecimal unallocatedAmount,
|
|
||||||
@Param("status") String status, @Param("operator") String operator);
|
|
||||||
|
|
||||||
int updateCancelled(@Param("receiptId") Long receiptId, @Param("operator") String operator,
|
|
||||||
@Param("remark") String remark);
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package com.ruoyi.ar.mapper;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.List;
|
|
||||||
import org.apache.ibatis.annotations.Param;
|
|
||||||
import com.ruoyi.ar.domain.ArReceiptWriteoff;
|
|
||||||
|
|
||||||
public interface ArReceiptWriteoffMapper
|
|
||||||
{
|
|
||||||
List<ArReceiptWriteoff> selectByBillId(Long arBillId);
|
|
||||||
|
|
||||||
List<ArReceiptWriteoff> selectByReceiptId(Long receiptId);
|
|
||||||
|
|
||||||
ArReceiptWriteoff selectById(Long writeoffId);
|
|
||||||
|
|
||||||
ArReceiptWriteoff selectByIdForUpdate(Long writeoffId);
|
|
||||||
|
|
||||||
int insert(ArReceiptWriteoff writeoff);
|
|
||||||
|
|
||||||
int updateCancelled(@Param("writeoffId") Long writeoffId, @Param("cancelBy") String cancelBy,
|
|
||||||
@Param("remark") String remark);
|
|
||||||
|
|
||||||
BigDecimal sumEffectiveByBillId(Long arBillId);
|
|
||||||
|
|
||||||
BigDecimal sumEffectiveByReceiptId(Long receiptId);
|
|
||||||
}
|
|
||||||
+1
-2
@@ -7,8 +7,7 @@ import com.ruoyi.ar.domain.ArBill;
|
|||||||
/**
|
/**
|
||||||
* 应收账单余额核销 Service接口
|
* 应收账单余额核销 Service接口
|
||||||
*
|
*
|
||||||
* <p>资金来源为客户账户余额(customer_account.balance),与收款核销
|
* <p>资金来源为客户账户余额(customer_account.balance);核销时三方联动:账户余额扣减、已用额度释放、
|
||||||
* (ar_receipt_writeoff)并列;核销时三方联动:账户余额扣减、已用额度释放、
|
|
||||||
* 账单已核销/未核销金额及状态更新。</p>
|
* 账单已核销/未核销金额及状态更新。</p>
|
||||||
*
|
*
|
||||||
* @author ruoyi
|
* @author ruoyi
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
package com.ruoyi.ar.service;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import com.ruoyi.ar.domain.ArReceipt;
|
|
||||||
import com.ruoyi.ar.domain.ArReceiptWriteoff;
|
|
||||||
import com.ruoyi.ar.domain.vo.ArReceiptQuery;
|
|
||||||
import com.ruoyi.ar.domain.vo.ArReceiptWriteoffRequest;
|
|
||||||
|
|
||||||
public interface IArReceiptService
|
|
||||||
{
|
|
||||||
List<ArReceipt> selectReceiptList(ArReceiptQuery query);
|
|
||||||
|
|
||||||
List<ArReceiptWriteoff> selectWriteoffs(Long receiptId);
|
|
||||||
|
|
||||||
ArReceipt registerReceipt(ArReceipt receipt, String operator, String requestId);
|
|
||||||
|
|
||||||
void cancelReceipt(Long receiptId, String remark, String operator, String requestId);
|
|
||||||
|
|
||||||
List<ArReceiptWriteoff> writeoff(ArReceiptWriteoffRequest request, String operator, String requestId);
|
|
||||||
|
|
||||||
void cancelWriteoff(Long writeoffId, String remark, String operator, String requestId);
|
|
||||||
}
|
|
||||||
+23
-6
@@ -23,8 +23,10 @@ import com.ruoyi.ar.service.IArBillLogService;
|
|||||||
import com.ruoyi.common.exception.ServiceException;
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
import com.ruoyi.common.utils.SecurityUtils;
|
import com.ruoyi.common.utils.SecurityUtils;
|
||||||
import com.ruoyi.common.utils.uuid.IdUtils;
|
import com.ruoyi.common.utils.uuid.IdUtils;
|
||||||
|
import com.ruoyi.customer.customermoney.api.BalanceChangeCommand;
|
||||||
|
import com.ruoyi.customer.customermoney.api.CustomerFundBizType;
|
||||||
|
import com.ruoyi.customer.customermoney.api.ICustomerAccountApi;
|
||||||
import com.ruoyi.customer.customermoney.domain.CustomerBalanceFlow;
|
import com.ruoyi.customer.customermoney.domain.CustomerBalanceFlow;
|
||||||
import com.ruoyi.customer.customermoney.service.ICustomerMoneyService;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 应收账单余额核销 Service实现
|
* 应收账单余额核销 Service实现
|
||||||
@@ -48,7 +50,7 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService
|
|||||||
private IArBillLogService logService;
|
private IArBillLogService logService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private ICustomerMoneyService customerMoneyService;
|
private ICustomerAccountApi customerAccountApi;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ArBill> selectUnsettledBills(Long customerId)
|
public List<ArBill> selectUnsettledBills(Long customerId)
|
||||||
@@ -125,8 +127,15 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService
|
|||||||
// 3. 扣减账户余额并释放等额已用额度,写 BILL_PAY 扣款流水(余额不足在此处拦截)
|
// 3. 扣减账户余额并释放等额已用额度,写 BILL_PAY 扣款流水(余额不足在此处拦截)
|
||||||
String flowRemark = "余额核销账单:" + bill.getBillNo()
|
String flowRemark = "余额核销账单:" + bill.getBillNo()
|
||||||
+ (StringUtils.hasText(writeoff.getRemark()) ? "(" + writeoff.getRemark() + ")" : "");
|
+ (StringUtils.hasText(writeoff.getRemark()) ? "(" + writeoff.getRemark() + ")" : "");
|
||||||
CustomerBalanceFlow flow = customerMoneyService.payBill(bill.getCustomerId(), currency,
|
BalanceChangeCommand payCmd = new BalanceChangeCommand();
|
||||||
amount, writeoff.getWriteoffId(), bill.getBillNo(), flowRemark);
|
payCmd.setCustomerId(bill.getCustomerId());
|
||||||
|
payCmd.setCurrency(currency);
|
||||||
|
payCmd.setAmount(amount);
|
||||||
|
payCmd.setBizType(CustomerFundBizType.BILL_PAY);
|
||||||
|
payCmd.setBizId(writeoff.getWriteoffId());
|
||||||
|
payCmd.setBizNo(bill.getBillNo());
|
||||||
|
payCmd.setRemark(flowRemark);
|
||||||
|
CustomerBalanceFlow flow = customerAccountApi.deductBalance(payCmd);
|
||||||
writeoffMapper.updateFlowRefs(writeoff.getWriteoffId(), flow.getAccountId(), flow.getId());
|
writeoffMapper.updateFlowRefs(writeoff.getWriteoffId(), flow.getAccountId(), flow.getId());
|
||||||
|
|
||||||
// 4. 更新账单已核销/未核销缓存与状态(乐观条件更新,状态被并发改动则整体回滚)
|
// 4. 更新账单已核销/未核销缓存与状态(乐观条件更新,状态被并发改动则整体回滚)
|
||||||
@@ -178,8 +187,16 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService
|
|||||||
|
|
||||||
// 2. 余额回补、已用额度重新占用,写反向 BILL_PAY 流水
|
// 2. 余额回补、已用额度重新占用,写反向 BILL_PAY 流水
|
||||||
String flowRemark = "反核销回补账单:" + bill.getBillNo() + "(" + cancelRemark + ")";
|
String flowRemark = "反核销回补账单:" + bill.getBillNo() + "(" + cancelRemark + ")";
|
||||||
CustomerBalanceFlow reverseFlow = customerMoneyService.reverseBillPay(writeoff.getCustomerId(),
|
BalanceChangeCommand reverseCmd = new BalanceChangeCommand();
|
||||||
writeoff.getCurrency(), nvl(writeoff.getWriteoffAmount()), writeoffId, bill.getBillNo(), flowRemark);
|
reverseCmd.setCustomerId(writeoff.getCustomerId());
|
||||||
|
reverseCmd.setCurrency(writeoff.getCurrency());
|
||||||
|
reverseCmd.setAmount(nvl(writeoff.getWriteoffAmount()));
|
||||||
|
reverseCmd.setBizType(CustomerFundBizType.BILL_PAY);
|
||||||
|
reverseCmd.setBizId(writeoffId);
|
||||||
|
reverseCmd.setBizNo(bill.getBillNo());
|
||||||
|
reverseCmd.setRemark(flowRemark);
|
||||||
|
reverseCmd.setReoccupyUsedCredit(Boolean.TRUE);
|
||||||
|
CustomerBalanceFlow reverseFlow = customerAccountApi.refundBalance(reverseCmd);
|
||||||
|
|
||||||
// 3. 账单核销金额回退,状态按剩余已核销额重算(归零则回到已确认)
|
// 3. 账单核销金额回退,状态按剩余已核销额重算(归零则回到已确认)
|
||||||
BigDecimal written = nvl(bill.getWrittenOffAmount()).subtract(nvl(writeoff.getWriteoffAmount()));
|
BigDecimal written = nvl(bill.getWrittenOffAmount()).subtract(nvl(writeoff.getWriteoffAmount()));
|
||||||
|
|||||||
+23
-7
@@ -58,7 +58,9 @@ import com.ruoyi.common.utils.StringUtils;
|
|||||||
import com.ruoyi.common.utils.file.FileUtils;
|
import com.ruoyi.common.utils.file.FileUtils;
|
||||||
import com.ruoyi.common.utils.file.FileUploadUtils;
|
import com.ruoyi.common.utils.file.FileUploadUtils;
|
||||||
import com.ruoyi.common.utils.uuid.IdUtils;
|
import com.ruoyi.common.utils.uuid.IdUtils;
|
||||||
import com.ruoyi.customer.customermoney.service.ICustomerMoneyService;
|
import com.ruoyi.customer.customermoney.api.CreditUsageCommand;
|
||||||
|
import com.ruoyi.customer.customermoney.api.CustomerFundBizType;
|
||||||
|
import com.ruoyi.customer.customermoney.api.ICustomerAccountApi;
|
||||||
import com.ruoyi.price.domain.PrcIncomeBillFee;
|
import com.ruoyi.price.domain.PrcIncomeBillFee;
|
||||||
import com.ruoyi.price.domain.PrcOrderBill;
|
import com.ruoyi.price.domain.PrcOrderBill;
|
||||||
import com.ruoyi.price.mapper.PrcIncomeBillFeeMapper;
|
import com.ruoyi.price.mapper.PrcIncomeBillFeeMapper;
|
||||||
@@ -81,7 +83,7 @@ public class ArBillServiceImpl implements IArBillService
|
|||||||
@Autowired private IArBillLogService logService;
|
@Autowired private IArBillLogService logService;
|
||||||
@Autowired private PrcOrderBillMapper sourceBillMapper;
|
@Autowired private PrcOrderBillMapper sourceBillMapper;
|
||||||
@Autowired private PrcIncomeBillFeeMapper sourceFeeMapper;
|
@Autowired private PrcIncomeBillFeeMapper sourceFeeMapper;
|
||||||
@Autowired private ICustomerMoneyService customerMoneyService;
|
@Autowired private ICustomerAccountApi customerAccountApi;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ArBill> selectArBillList(ArBillQuery query)
|
public List<ArBill> selectArBillList(ArBillQuery query)
|
||||||
@@ -279,9 +281,9 @@ public class ArBillServiceImpl implements IArBillService
|
|||||||
throw failure(arBillId, "BILL", String.valueOf(arBillId), "CONFIRM", bill.getStatus(),
|
throw failure(arBillId, "BILL", String.valueOf(arBillId), "CONFIRM", bill.getStatus(),
|
||||||
"账单状态已发生变化,请刷新后重试", operator, requestId);
|
"账单状态已发生变化,请刷新后重试", operator, requestId);
|
||||||
}
|
}
|
||||||
// 账单确认:应收形成,按账单金额占用客户授信额度(账户不存在时仅CNY会自动初始化)
|
// 账单确认:应收形成,按账单金额占用客户授信额度(账户不存在时仅CNY会自动初始化)。
|
||||||
customerMoneyService.occupyCredit(bill.getCustomerId(), bill.getCurrency(),
|
// 不校验可用额度,兼容历史 used_credit 可高于总额度、外币账户静默跳过的口径
|
||||||
nvl(bill.getTotalAmount()));
|
customerAccountApi.occupyCredit(buildCreditCommand(bill));
|
||||||
recordSuccess(bill, "CONFIRM", "确认应收账单", ArBillStatus.DRAFT.getCode(), ArBillStatus.CONFIRMED.getCode(), requestId, operator);
|
recordSuccess(bill, "CONFIRM", "确认应收账单", ArBillStatus.DRAFT.getCode(), ArBillStatus.CONFIRMED.getCode(), requestId, operator);
|
||||||
return selectArBillById(arBillId);
|
return selectArBillById(arBillId);
|
||||||
}
|
}
|
||||||
@@ -322,8 +324,7 @@ public class ArBillServiceImpl implements IArBillService
|
|||||||
// 账单作废:已确认账单占用的授信额度全额释放(可作废的CONFIRMED账单必然零核销)
|
// 账单作废:已确认账单占用的授信额度全额释放(可作废的CONFIRMED账单必然零核销)
|
||||||
if (currentStatus == ArBillStatus.CONFIRMED)
|
if (currentStatus == ArBillStatus.CONFIRMED)
|
||||||
{
|
{
|
||||||
customerMoneyService.releaseCredit(bill.getCustomerId(), bill.getCurrency(),
|
customerAccountApi.releaseCredit(buildCreditCommand(bill));
|
||||||
nvl(bill.getTotalAmount()));
|
|
||||||
}
|
}
|
||||||
if (!sourceBillIds.isEmpty())
|
if (!sourceBillIds.isEmpty())
|
||||||
{
|
{
|
||||||
@@ -860,6 +861,21 @@ public class ArBillServiceImpl implements IArBillService
|
|||||||
|
|
||||||
private BigDecimal decimal(BigDecimal value, int scale) { return value == null ? null : value.setScale(scale, java.math.RoundingMode.HALF_UP); }
|
private BigDecimal decimal(BigDecimal value, int scale) { return value == null ? null : value.setScale(scale, java.math.RoundingMode.HALF_UP); }
|
||||||
private BigDecimal nvl(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; }
|
private BigDecimal nvl(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; }
|
||||||
|
|
||||||
|
/** 构造账单授信占用/释放命令(确认占用、作废释放共用,关联账单单据便于流水追溯) */
|
||||||
|
private CreditUsageCommand buildCreditCommand(ArBill bill)
|
||||||
|
{
|
||||||
|
CreditUsageCommand command = new CreditUsageCommand();
|
||||||
|
command.setCustomerId(bill.getCustomerId());
|
||||||
|
command.setCurrency(bill.getCurrency());
|
||||||
|
command.setAmount(nvl(bill.getTotalAmount()));
|
||||||
|
command.setBizType(CustomerFundBizType.AR_BILL);
|
||||||
|
command.setBizId(bill.getArBillId());
|
||||||
|
command.setBizNo(bill.getBillNo());
|
||||||
|
// 兼容历史口径:不校验可用额度(允许 used_credit 高于总额度、外币账户静默跳过)
|
||||||
|
command.setCheckAvailable(Boolean.FALSE);
|
||||||
|
return command;
|
||||||
|
}
|
||||||
private String safeFilePart(String value) { return StringUtils.isEmpty(value) ? "客户" : value.replaceAll("[^a-zA-Z0-9\\u4e00-\\u9fa5_-]", "_"); }
|
private String safeFilePart(String value) { return StringUtils.isEmpty(value) ? "客户" : value.replaceAll("[^a-zA-Z0-9\\u4e00-\\u9fa5_-]", "_"); }
|
||||||
private String maskAccount(String value) { if (StringUtils.isEmpty(value)) return ""; return value.length() <= 4 ? "****" : "****" + value.substring(value.length() - 4); }
|
private String maskAccount(String value) { if (StringUtils.isEmpty(value)) return ""; return value.length() <= 4 ? "****" : "****" + value.substring(value.length() - 4); }
|
||||||
private String sha256(byte[] data) throws NoSuchAlgorithmException { byte[] digest = MessageDigest.getInstance("SHA-256").digest(data); StringBuilder sb = new StringBuilder(); for (byte b : digest) sb.append(String.format("%02x", b)); return sb.toString(); }
|
private String sha256(byte[] data) throws NoSuchAlgorithmException { byte[] digest = MessageDigest.getInstance("SHA-256").digest(data); StringBuilder sb = new StringBuilder(); for (byte b : digest) sb.append(String.format("%02x", b)); return sb.toString(); }
|
||||||
|
|||||||
-275
@@ -1,275 +0,0 @@
|
|||||||
package com.ruoyi.ar.service.impl;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.dao.DuplicateKeyException;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
import com.ruoyi.ar.domain.ArBill;
|
|
||||||
import com.ruoyi.ar.domain.ArBillLog;
|
|
||||||
import com.ruoyi.ar.domain.ArReceipt;
|
|
||||||
import com.ruoyi.ar.domain.ArReceiptWriteoff;
|
|
||||||
import com.ruoyi.ar.domain.vo.ArReceiptQuery;
|
|
||||||
import com.ruoyi.ar.domain.vo.ArReceiptWriteoffRequest;
|
|
||||||
import com.ruoyi.ar.enums.ArBillStatus;
|
|
||||||
import com.ruoyi.ar.enums.ArLogResult;
|
|
||||||
import com.ruoyi.ar.enums.ArReceiptStatus;
|
|
||||||
import com.ruoyi.ar.enums.ArWriteoffStatus;
|
|
||||||
import com.ruoyi.ar.mapper.ArBillMapper;
|
|
||||||
import com.ruoyi.ar.mapper.ArBillLogMapper;
|
|
||||||
import com.ruoyi.ar.mapper.ArReceiptMapper;
|
|
||||||
import com.ruoyi.ar.mapper.ArReceiptWriteoffMapper;
|
|
||||||
import com.ruoyi.ar.service.IArBillLogService;
|
|
||||||
import com.ruoyi.ar.service.IArReceiptService;
|
|
||||||
import com.ruoyi.common.exception.ServiceException;
|
|
||||||
import com.ruoyi.common.utils.DateUtils;
|
|
||||||
import com.ruoyi.common.utils.StringUtils;
|
|
||||||
import com.ruoyi.common.utils.uuid.IdUtils;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class ArReceiptServiceImpl implements IArReceiptService
|
|
||||||
{
|
|
||||||
@Autowired private ArReceiptMapper receiptMapper;
|
|
||||||
@Autowired private ArReceiptWriteoffMapper writeoffMapper;
|
|
||||||
@Autowired private ArBillMapper billMapper;
|
|
||||||
@Autowired private ArBillLogMapper logMapper;
|
|
||||||
@Autowired private IArBillLogService logService;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<ArReceipt> selectReceiptList(ArReceiptQuery query)
|
|
||||||
{
|
|
||||||
return receiptMapper.selectArReceiptList(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<ArReceiptWriteoff> selectWriteoffs(Long receiptId)
|
|
||||||
{
|
|
||||||
return writeoffMapper.selectByReceiptId(receiptId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
|
||||||
public ArReceipt registerReceipt(ArReceipt receipt, String operator, String requestId)
|
|
||||||
{
|
|
||||||
if (receipt == null || receipt.getCustomerId() == null || receipt.getReceiptDate() == null
|
|
||||||
|| receipt.getReceiptAmount() == null || receipt.getReceiptAmount().compareTo(BigDecimal.ZERO) <= 0)
|
|
||||||
{
|
|
||||||
throw failure(null, "RECEIPT", "REGISTER", "收款客户、日期和正数金额不能为空", operator, requestId);
|
|
||||||
}
|
|
||||||
if (StringUtils.isEmpty(receipt.getReceiptNo()))
|
|
||||||
{
|
|
||||||
receipt.setReceiptNo("RC" + DateUtils.dateTimeNow("yyyyMMddHHmmss") + IdUtils.fastSimpleUUID().substring(0, 6).toUpperCase());
|
|
||||||
}
|
|
||||||
if (StringUtils.isEmpty(receipt.getCurrency())) receipt.setCurrency("CNY");
|
|
||||||
if (StringUtils.isEmpty(receipt.getPaymentMethod())) receipt.setPaymentMethod("BANK");
|
|
||||||
ArBillLog previous = logMapper.selectSuccessByRequestId(requestId, "RECEIPT");
|
|
||||||
if (previous != null && isNumber(previous.getObjectId()))
|
|
||||||
{
|
|
||||||
ArReceipt existed = receiptMapper.selectArReceiptById(Long.valueOf(previous.getObjectId()));
|
|
||||||
if (existed != null) return existed;
|
|
||||||
}
|
|
||||||
if (receiptMapper.selectArReceiptByNo(receipt.getReceiptNo()) != null)
|
|
||||||
{
|
|
||||||
throw failure(null, "RECEIPT", receipt.getReceiptNo(), "收款单号已存在", operator, requestId);
|
|
||||||
}
|
|
||||||
if (!StringUtils.isEmpty(receipt.getBankReference())
|
|
||||||
&& receiptMapper.selectArReceiptByReference(receipt.getPaymentMethod(), receipt.getBankReference()) != null)
|
|
||||||
{
|
|
||||||
throw failure(null, "RECEIPT", receipt.getReceiptNo(), "银行流水号已存在", operator, requestId);
|
|
||||||
}
|
|
||||||
receipt.setUnallocatedAmount(receipt.getReceiptAmount());
|
|
||||||
receipt.setStatus(ArReceiptStatus.REGISTERED.getCode());
|
|
||||||
receipt.setCreateBy(operator);
|
|
||||||
receipt.setUpdateBy(operator);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
receiptMapper.insertArReceipt(receipt);
|
|
||||||
}
|
|
||||||
catch (DuplicateKeyException e)
|
|
||||||
{
|
|
||||||
ArReceipt existed = receiptMapper.selectArReceiptByNo(receipt.getReceiptNo());
|
|
||||||
if (existed != null) return existed;
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
ArBillLog log = new ArBillLog();
|
|
||||||
log.setObjectType("RECEIPT"); log.setObjectId(String.valueOf(receipt.getReceiptId()));
|
|
||||||
log.setOperationType("RECEIPT"); log.setOperationDesc("登记应收收款");
|
|
||||||
log.setOperatorId(operator); log.setOperatorName(operator); log.setOperationSource("WEB");
|
|
||||||
log.setRequestId(requestId); log.setResult(ArLogResult.SUCCESS.getCode()); logService.record(log);
|
|
||||||
return receipt;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
|
||||||
public void cancelReceipt(Long receiptId, String remark, String operator, String requestId)
|
|
||||||
{
|
|
||||||
ArBillLog previous = logMapper.selectSuccessByRequestId(requestId, "RECEIPT");
|
|
||||||
if (previous != null && "CANCEL_RECEIPT".equals(previous.getOperationType())) return;
|
|
||||||
ArReceipt receipt = receiptMapper.selectArReceiptByIdForUpdate(receiptId);
|
|
||||||
if (receipt == null) throw failure(null, "RECEIPT", String.valueOf(receiptId), "收款不存在", operator, requestId);
|
|
||||||
if (!ArReceiptStatus.REGISTERED.getCode().equals(receipt.getStatus())
|
|
||||||
|| nvl(receipt.getUnallocatedAmount()).compareTo(nvl(receipt.getReceiptAmount())) != 0)
|
|
||||||
{
|
|
||||||
throw failure(null, "RECEIPT", String.valueOf(receiptId), "收款已发生核销,不能直接取消", operator, requestId);
|
|
||||||
}
|
|
||||||
receiptMapper.updateCancelled(receiptId, operator, StringUtils.isEmpty(remark) ? "取消收款" : remark);
|
|
||||||
ArBillLog log = new ArBillLog();
|
|
||||||
log.setObjectType("RECEIPT"); log.setObjectId(String.valueOf(receiptId)); log.setOperationType("CANCEL_RECEIPT");
|
|
||||||
log.setOperationDesc(StringUtils.isEmpty(remark) ? "取消收款" : remark); log.setFromStatus(ArReceiptStatus.REGISTERED.getCode());
|
|
||||||
log.setToStatus(ArReceiptStatus.CANCELLED.getCode()); log.setOperatorId(operator); log.setOperatorName(operator);
|
|
||||||
log.setOperationSource("WEB"); log.setRequestId(requestId); log.setResult(ArLogResult.SUCCESS.getCode()); logService.record(log);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
|
||||||
public List<ArReceiptWriteoff> writeoff(ArReceiptWriteoffRequest request, String operator, String requestId)
|
|
||||||
{
|
|
||||||
if (request == null || request.getReceiptId() == null || request.getAllocations() == null || request.getAllocations().isEmpty())
|
|
||||||
{
|
|
||||||
throw failure(null, "WRITEOFF", "", "核销收款和账单不能为空", operator, requestId);
|
|
||||||
}
|
|
||||||
ArBillLog previous = logMapper.selectSuccessByRequestId(requestId, "WRITEOFF");
|
|
||||||
if (previous != null && isNumber(previous.getObjectId()))
|
|
||||||
{
|
|
||||||
ArReceiptWriteoff existed = writeoffMapper.selectById(Long.valueOf(previous.getObjectId()));
|
|
||||||
if (existed != null) return writeoffMapper.selectByReceiptId(existed.getReceiptId());
|
|
||||||
}
|
|
||||||
ArReceipt receipt = receiptMapper.selectArReceiptByIdForUpdate(request.getReceiptId());
|
|
||||||
if (receipt == null) throw failure(null, "WRITEOFF", String.valueOf(request.getReceiptId()), "收款不存在", operator, requestId);
|
|
||||||
if (ArReceiptStatus.CANCELLED.getCode().equals(receipt.getStatus()))
|
|
||||||
{
|
|
||||||
throw failure(null, "WRITEOFF", String.valueOf(request.getReceiptId()), "收款已取消,不允许核销", operator, requestId);
|
|
||||||
}
|
|
||||||
String currency = StringUtils.isEmpty(request.getCurrency()) ? receipt.getCurrency() : request.getCurrency();
|
|
||||||
if (!currency.equalsIgnoreCase(receipt.getCurrency()))
|
|
||||||
{
|
|
||||||
throw failure(null, "WRITEOFF", String.valueOf(request.getReceiptId()), "收款与核销币种不一致", operator, requestId);
|
|
||||||
}
|
|
||||||
BigDecimal allocationTotal = BigDecimal.ZERO;
|
|
||||||
for (ArReceiptWriteoffRequest.Allocation allocation : request.getAllocations())
|
|
||||||
{
|
|
||||||
if (allocation.getAmount() == null || allocation.getAmount().compareTo(BigDecimal.ZERO) <= 0)
|
|
||||||
{
|
|
||||||
throw failure(null, "WRITEOFF", String.valueOf(request.getReceiptId()), "核销金额必须大于零", operator, requestId);
|
|
||||||
}
|
|
||||||
allocationTotal = allocationTotal.add(allocation.getAmount());
|
|
||||||
}
|
|
||||||
if (allocationTotal.compareTo(nvl(receipt.getUnallocatedAmount())) > 0)
|
|
||||||
{
|
|
||||||
throw failure(null, "WRITEOFF", String.valueOf(request.getReceiptId()), "核销金额超过收款未分配余额", operator, requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<ArReceiptWriteoff> result = new ArrayList<>();
|
|
||||||
BigDecimal remainingReceipt = nvl(receipt.getUnallocatedAmount());
|
|
||||||
Date writeoffDate = request.getWriteoffDate() == null ? new Date() : request.getWriteoffDate();
|
|
||||||
for (ArReceiptWriteoffRequest.Allocation allocation : request.getAllocations())
|
|
||||||
{
|
|
||||||
ArBill bill = billMapper.selectArBillByIdForUpdate(allocation.getArBillId());
|
|
||||||
if (bill == null) throw failure(null, "WRITEOFF", String.valueOf(allocation.getArBillId()), "账单不存在", operator, requestId);
|
|
||||||
ArBillStatus billStatus = ArBillStatus.of(bill.getStatus());
|
|
||||||
if (billStatus == ArBillStatus.VOID) throw failure(bill.getArBillId(), "WRITEOFF", String.valueOf(bill.getArBillId()), "账单已作废,不允许核销", operator, requestId);
|
|
||||||
if (billStatus == null || !billStatus.canWriteOff())
|
|
||||||
{
|
|
||||||
throw failure(bill.getArBillId(), "WRITEOFF", String.valueOf(bill.getArBillId()), "账单尚未确认,不允许核销", operator, requestId);
|
|
||||||
}
|
|
||||||
if (!currency.equalsIgnoreCase(bill.getCurrency())) throw failure(bill.getArBillId(), "WRITEOFF", String.valueOf(bill.getArBillId()), "收款与账单币种不一致", operator, requestId);
|
|
||||||
BigDecimal amount = allocation.getAmount();
|
|
||||||
if (amount.compareTo(nvl(bill.getUnwrittenAmount())) > 0)
|
|
||||||
{
|
|
||||||
throw failure(bill.getArBillId(), "WRITEOFF", String.valueOf(bill.getArBillId()), "核销金额超过账单未核销余额", operator, requestId);
|
|
||||||
}
|
|
||||||
ArReceiptWriteoff writeoff = new ArReceiptWriteoff();
|
|
||||||
writeoff.setReceiptId(receipt.getReceiptId()); writeoff.setArBillId(bill.getArBillId());
|
|
||||||
writeoff.setWriteoffNo("WO" + DateUtils.dateTimeNow("yyyyMMddHHmmss") + IdUtils.fastSimpleUUID().substring(0, 6).toUpperCase());
|
|
||||||
writeoff.setWriteoffAmount(amount); writeoff.setCurrency(currency); writeoff.setWriteoffDate(writeoffDate);
|
|
||||||
writeoff.setStatus(ArWriteoffStatus.EFFECTIVE.getCode()); writeoff.setRemark(request.getRemark()); writeoff.setCreateBy(operator);
|
|
||||||
writeoffMapper.insert(writeoff);
|
|
||||||
remainingReceipt = remainingReceipt.subtract(amount);
|
|
||||||
BigDecimal written = nvl(bill.getWrittenOffAmount()).add(amount);
|
|
||||||
BigDecimal unwritten = nvl(bill.getUnwrittenAmount()).subtract(amount);
|
|
||||||
String nextBillStatus = billStatus(written, nvl(bill.getTotalAmount()), bill.getStatus());
|
|
||||||
if (billMapper.updateArBillAmounts(bill.getArBillId(), written, unwritten,
|
|
||||||
nextBillStatus, operator, bill.getStatus()) != 1)
|
|
||||||
{
|
|
||||||
throw failure(bill.getArBillId(), "WRITEOFF", String.valueOf(bill.getArBillId()), "账单状态已发生变化,请刷新后重试", operator, requestId);
|
|
||||||
}
|
|
||||||
writeoffLog(writeoff, bill, operator, requestId);
|
|
||||||
result.add(writeoff);
|
|
||||||
}
|
|
||||||
receiptMapper.updateBalanceAndStatus(receipt.getReceiptId(), remainingReceipt,
|
|
||||||
remainingReceipt.compareTo(BigDecimal.ZERO) == 0 ? ArReceiptStatus.WRITTEN_OFF.getCode() : ArReceiptStatus.PARTIAL_WRITTEN_OFF.getCode(), operator);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
|
||||||
public void cancelWriteoff(Long writeoffId, String remark, String operator, String requestId)
|
|
||||||
{
|
|
||||||
ArBillLog previous = logMapper.selectSuccessByRequestId(requestId, "WRITEOFF");
|
|
||||||
if (previous != null && "CANCEL_WRITEOFF".equals(previous.getOperationType())) return;
|
|
||||||
ArReceiptWriteoff writeoff = writeoffMapper.selectByIdForUpdate(writeoffId);
|
|
||||||
if (writeoff == null) throw failure(null, "WRITEOFF", String.valueOf(writeoffId), "核销记录不存在", operator, requestId);
|
|
||||||
if (!ArWriteoffStatus.EFFECTIVE.getCode().equals(writeoff.getStatus())) throw failure(writeoff.getArBillId(), "WRITEOFF", String.valueOf(writeoffId), "只有有效核销可以取消", operator, requestId);
|
|
||||||
ArReceipt receipt = receiptMapper.selectArReceiptByIdForUpdate(writeoff.getReceiptId());
|
|
||||||
ArBill bill = billMapper.selectArBillByIdForUpdate(writeoff.getArBillId());
|
|
||||||
if (receipt == null || bill == null) throw failure(writeoff.getArBillId(), "WRITEOFF", String.valueOf(writeoffId), "核销关联的收款或账单不存在", operator, requestId);
|
|
||||||
writeoffMapper.updateCancelled(writeoffId, operator, StringUtils.isEmpty(remark) ? "取消核销" : remark);
|
|
||||||
BigDecimal receiptUnallocated = nvl(receipt.getUnallocatedAmount()).add(writeoff.getWriteoffAmount());
|
|
||||||
receiptMapper.updateBalanceAndStatus(receipt.getReceiptId(), receiptUnallocated,
|
|
||||||
receiptUnallocated.compareTo(nvl(receipt.getReceiptAmount())) == 0 ? ArReceiptStatus.REGISTERED.getCode() : ArReceiptStatus.PARTIAL_WRITTEN_OFF.getCode(), operator);
|
|
||||||
BigDecimal written = nvl(bill.getWrittenOffAmount()).subtract(writeoff.getWriteoffAmount());
|
|
||||||
if (written.compareTo(BigDecimal.ZERO) < 0) written = BigDecimal.ZERO;
|
|
||||||
BigDecimal unwritten = nvl(bill.getTotalAmount()).subtract(written);
|
|
||||||
String nextBillStatus = billStatus(written, nvl(bill.getTotalAmount()), bill.getStatus());
|
|
||||||
if (billMapper.updateArBillAmounts(bill.getArBillId(), written, unwritten,
|
|
||||||
nextBillStatus, operator, bill.getStatus()) != 1)
|
|
||||||
{
|
|
||||||
throw failure(bill.getArBillId(), "WRITEOFF", String.valueOf(bill.getArBillId()), "账单状态已发生变化,请刷新后重试", operator, requestId);
|
|
||||||
}
|
|
||||||
ArBillLog log = new ArBillLog();
|
|
||||||
log.setArBillId(bill.getArBillId()); log.setObjectType("WRITEOFF"); log.setObjectId(String.valueOf(writeoffId));
|
|
||||||
log.setOperationType("CANCEL_WRITEOFF"); log.setOperationDesc(StringUtils.isEmpty(remark) ? "取消核销" : remark);
|
|
||||||
log.setFromStatus(ArWriteoffStatus.EFFECTIVE.getCode()); log.setToStatus(ArWriteoffStatus.CANCELLED.getCode()); log.setOperatorId(operator); log.setOperatorName(operator);
|
|
||||||
log.setOperationSource("WEB"); log.setRequestId(requestId); log.setResult(ArLogResult.SUCCESS.getCode()); logService.record(log);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeoffLog(ArReceiptWriteoff writeoff, ArBill bill, String operator, String requestId)
|
|
||||||
{
|
|
||||||
ArBillLog log = new ArBillLog();
|
|
||||||
log.setArBillId(bill.getArBillId()); log.setObjectType("WRITEOFF"); log.setObjectId(String.valueOf(writeoff.getWriteoffId()));
|
|
||||||
log.setOperationType("WRITEOFF"); log.setOperationDesc("收款核销到账单"); log.setFromStatus(bill.getStatus());
|
|
||||||
log.setToStatus(billStatus(nvl(bill.getWrittenOffAmount()).add(writeoff.getWriteoffAmount()), nvl(bill.getTotalAmount()), bill.getStatus()));
|
|
||||||
log.setOperatorId(operator); log.setOperatorName(operator); log.setOperationSource("WEB"); log.setRequestId(requestId); log.setResult(ArLogResult.SUCCESS.getCode());
|
|
||||||
logService.record(log);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String billStatus(BigDecimal written, BigDecimal total, String current)
|
|
||||||
{
|
|
||||||
if (written.compareTo(BigDecimal.ZERO) == 0)
|
|
||||||
{
|
|
||||||
return ArBillStatus.DRAFT.getCode().equals(current)
|
|
||||||
? ArBillStatus.DRAFT.getCode() : ArBillStatus.CONFIRMED.getCode();
|
|
||||||
}
|
|
||||||
return written.compareTo(total) >= 0
|
|
||||||
? ArBillStatus.WRITTEN_OFF.getCode() : ArBillStatus.PARTIAL_WRITTEN_OFF.getCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
private ServiceException failure(Long billId, String objectType, String message, String operator, String requestId)
|
|
||||||
{
|
|
||||||
logService.recordFailure(billId, objectType, objectType, "RECEIPT", null, message, operator, requestId);
|
|
||||||
return new ServiceException(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ServiceException failure(Long billId, String objectType, String objectId, String message, String operator, String requestId)
|
|
||||||
{
|
|
||||||
logService.recordFailure(billId, objectType, objectId,
|
|
||||||
"RECEIPT".equals(objectType) ? "RECEIPT" : "WRITEOFF", null, message, operator, requestId);
|
|
||||||
return new ServiceException(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
private BigDecimal nvl(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; }
|
|
||||||
private boolean isNumber(String value) { if (StringUtils.isEmpty(value)) return false; try { Long.valueOf(value); return true; } catch (NumberFormatException e) { return false; } }
|
|
||||||
}
|
|
||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
package com.ruoyi.customer.customermoney.api;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客户账户余额变动命令(扣减/回补余额,写余额流水;可选联动已用额度)。
|
||||||
|
*
|
||||||
|
* <p>典型场景:</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>余额核销账单/订单扣款 -> {@link ICustomerAccountApi#deductBalance}(余额减少,默认同步释放已用额度)</li>
|
||||||
|
* <li>收款审核入账/反核销回补/退款 -> {@link ICustomerAccountApi#refundBalance}(余额增加)</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @author ruoyi
|
||||||
|
* @date 2026-09-17
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class BalanceChangeCommand
|
||||||
|
{
|
||||||
|
/** 客户ID(必填) */
|
||||||
|
private Long customerId;
|
||||||
|
|
||||||
|
/** 币种(流水原币口径;为空按本位币 CNY 处理) */
|
||||||
|
private String currency;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实际加锁/变动的账户币种(为空时取 {@link #currency})。
|
||||||
|
* 外币收款折算入账场景:账户只有CNY,需锁CNY账户按 {@link #baseAmount} 变动余额,
|
||||||
|
* 但流水币种仍保留收款原币 {@link #currency}。
|
||||||
|
*/
|
||||||
|
private String accountCurrency;
|
||||||
|
|
||||||
|
/** 变动金额,原币口径(必须大于0,恒为正数) */
|
||||||
|
private BigDecimal amount;
|
||||||
|
|
||||||
|
/** 对CNY汇率(1原币=?CNY),为空按1处理 */
|
||||||
|
private BigDecimal exchangeRate;
|
||||||
|
|
||||||
|
/** 本位币变动金额(实际参与余额变动,为空时取 {@link #amount}) */
|
||||||
|
private BigDecimal baseAmount;
|
||||||
|
|
||||||
|
/** 业务类型,取 {@link CustomerFundBizType} 常量(必填) */
|
||||||
|
private String bizType;
|
||||||
|
|
||||||
|
/** 关联业务单据ID */
|
||||||
|
private Long bizId;
|
||||||
|
|
||||||
|
/** 关联业务单据号 */
|
||||||
|
private String bizNo;
|
||||||
|
|
||||||
|
/** 流水备注 */
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
/** 扣减余额时是否同步释放等额已用额度(账单核销场景为true),默认true */
|
||||||
|
private Boolean releaseUsedCredit = Boolean.TRUE;
|
||||||
|
|
||||||
|
/** 回补余额时是否同步重新占用等额已用额度(反核销场景为true,收款入账为false),默认false */
|
||||||
|
private Boolean reoccupyUsedCredit = Boolean.FALSE;
|
||||||
|
|
||||||
|
/** 扣减时是否校验余额充足(不足抛异常),默认true */
|
||||||
|
private Boolean checkBalanceEnough = Boolean.TRUE;
|
||||||
|
}
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
package com.ruoyi.customer.customermoney.api;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 授信额度占用/释放命令(变动客户账户已用额度 used_credit,并写额度流水)。
|
||||||
|
*
|
||||||
|
* <p>典型场景:</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>计费订单开单/应收账单确认 -> {@link ICustomerAccountApi#occupyCredit}(已用额度增加)</li>
|
||||||
|
* <li>账单作废/计费变动核减 -> {@link ICustomerAccountApi#releaseCredit}(已用额度减少)</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @author ruoyi
|
||||||
|
* @date 2026-09-17
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class CreditUsageCommand
|
||||||
|
{
|
||||||
|
/** 客户ID(必填) */
|
||||||
|
private Long customerId;
|
||||||
|
|
||||||
|
/** 币种(为空按本位币 CNY 处理) */
|
||||||
|
private String currency;
|
||||||
|
|
||||||
|
/** 变动金额(必须大于0,恒为正数) */
|
||||||
|
private BigDecimal amount;
|
||||||
|
|
||||||
|
/** 业务来源类型,取 {@link CustomerFundBizType} 常量(必填) */
|
||||||
|
private String bizType;
|
||||||
|
|
||||||
|
/** 关联业务单据ID(如账单ID/订单ID) */
|
||||||
|
private Long bizId;
|
||||||
|
|
||||||
|
/** 关联业务单据号(如账单号/订单号,写入流水便于追溯) */
|
||||||
|
private String bizNo;
|
||||||
|
|
||||||
|
/** 流水原因(为空按业务类型+单据号自动生成) */
|
||||||
|
private String reason;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 占用时是否校验可用额度(授信总额-已用 >= 本次占用额)。
|
||||||
|
* true=余额/额度不足直接抛异常(计费开单等新业务场景默认值);
|
||||||
|
* false=允许超出总额度占用(兼容历史账单确认口径)。
|
||||||
|
* 释放场景该参数无意义。
|
||||||
|
*/
|
||||||
|
private Boolean checkAvailable = Boolean.TRUE;
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
package com.ruoyi.customer.customermoney.api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客户资金变动业务来源类型(写入余额流水 biz_type / 额度流水 biz_type)。
|
||||||
|
* 各业务模块调用 {@link ICustomerAccountApi} 时传本类常量,便于按来源单据反查流水。
|
||||||
|
*
|
||||||
|
* @author ruoyi
|
||||||
|
* @date 2026-09-17
|
||||||
|
*/
|
||||||
|
public final class CustomerFundBizType
|
||||||
|
{
|
||||||
|
/** 收款单(余额入账) */
|
||||||
|
public static final String RECEIPT = "RECEIPT";
|
||||||
|
|
||||||
|
/** 应收账单(确认占用授信 / 作废释放授信) */
|
||||||
|
public static final String AR_BILL = "AR_BILL";
|
||||||
|
|
||||||
|
/** 账单余额核销(余额扣款 / 反核销回补) */
|
||||||
|
public static final String BILL_PAY = "BILL_PAY";
|
||||||
|
|
||||||
|
/** 计费订单(开单占用授信 / 计费变动核减释放) */
|
||||||
|
public static final String ORDER = "ORDER";
|
||||||
|
|
||||||
|
private CustomerFundBizType()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
+84
@@ -0,0 +1,84 @@
|
|||||||
|
package com.ruoyi.customer.customermoney.api;
|
||||||
|
|
||||||
|
import com.ruoyi.customer.customermoney.domain.CustomerAccount;
|
||||||
|
import com.ruoyi.customer.customermoney.domain.CustomerBalanceFlow;
|
||||||
|
import com.ruoyi.customer.customermoney.domain.CustomerCreditFlow;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客户资金公共服务:客户余额扣减/回补、授信额度占用/释放的统一入口。
|
||||||
|
*
|
||||||
|
* <p>客户余额、授信额度及其流水(客户模块资产)的所有变动都必须经过本接口,
|
||||||
|
* 其他模块(应收账单、计费订单/计费变动等)不得直接更新 customer_account /
|
||||||
|
* 直接插入流水表。每个方法在同一事务内完成:行锁账户 -> 乐观锁变动 -> 写流水,
|
||||||
|
* 调用方自身的事务会与本操作共享事务(默认 REQUIRED),任一失败整体回滚。</p>
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>单据形成应收/计费开单:{@link #occupyCredit} 占用授信(used_credit 增加)</li>
|
||||||
|
* <li>单据作废/计费核减:{@link #releaseCredit} 释放授信(used_credit 减少)</li>
|
||||||
|
* <li>用账户余额支付/核销:{@link #deductBalance} 扣余额(可同步释放已用额度)</li>
|
||||||
|
* <li>收款入账/退款/反核销回补:{@link #refundBalance} 加余额(可同步重新占用)</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @author ruoyi
|
||||||
|
* @date 2026-09-17
|
||||||
|
*/
|
||||||
|
public interface ICustomerAccountApi
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 普通查询客户某币种账户(不加锁,不存在返回null),供业务模块判断余额/可用额度。
|
||||||
|
*/
|
||||||
|
CustomerAccount getAccount(Long customerId, String currency);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 事务内行锁获取客户CNY账户,不存在则按客户档案额度自动初始化(不处理临时额度过期)。
|
||||||
|
* 调用方拿到后如需最新临时额度,可再调 {@link #expireTempCreditIfNeeded}。
|
||||||
|
*/
|
||||||
|
CustomerAccount lockCnyAccount(Long customerId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 临时额度到期自动失效(额度清零 + 回写客户档案 + EXPIRE 额度流水)。
|
||||||
|
* 入参必须是同一事务内行锁查出的账户;未过期时不做任何操作。
|
||||||
|
*/
|
||||||
|
void expireTempCreditIfNeeded(CustomerAccount account);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新建客户时初始化默认CNY账户(余额0,默认总额度1000 CNY,档案有值按档案),
|
||||||
|
* 写 INIT 额度流水;CNY账户已存在时不重复创建。
|
||||||
|
*/
|
||||||
|
int initDefaultCnyAccount(Long customerId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 占用授信额度:used_credit += amount,写 OCCUPY 额度流水(关联来源单据)。
|
||||||
|
* checkAvailable=true 时可用额度不足抛 ServiceException;CNY账户不存在自动初始化,
|
||||||
|
* 外币账户不存在时静默跳过(返回null,兼容历史口径)。
|
||||||
|
*
|
||||||
|
* @return 已落库的额度流水(含ID);账户不存在跳过场景返回null
|
||||||
|
*/
|
||||||
|
CustomerCreditFlow occupyCredit(CreditUsageCommand command);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 释放授信额度:used_credit -= amount(不小于0),写 RELEASE 额度流水。
|
||||||
|
* 账户不存在时静默跳过(返回null)。
|
||||||
|
*
|
||||||
|
* @return 已落库的额度流水(含ID);账户不存在跳过场景返回null
|
||||||
|
*/
|
||||||
|
CustomerCreditFlow releaseCredit(CreditUsageCommand command);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扣减账户余额:balance -= amount,写 -1 余额流水;
|
||||||
|
* releaseUsedCredit=true 时同步释放等额已用额度(余额清偿应收场景)。
|
||||||
|
* 账户不存在或余额不足(checkBalanceEnough=true)抛 ServiceException。
|
||||||
|
*
|
||||||
|
* @return 已落库的余额流水(含ID、账户ID)
|
||||||
|
*/
|
||||||
|
CustomerBalanceFlow deductBalance(BalanceChangeCommand command);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回补/入账账户余额:balance += amount,写 +1 余额流水;
|
||||||
|
* reoccupyUsedCredit=true 时同步加回等额已用额度(反核销场景),
|
||||||
|
* 收款审核入账等场景保持false。
|
||||||
|
*
|
||||||
|
* @return 已落库的余额流水(含ID、账户ID)
|
||||||
|
*/
|
||||||
|
CustomerBalanceFlow refundBalance(BalanceChangeCommand command);
|
||||||
|
}
|
||||||
+566
@@ -0,0 +1,566 @@
|
|||||||
|
package com.ruoyi.customer.customermoney.api.impl;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
|
import com.ruoyi.common.utils.SecurityUtils;
|
||||||
|
import com.ruoyi.customer.customerinfo.domain.BmsCustomer;
|
||||||
|
import com.ruoyi.customer.customerinfo.mapper.BmsCustomerMapper;
|
||||||
|
import com.ruoyi.customer.customermoney.api.BalanceChangeCommand;
|
||||||
|
import com.ruoyi.customer.customermoney.api.CreditUsageCommand;
|
||||||
|
import com.ruoyi.customer.customermoney.api.CustomerFundBizType;
|
||||||
|
import com.ruoyi.customer.customermoney.api.ICustomerAccountApi;
|
||||||
|
import com.ruoyi.customer.customermoney.domain.CustomerAccount;
|
||||||
|
import com.ruoyi.customer.customermoney.domain.CustomerBalanceFlow;
|
||||||
|
import com.ruoyi.customer.customermoney.domain.CustomerCreditFlow;
|
||||||
|
import com.ruoyi.customer.customermoney.mapper.CustomerMoneyMapper;
|
||||||
|
import com.ruoyi.system.domain.BmsExchangeRate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客户资金公共服务实现:客户账户余额/授信额度变动与流水落库的唯一收口。
|
||||||
|
* 加锁顺序统一为「业务单据(调用方持锁)-> 客户账户(本服务行锁)」。
|
||||||
|
*
|
||||||
|
* @author ruoyi
|
||||||
|
* @date 2026-09-17
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class CustomerAccountApiImpl implements ICustomerAccountApi
|
||||||
|
{
|
||||||
|
private static final DateTimeFormatter NO_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");
|
||||||
|
|
||||||
|
/** 本位币币种 */
|
||||||
|
private static final String BASE_CURRENCY = BmsExchangeRate.BASE_CURRENCY;
|
||||||
|
/** 新客户默认总额度(CNY) */
|
||||||
|
private static final BigDecimal DEFAULT_CREDIT = new BigDecimal("1000");
|
||||||
|
/** 金额保留位数 */
|
||||||
|
private static final int MONEY_SCALE = 2;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private CustomerMoneyMapper customerMoneyMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private BmsCustomerMapper bmsCustomerMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CustomerAccount getAccount(Long customerId, String currency)
|
||||||
|
{
|
||||||
|
if (customerId == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return customerMoneyMapper.selectAccountByCustomerCurrency(customerId, resolveCurrency(currency));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CustomerAccount lockCnyAccount(Long customerId)
|
||||||
|
{
|
||||||
|
if (customerId == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("客户ID不能为空");
|
||||||
|
}
|
||||||
|
return getOrInitCnyAccount(customerId, SecurityUtils.getUsername());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public int initDefaultCnyAccount(Long customerId)
|
||||||
|
{
|
||||||
|
if (customerId == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("客户ID不能为空");
|
||||||
|
}
|
||||||
|
String username = SecurityUtils.getUsername();
|
||||||
|
CustomerAccount account = customerMoneyMapper.selectAccountForUpdate(customerId, BASE_CURRENCY);
|
||||||
|
if (account != null)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
BmsCustomer customer = bmsCustomerMapper.selectRawCreditByCustomerId(customerId);
|
||||||
|
if (customer == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("客户不存在");
|
||||||
|
}
|
||||||
|
BigDecimal initCredit = nz(customer.getCreditEd());
|
||||||
|
if (initCredit.compareTo(BigDecimal.ZERO) <= 0)
|
||||||
|
{
|
||||||
|
initCredit = DEFAULT_CREDIT;
|
||||||
|
}
|
||||||
|
BigDecimal initTemp = nz(customer.getTempEd());
|
||||||
|
account = createCnyAccount(customerId, initCredit, initTemp, username);
|
||||||
|
// 档案额度与实际初始额度保持一致
|
||||||
|
bmsCustomerMapper.updateCustomerCredit(customerId, initCredit, initTemp, username);
|
||||||
|
writeInitFlow(account, CustomerCreditFlow.CREDIT_TYPE_CREDIT,
|
||||||
|
initCredit, null, null, username, "新客户默认初始化额度1000 CNY");
|
||||||
|
if (initTemp.compareTo(BigDecimal.ZERO) > 0)
|
||||||
|
{
|
||||||
|
writeInitFlow(account, CustomerCreditFlow.CREDIT_TYPE_TEMP,
|
||||||
|
initTemp, null, null, username, "新客户初始化临时额度");
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void expireTempCreditIfNeeded(CustomerAccount account)
|
||||||
|
{
|
||||||
|
if (account == null || !isTempCreditExpired(account))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String username = SecurityUtils.getUsername();
|
||||||
|
BigDecimal before = nz(account.getExtraCredit());
|
||||||
|
account.setExtraCredit(BigDecimal.ZERO);
|
||||||
|
account.setUpdateBy(username);
|
||||||
|
int rows = customerMoneyMapper.updateAccountCredit(account);
|
||||||
|
if (rows == 0)
|
||||||
|
{
|
||||||
|
throw new ServiceException("账户额度已被其他操作变动,请刷新后重试");
|
||||||
|
}
|
||||||
|
account.setVersion(account.getVersion() + 1);
|
||||||
|
bmsCustomerMapper.updateCustomerCredit(account.getCustomerId(),
|
||||||
|
nz(account.getTotalCredit()), BigDecimal.ZERO, username);
|
||||||
|
|
||||||
|
CustomerCreditFlow flow = new CustomerCreditFlow();
|
||||||
|
flow.setFlowNo(generateNo("EDLS"));
|
||||||
|
flow.setCustomerId(account.getCustomerId());
|
||||||
|
flow.setAccountId(account.getId());
|
||||||
|
flow.setCreditType(CustomerCreditFlow.CREDIT_TYPE_TEMP);
|
||||||
|
flow.setChangeType(CustomerCreditFlow.CHANGE_EXPIRE);
|
||||||
|
flow.setDirection(-1);
|
||||||
|
flow.setBeforeAmount(before);
|
||||||
|
flow.setChangeAmount(before);
|
||||||
|
flow.setAfterAmount(BigDecimal.ZERO);
|
||||||
|
flow.setUsedCredit(nz(account.getUsedCredit()));
|
||||||
|
flow.setEffectiveDate(account.getExtraEffectiveDate());
|
||||||
|
flow.setExpireDate(account.getExtraExpireDate());
|
||||||
|
flow.setReason("临时额度到期自动失效");
|
||||||
|
flow.setOperatorId(SecurityUtils.getUserId());
|
||||||
|
flow.setCreateBy(username);
|
||||||
|
flow.setRemark("系统在访问账户时检测到临时额度已过失效日期,自动置零");
|
||||||
|
flow.setDelFlag("0");
|
||||||
|
customerMoneyMapper.insertCreditFlow(flow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= 授信额度占用 / 释放 ================= */
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public CustomerCreditFlow occupyCredit(CreditUsageCommand command)
|
||||||
|
{
|
||||||
|
validateUsageCommand(command);
|
||||||
|
String currency = resolveCurrency(command.getCurrency());
|
||||||
|
String username = SecurityUtils.getUsername();
|
||||||
|
Long userId = SecurityUtils.getUserId();
|
||||||
|
CustomerAccount account = lockAccountForUsage(command.getCustomerId(), currency, username);
|
||||||
|
if (account == null)
|
||||||
|
{
|
||||||
|
// 外币账户尚未开立时不自动占用,与历史账单口径保持一致(CNY账户会自动初始化)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
BigDecimal amount = money(command.getAmount());
|
||||||
|
BigDecimal beforeUsed = nz(account.getUsedCredit());
|
||||||
|
BigDecimal totalAvailable = nz(account.getTotalCredit()).add(nz(account.getExtraCredit()))
|
||||||
|
.subtract(beforeUsed);
|
||||||
|
if (Boolean.TRUE.equals(command.getCheckAvailable()) && totalAvailable.compareTo(amount) < 0)
|
||||||
|
{
|
||||||
|
throw new ServiceException("客户可用额度不足,当前可用额度:" + totalAvailable.toPlainString()
|
||||||
|
+ " " + currency + ",本次需占用:" + amount.toPlainString());
|
||||||
|
}
|
||||||
|
BigDecimal afterUsed = beforeUsed.add(amount).setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||||
|
account.setUsedCredit(afterUsed);
|
||||||
|
account.setUpdateBy(username);
|
||||||
|
if (customerMoneyMapper.updateAccountBalance(account) == 0)
|
||||||
|
{
|
||||||
|
throw new ServiceException("账户已被其他操作变动,请刷新后重试");
|
||||||
|
}
|
||||||
|
return writeUsedCreditFlow(account, command, CustomerCreditFlow.CHANGE_OCCUPY, 1,
|
||||||
|
beforeUsed, afterUsed, amount, username, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public CustomerCreditFlow releaseCredit(CreditUsageCommand command)
|
||||||
|
{
|
||||||
|
validateUsageCommand(command);
|
||||||
|
String currency = resolveCurrency(command.getCurrency());
|
||||||
|
String username = SecurityUtils.getUsername();
|
||||||
|
Long userId = SecurityUtils.getUserId();
|
||||||
|
CustomerAccount account = lockAccountForUsage(command.getCustomerId(), currency, username);
|
||||||
|
if (account == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
BigDecimal amount = money(command.getAmount());
|
||||||
|
BigDecimal beforeUsed = nz(account.getUsedCredit());
|
||||||
|
BigDecimal afterUsed = beforeUsed.subtract(amount).max(BigDecimal.ZERO)
|
||||||
|
.setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||||
|
if (afterUsed.compareTo(beforeUsed) == 0)
|
||||||
|
{
|
||||||
|
// 无实际占用可释放,仍返回null不产生流水
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
account.setUsedCredit(afterUsed);
|
||||||
|
account.setUpdateBy(username);
|
||||||
|
if (customerMoneyMapper.updateAccountBalance(account) == 0)
|
||||||
|
{
|
||||||
|
throw new ServiceException("账户已被其他操作变动,请刷新后重试");
|
||||||
|
}
|
||||||
|
return writeUsedCreditFlow(account, command, CustomerCreditFlow.CHANGE_RELEASE, -1,
|
||||||
|
beforeUsed, afterUsed, amount, username, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= 余额扣减 / 回补 ================= */
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public CustomerBalanceFlow deductBalance(BalanceChangeCommand command)
|
||||||
|
{
|
||||||
|
validateBalanceCommand(command);
|
||||||
|
String flowCurrency = resolveCurrency(command.getCurrency());
|
||||||
|
String accountCurrency = StringUtils.hasText(command.getAccountCurrency())
|
||||||
|
? resolveCurrency(command.getAccountCurrency()) : flowCurrency;
|
||||||
|
String username = SecurityUtils.getUsername();
|
||||||
|
Long userId = SecurityUtils.getUserId();
|
||||||
|
CustomerAccount account = lockAccountForChange(command.getCustomerId(), accountCurrency, username, "扣款");
|
||||||
|
|
||||||
|
BigDecimal originalAmount = money(command.getAmount());
|
||||||
|
// 账户币种实际变动额:折算入账时取本位币金额,其余场景等于原币金额
|
||||||
|
BigDecimal changeAmount = command.getBaseAmount() != null ? money(command.getBaseAmount()) : originalAmount;
|
||||||
|
BigDecimal beforeBalance = nz(account.getBalance());
|
||||||
|
if (!Boolean.FALSE.equals(command.getCheckBalanceEnough()) && beforeBalance.compareTo(changeAmount) < 0)
|
||||||
|
{
|
||||||
|
throw new ServiceException("账户余额不足,当前" + accountCurrency + "余额:" + beforeBalance.toPlainString()
|
||||||
|
+ ",本次需扣减:" + changeAmount.toPlainString());
|
||||||
|
}
|
||||||
|
BigDecimal afterBalance = beforeBalance.subtract(changeAmount);
|
||||||
|
BigDecimal frozenBalance = nz(account.getFrozenBalance());
|
||||||
|
account.setBalance(afterBalance);
|
||||||
|
if (Boolean.TRUE.equals(command.getReleaseUsedCredit()))
|
||||||
|
{
|
||||||
|
account.setUsedCredit(nz(account.getUsedCredit()).subtract(changeAmount).max(BigDecimal.ZERO)
|
||||||
|
.setScale(MONEY_SCALE, RoundingMode.HALF_UP));
|
||||||
|
}
|
||||||
|
account.setUpdateBy(username);
|
||||||
|
if (customerMoneyMapper.updateAccountBalance(account) == 0)
|
||||||
|
{
|
||||||
|
throw new ServiceException("账户余额已被其他操作变动,请刷新后重试");
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomerBalanceFlow flow = newBalanceFlow(account, command, flowCurrency, originalAmount, -1,
|
||||||
|
beforeBalance, afterBalance, frozenBalance, frozenBalance, username, userId);
|
||||||
|
customerMoneyMapper.insertFlow(flow);
|
||||||
|
return flow;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public CustomerBalanceFlow refundBalance(BalanceChangeCommand command)
|
||||||
|
{
|
||||||
|
validateBalanceCommand(command);
|
||||||
|
String flowCurrency = resolveCurrency(command.getCurrency());
|
||||||
|
String accountCurrency = StringUtils.hasText(command.getAccountCurrency())
|
||||||
|
? resolveCurrency(command.getAccountCurrency()) : flowCurrency;
|
||||||
|
String username = SecurityUtils.getUsername();
|
||||||
|
Long userId = SecurityUtils.getUserId();
|
||||||
|
CustomerAccount account = lockAccountForChange(command.getCustomerId(), accountCurrency, username, "回补");
|
||||||
|
|
||||||
|
BigDecimal originalAmount = money(command.getAmount());
|
||||||
|
// 账户币种实际变动额:折算入账时取本位币金额,其余场景等于原币金额
|
||||||
|
BigDecimal changeAmount = command.getBaseAmount() != null ? money(command.getBaseAmount()) : originalAmount;
|
||||||
|
BigDecimal beforeBalance = nz(account.getBalance());
|
||||||
|
BigDecimal afterBalance = beforeBalance.add(changeAmount);
|
||||||
|
BigDecimal frozenBalance = nz(account.getFrozenBalance());
|
||||||
|
account.setBalance(afterBalance);
|
||||||
|
if (Boolean.TRUE.equals(command.getReoccupyUsedCredit()))
|
||||||
|
{
|
||||||
|
// 反核销回补:账单恢复未清偿,授信占用同步加回(允许出现高于总额度的占用存量)
|
||||||
|
account.setUsedCredit(nz(account.getUsedCredit()).add(changeAmount)
|
||||||
|
.setScale(MONEY_SCALE, RoundingMode.HALF_UP));
|
||||||
|
}
|
||||||
|
account.setUpdateBy(username);
|
||||||
|
if (customerMoneyMapper.updateAccountBalance(account) == 0)
|
||||||
|
{
|
||||||
|
throw new ServiceException("账户余额已被其他操作变动,请刷新后重试");
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomerBalanceFlow flow = newBalanceFlow(account, command, flowCurrency, originalAmount, 1,
|
||||||
|
beforeBalance, afterBalance, frozenBalance, frozenBalance, username, userId);
|
||||||
|
customerMoneyMapper.insertFlow(flow);
|
||||||
|
return flow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= 内部实现 ================= */
|
||||||
|
|
||||||
|
/** 额度占用/释放写额度流水(before/after 为已用额度快照) */
|
||||||
|
private CustomerCreditFlow writeUsedCreditFlow(CustomerAccount account, CreditUsageCommand command,
|
||||||
|
String changeType, int direction, BigDecimal beforeUsed, BigDecimal afterUsed,
|
||||||
|
BigDecimal amount, String username, Long userId)
|
||||||
|
{
|
||||||
|
CustomerCreditFlow flow = new CustomerCreditFlow();
|
||||||
|
flow.setFlowNo(generateNo("EDLS"));
|
||||||
|
flow.setCustomerId(account.getCustomerId());
|
||||||
|
flow.setAccountId(account.getId());
|
||||||
|
flow.setCreditType(CustomerCreditFlow.CREDIT_TYPE_CREDIT);
|
||||||
|
flow.setChangeType(changeType);
|
||||||
|
flow.setBizType(command.getBizType());
|
||||||
|
flow.setBizId(command.getBizId());
|
||||||
|
flow.setBizNo(command.getBizNo());
|
||||||
|
flow.setDirection(direction);
|
||||||
|
flow.setBeforeAmount(beforeUsed);
|
||||||
|
flow.setChangeAmount(amount);
|
||||||
|
flow.setAfterAmount(afterUsed);
|
||||||
|
flow.setUsedCredit(afterUsed);
|
||||||
|
flow.setReason(resolveUsageReason(command, changeType));
|
||||||
|
flow.setOperatorId(userId);
|
||||||
|
flow.setCreateBy(username);
|
||||||
|
flow.setDelFlag("0");
|
||||||
|
customerMoneyMapper.insertCreditFlow(flow);
|
||||||
|
return flow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 余额变动写余额流水(原币金额留痕,本位币金额参与余额变动) */
|
||||||
|
private CustomerBalanceFlow newBalanceFlow(CustomerAccount account, BalanceChangeCommand command,
|
||||||
|
String currency, BigDecimal amount, int direction,
|
||||||
|
BigDecimal beforeBalance, BigDecimal afterBalance,
|
||||||
|
BigDecimal beforeFrozen, BigDecimal afterFrozen,
|
||||||
|
String username, Long userId)
|
||||||
|
{
|
||||||
|
BigDecimal rate = command.getExchangeRate() != null
|
||||||
|
&& command.getExchangeRate().compareTo(BigDecimal.ZERO) > 0
|
||||||
|
? command.getExchangeRate() : BigDecimal.ONE;
|
||||||
|
BigDecimal baseAmount = command.getBaseAmount() != null ? command.getBaseAmount()
|
||||||
|
: amount.multiply(rate).setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||||
|
CustomerBalanceFlow flow = new CustomerBalanceFlow();
|
||||||
|
flow.setFlowNo(generateNo("LS"));
|
||||||
|
flow.setCustomerId(account.getCustomerId());
|
||||||
|
flow.setAccountId(account.getId());
|
||||||
|
flow.setCurrency(currency);
|
||||||
|
flow.setExchangeRate(rate.setScale(8, RoundingMode.HALF_UP));
|
||||||
|
flow.setBaseAmount(baseAmount);
|
||||||
|
flow.setBizType(command.getBizType());
|
||||||
|
flow.setBizId(command.getBizId());
|
||||||
|
flow.setBizNo(command.getBizNo());
|
||||||
|
flow.setDirection(direction);
|
||||||
|
flow.setAmount(amount);
|
||||||
|
flow.setBeforeBalance(beforeBalance);
|
||||||
|
flow.setAfterBalance(afterBalance);
|
||||||
|
flow.setBeforeFrozenBalance(beforeFrozen);
|
||||||
|
flow.setAfterFrozenBalance(afterFrozen);
|
||||||
|
flow.setOperatorId(userId);
|
||||||
|
flow.setCreateBy(username);
|
||||||
|
flow.setRemark(command.getRemark());
|
||||||
|
return flow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 占用/释放场景锁账户:CNY 不存在自动初始化(先处理临时额度过期),
|
||||||
|
* 外币账户不存在返回null(调用方静默跳过)。
|
||||||
|
*/
|
||||||
|
private CustomerAccount lockAccountForUsage(Long customerId, String currency, String username)
|
||||||
|
{
|
||||||
|
if (BASE_CURRENCY.equals(currency))
|
||||||
|
{
|
||||||
|
CustomerAccount account = getOrInitCnyAccount(customerId, username);
|
||||||
|
expireTempCreditIfNeeded(account);
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
return customerMoneyMapper.selectAccountForUpdate(customerId, currency);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 余额变动场景锁账户:账户不存在直接抛异常(余额扣款/回补必须有账户)。
|
||||||
|
*/
|
||||||
|
private CustomerAccount lockAccountForChange(Long customerId, String currency, String username, String action)
|
||||||
|
{
|
||||||
|
CustomerAccount account;
|
||||||
|
if (BASE_CURRENCY.equals(currency))
|
||||||
|
{
|
||||||
|
account = getOrInitCnyAccount(customerId, username);
|
||||||
|
expireTempCreditIfNeeded(account);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
account = customerMoneyMapper.selectAccountForUpdate(customerId, currency);
|
||||||
|
}
|
||||||
|
if (account == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("客户没有" + currency + "账户,无法" + action);
|
||||||
|
}
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 行锁获取客户CNY账户,不存在则按客户档案当前额度初始化创建 */
|
||||||
|
private CustomerAccount getOrInitCnyAccount(Long customerId, String username)
|
||||||
|
{
|
||||||
|
CustomerAccount account = customerMoneyMapper.selectAccountForUpdate(customerId, BASE_CURRENCY);
|
||||||
|
if (account == null)
|
||||||
|
{
|
||||||
|
BmsCustomer customer = bmsCustomerMapper.selectRawCreditByCustomerId(customerId);
|
||||||
|
if (customer == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("客户不存在");
|
||||||
|
}
|
||||||
|
BigDecimal initCredit = nz(customer.getCreditEd());
|
||||||
|
BigDecimal initTemp = nz(customer.getTempEd());
|
||||||
|
account = createCnyAccount(customerId, initCredit, initTemp, username);
|
||||||
|
writeInitFlow(account, CustomerCreditFlow.CREDIT_TYPE_CREDIT,
|
||||||
|
initCredit, null, null, username, "首次创建CNY账户,从客户档案同步初始额度");
|
||||||
|
if (initTemp.compareTo(BigDecimal.ZERO) > 0)
|
||||||
|
{
|
||||||
|
writeInitFlow(account, CustomerCreditFlow.CREDIT_TYPE_TEMP,
|
||||||
|
initTemp, null, null, username, "首次创建CNY账户,从客户档案同步临时额度");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建CNY账户(余额0,已用0) */
|
||||||
|
private CustomerAccount createCnyAccount(Long customerId, BigDecimal totalCredit,
|
||||||
|
BigDecimal extraCredit, String username)
|
||||||
|
{
|
||||||
|
CustomerAccount account = new CustomerAccount();
|
||||||
|
account.setCustomerId(customerId);
|
||||||
|
account.setCurrency(BASE_CURRENCY);
|
||||||
|
account.setBalance(BigDecimal.ZERO);
|
||||||
|
account.setFrozenBalance(BigDecimal.ZERO);
|
||||||
|
account.setTotalCredit(totalCredit);
|
||||||
|
account.setExtraCredit(extraCredit);
|
||||||
|
account.setUsedCredit(BigDecimal.ZERO);
|
||||||
|
account.setTax(BigDecimal.ZERO);
|
||||||
|
account.setVersion(0);
|
||||||
|
account.setCreateBy(username);
|
||||||
|
customerMoneyMapper.insertAccount(account);
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 账户首次初始化时写额度 INIT 流水(从客户档案额度同步) */
|
||||||
|
private void writeInitFlow(CustomerAccount account, String creditType,
|
||||||
|
BigDecimal initAmount, Date effectiveDate, Date expireDate,
|
||||||
|
String username, String remark)
|
||||||
|
{
|
||||||
|
CustomerCreditFlow flow = new CustomerCreditFlow();
|
||||||
|
flow.setFlowNo(generateNo("EDLS"));
|
||||||
|
flow.setCustomerId(account.getCustomerId());
|
||||||
|
flow.setAccountId(account.getId());
|
||||||
|
flow.setCreditType(creditType);
|
||||||
|
flow.setChangeType(CustomerCreditFlow.CHANGE_INIT);
|
||||||
|
flow.setDirection(initAmount.compareTo(BigDecimal.ZERO) > 0 ? 1 : 0);
|
||||||
|
flow.setBeforeAmount(BigDecimal.ZERO);
|
||||||
|
flow.setChangeAmount(initAmount);
|
||||||
|
flow.setAfterAmount(initAmount);
|
||||||
|
flow.setUsedCredit(nz(account.getUsedCredit()));
|
||||||
|
flow.setEffectiveDate(effectiveDate);
|
||||||
|
flow.setExpireDate(expireDate);
|
||||||
|
flow.setReason("账户初始化");
|
||||||
|
flow.setOperatorId(SecurityUtils.getUserId());
|
||||||
|
flow.setCreateBy(username);
|
||||||
|
flow.setRemark(remark);
|
||||||
|
flow.setDelFlag("0");
|
||||||
|
customerMoneyMapper.insertCreditFlow(flow);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateUsageCommand(CreditUsageCommand command)
|
||||||
|
{
|
||||||
|
if (command == null || command.getCustomerId() == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("客户ID不能为空");
|
||||||
|
}
|
||||||
|
if (!StringUtils.hasText(command.getBizType()))
|
||||||
|
{
|
||||||
|
throw new ServiceException("业务类型不能为空");
|
||||||
|
}
|
||||||
|
if (command.getAmount() == null || command.getAmount().compareTo(BigDecimal.ZERO) <= 0)
|
||||||
|
{
|
||||||
|
throw new ServiceException("额度变动金额必须大于0");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateBalanceCommand(BalanceChangeCommand command)
|
||||||
|
{
|
||||||
|
if (command == null || command.getCustomerId() == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("客户ID不能为空");
|
||||||
|
}
|
||||||
|
if (!StringUtils.hasText(command.getBizType()))
|
||||||
|
{
|
||||||
|
throw new ServiceException("业务类型不能为空");
|
||||||
|
}
|
||||||
|
if (command.getAmount() == null || command.getAmount().compareTo(BigDecimal.ZERO) <= 0)
|
||||||
|
{
|
||||||
|
throw new ServiceException("余额变动金额必须大于0");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveUsageReason(CreditUsageCommand command, String changeType)
|
||||||
|
{
|
||||||
|
if (StringUtils.hasText(command.getReason()))
|
||||||
|
{
|
||||||
|
return command.getReason().trim();
|
||||||
|
}
|
||||||
|
String action = CustomerCreditFlow.CHANGE_OCCUPY.equals(changeType) ? "占用授信额度" : "释放授信额度";
|
||||||
|
String biz = bizTypeLabel(command.getBizType());
|
||||||
|
String reason = biz + action;
|
||||||
|
if (StringUtils.hasText(command.getBizNo()))
|
||||||
|
{
|
||||||
|
reason = reason + ":" + command.getBizNo();
|
||||||
|
}
|
||||||
|
return reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String bizTypeLabel(String bizType)
|
||||||
|
{
|
||||||
|
if (CustomerFundBizType.AR_BILL.equals(bizType))
|
||||||
|
{
|
||||||
|
return "应收账单";
|
||||||
|
}
|
||||||
|
if (CustomerFundBizType.ORDER.equals(bizType))
|
||||||
|
{
|
||||||
|
return "计费订单";
|
||||||
|
}
|
||||||
|
if (CustomerFundBizType.BILL_PAY.equals(bizType))
|
||||||
|
{
|
||||||
|
return "账单核销";
|
||||||
|
}
|
||||||
|
return "业务";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 临时额度是否已过期:有失效日期且今天已晚于失效日(失效日当天仍有效) */
|
||||||
|
private boolean isTempCreditExpired(CustomerAccount account)
|
||||||
|
{
|
||||||
|
return account != null
|
||||||
|
&& nz(account.getExtraCredit()).compareTo(BigDecimal.ZERO) > 0
|
||||||
|
&& account.getExtraExpireDate() != null
|
||||||
|
&& LocalDate.now().isAfter(account.getExtraExpireDate().toInstant()
|
||||||
|
.atZone(java.time.ZoneId.systemDefault()).toLocalDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveCurrency(String currency)
|
||||||
|
{
|
||||||
|
return StringUtils.hasText(currency) ? currency.trim().toUpperCase() : BASE_CURRENCY;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal money(BigDecimal value)
|
||||||
|
{
|
||||||
|
return nz(value).setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 生成业务单号:前缀 + 时间戳(毫秒) + 3位随机数 */
|
||||||
|
private String generateNo(String prefix)
|
||||||
|
{
|
||||||
|
return prefix + LocalDateTime.now().format(NO_FORMATTER)
|
||||||
|
+ ThreadLocalRandom.current().nextInt(100, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal nz(BigDecimal value)
|
||||||
|
{
|
||||||
|
return value == null ? BigDecimal.ZERO : value;
|
||||||
|
}
|
||||||
|
}
|
||||||
+102
@@ -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;
|
||||||
|
}
|
||||||
+14
-1
@@ -30,6 +30,10 @@ public class CustomerCreditFlow extends BaseEntity
|
|||||||
public static final String CHANGE_ADJUST = "ADJUST";
|
public static final String CHANGE_ADJUST = "ADJUST";
|
||||||
/** 变动类型:到期自动失效 */
|
/** 变动类型:到期自动失效 */
|
||||||
public static final String CHANGE_EXPIRE = "EXPIRE";
|
public static final String CHANGE_EXPIRE = "EXPIRE";
|
||||||
|
/** 变动类型:业务单据占用授信(已用额度增加,如账单确认/计费订单开单) */
|
||||||
|
public static final String CHANGE_OCCUPY = "OCCUPY";
|
||||||
|
/** 变动类型:业务单据释放授信(已用额度减少,如账单作废/计费变动核减) */
|
||||||
|
public static final String CHANGE_RELEASE = "RELEASE";
|
||||||
|
|
||||||
/** 调整方式:设置为 */
|
/** 调整方式:设置为 */
|
||||||
public static final String MODE_SET = "SET";
|
public static final String MODE_SET = "SET";
|
||||||
@@ -53,9 +57,18 @@ public class CustomerCreditFlow extends BaseEntity
|
|||||||
/** 额度类型:CREDIT信用额度 TEMP临时额度 */
|
/** 额度类型:CREDIT信用额度 TEMP临时额度 */
|
||||||
private String creditType;
|
private String creditType;
|
||||||
|
|
||||||
/** 变动类型:INIT初始化 ADJUST调整 EXPIRE到期失效 */
|
/** 变动类型:INIT初始化 ADJUST调整 EXPIRE到期失效 OCCUPY授信占用 RELEASE授信释放 */
|
||||||
private String changeType;
|
private String changeType;
|
||||||
|
|
||||||
|
/** 业务来源类型:AR_BILL应收账单 ORDER计费订单(仅占用/释放流水) */
|
||||||
|
private String bizType;
|
||||||
|
|
||||||
|
/** 关联业务单据ID */
|
||||||
|
private Long bizId;
|
||||||
|
|
||||||
|
/** 关联业务单据号 */
|
||||||
|
private String bizNo;
|
||||||
|
|
||||||
/** 变动方向:1增加 -1减少 0持平 */
|
/** 变动方向:1增加 -1减少 0持平 */
|
||||||
private Integer direction;
|
private Integer direction;
|
||||||
|
|
||||||
|
|||||||
+35
@@ -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<CustomerCreditApply> 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);
|
||||||
|
}
|
||||||
+16
-31
@@ -1,9 +1,9 @@
|
|||||||
package com.ruoyi.customer.customermoney.service;
|
package com.ruoyi.customer.customermoney.service;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import com.ruoyi.customer.customermoney.domain.CustomerAccount;
|
import com.ruoyi.customer.customermoney.domain.CustomerAccount;
|
||||||
import com.ruoyi.customer.customermoney.domain.CustomerBalanceFlow;
|
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.CustomerCreditFlow;
|
||||||
import com.ruoyi.customer.customermoney.domain.CustomerReceipt;
|
import com.ruoyi.customer.customermoney.domain.CustomerReceipt;
|
||||||
|
|
||||||
@@ -52,41 +52,26 @@ public interface ICustomerMoneyService
|
|||||||
List<CustomerCreditFlow> selectCreditFlowList(CustomerCreditFlow query);
|
List<CustomerCreditFlow> selectCreditFlowList(CustomerCreditFlow query);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 调整客户额度(信用额度/临时额度)
|
* 提交额度调整申请(信用额度/临时额度)。申请仅落库为 PENDING,不立即变动账户额度;
|
||||||
* 事务内:锁定CNY账户(不存在则初始化)-> 校验(不能为负、不能低于已用额度)
|
* 申请时会按账户当前额度做一次合法性校验(不能为负、不能低于已用额度),审批通过时会再次校验。
|
||||||
* -> 乐观锁更新额度 -> 回写客户档案额度字段 -> 写额度流水
|
|
||||||
*/
|
*/
|
||||||
int adjustCredit(CustomerCreditFlow flow);
|
int applyCreditAdjust(CustomerCreditApply apply);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 账单确认时占用授信额度(used_credit += amount)。该币种账户不存在时跳过。
|
* 审批通过额度调整申请:PENDING -> APPROVED。
|
||||||
* 调用方必须已持有账单行锁,本方法内部再锁账户(加锁顺序:账单 -> 账户)。
|
* 事务内重新锁定CNY账户并按审批时的实际额度计算/校验,通过后调整额度、回写客户档案、写额度流水。
|
||||||
*/
|
*/
|
||||||
void occupyCredit(Long customerId, String currency, BigDecimal amount);
|
int approveCreditApply(Long applyId, String approveRemark);
|
||||||
|
|
||||||
/**
|
/** 审批驳回:PENDING -> REJECTED,驳回原因必填 */
|
||||||
* 账单作废时释放授信额度(used_credit -= amount,不小于0)。该币种账户不存在时跳过。
|
int rejectCreditApply(Long applyId, String rejectReason);
|
||||||
* 调用方必须已持有账单行锁。
|
|
||||||
*/
|
|
||||||
void releaseCredit(Long customerId, String currency, BigDecimal amount);
|
|
||||||
|
|
||||||
/**
|
/** 申请人撤销:PENDING -> CANCELLED */
|
||||||
* 用账户余额核销账单:校验余额充足 -> balance -= amount、used_credit -= amount
|
int cancelCreditApply(Long applyId);
|
||||||
* -> 写一条 BILL_PAY 扣款流水(direction=-1)。返回已落库的流水(含ID)。
|
|
||||||
*
|
|
||||||
* @param bizId 余额核销单ID
|
|
||||||
* @param bizNo 账单号
|
|
||||||
*/
|
|
||||||
CustomerBalanceFlow payBill(Long customerId, String currency, BigDecimal amount,
|
|
||||||
Long bizId, String bizNo, String remark);
|
|
||||||
|
|
||||||
/**
|
/** 查询额度调整申请列表 */
|
||||||
* 反核销回补:balance += amount、used_credit += amount(重新占用授信)
|
List<CustomerCreditApply> selectCreditApplyList(CustomerCreditApply query);
|
||||||
* -> 写一条 BILL_PAY 回补流水(direction=1)。返回已落库的流水(含ID)。
|
|
||||||
*
|
/** 查询额度调整申请详情 */
|
||||||
* @param bizId 余额核销单ID
|
CustomerCreditApply selectCreditApplyById(Long applyId);
|
||||||
* @param bizNo 账单号
|
|
||||||
*/
|
|
||||||
CustomerBalanceFlow reverseBillPay(Long customerId, String currency, BigDecimal amount,
|
|
||||||
Long bizId, String bizNo, String remark);
|
|
||||||
}
|
}
|
||||||
|
|||||||
+266
-387
@@ -18,10 +18,15 @@ import com.ruoyi.common.utils.SecurityUtils;
|
|||||||
import com.ruoyi.customer.customerinfo.domain.BmsCustomer;
|
import com.ruoyi.customer.customerinfo.domain.BmsCustomer;
|
||||||
import com.ruoyi.customer.customerinfo.domain.BmsCustomerAttachment;
|
import com.ruoyi.customer.customerinfo.domain.BmsCustomerAttachment;
|
||||||
import com.ruoyi.customer.customerinfo.mapper.BmsCustomerMapper;
|
import com.ruoyi.customer.customerinfo.mapper.BmsCustomerMapper;
|
||||||
|
import com.ruoyi.customer.customermoney.api.BalanceChangeCommand;
|
||||||
|
import com.ruoyi.customer.customermoney.api.CustomerFundBizType;
|
||||||
|
import com.ruoyi.customer.customermoney.api.ICustomerAccountApi;
|
||||||
import com.ruoyi.customer.customermoney.domain.CustomerAccount;
|
import com.ruoyi.customer.customermoney.domain.CustomerAccount;
|
||||||
import com.ruoyi.customer.customermoney.domain.CustomerBalanceFlow;
|
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.CustomerCreditFlow;
|
||||||
import com.ruoyi.customer.customermoney.domain.CustomerReceipt;
|
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.mapper.CustomerMoneyMapper;
|
||||||
import com.ruoyi.customer.customermoney.service.ICustomerMoneyService;
|
import com.ruoyi.customer.customermoney.service.ICustomerMoneyService;
|
||||||
import com.ruoyi.system.domain.BmsExchangeRate;
|
import com.ruoyi.system.domain.BmsExchangeRate;
|
||||||
@@ -40,20 +45,25 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
|||||||
|
|
||||||
/** 本位币币种 */
|
/** 本位币币种 */
|
||||||
private static final String BASE_CURRENCY = BmsExchangeRate.BASE_CURRENCY;
|
private static final String BASE_CURRENCY = BmsExchangeRate.BASE_CURRENCY;
|
||||||
/** 新客户默认总额度(CNY) */
|
|
||||||
private static final BigDecimal DEFAULT_CREDIT = new BigDecimal("1000");
|
|
||||||
/** 金额保留位数 */
|
/** 金额保留位数 */
|
||||||
private static final int MONEY_SCALE = 2;
|
private static final int MONEY_SCALE = 2;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private CustomerMoneyMapper customerMoneyMapper;
|
private CustomerMoneyMapper customerMoneyMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private CustomerCreditApplyMapper creditApplyMapper;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private BmsCustomerMapper bmsCustomerMapper;
|
private BmsCustomerMapper bmsCustomerMapper;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private IBmsExchangeRateService exchangeRateService;
|
private IBmsExchangeRateService exchangeRateService;
|
||||||
|
|
||||||
|
/** 客户资金公共服务:账户/余额/授信额度变动统一收口 */
|
||||||
|
@Autowired
|
||||||
|
private ICustomerAccountApi customerAccountApi;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public List<CustomerAccount> selectAccountList(Long customerId)
|
public List<CustomerAccount> selectAccountList(Long customerId)
|
||||||
@@ -65,8 +75,8 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
|||||||
CustomerAccount account = list.get(i);
|
CustomerAccount account = list.get(i);
|
||||||
if (BASE_CURRENCY.equals(account.getCurrency()) && isTempCreditExpired(account))
|
if (BASE_CURRENCY.equals(account.getCurrency()) && isTempCreditExpired(account))
|
||||||
{
|
{
|
||||||
CustomerAccount locked = customerMoneyMapper.selectAccountForUpdate(customerId, BASE_CURRENCY);
|
CustomerAccount locked = customerAccountApi.lockCnyAccount(customerId);
|
||||||
expireTempCreditIfNeeded(locked);
|
customerAccountApi.expireTempCreditIfNeeded(locked);
|
||||||
if (locked != null)
|
if (locked != null)
|
||||||
{
|
{
|
||||||
list.set(i, locked);
|
list.set(i, locked);
|
||||||
@@ -188,46 +198,24 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
|||||||
? receipt.getBaseAmount()
|
? receipt.getBaseAmount()
|
||||||
: originalAmount.multiply(rate).setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
: originalAmount.multiply(rate).setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||||
|
|
||||||
// 1. 行锁锁定客户本位币(CNY)账户,不存在则按客户档案额度自动初始化。无论收什么币种,一律折算 CNY 入账
|
// 1. 本位币入账(公共资金服务内完成:锁户/初始化 -> 余额变动 -> 余额流水)。
|
||||||
CustomerAccount account = getOrInitCnyAccount(receipt.getCustomerId(), username);
|
|
||||||
|
|
||||||
// 2. 本位币余额变动(预充值/混合增加余额;还款核销释放已用额度)
|
|
||||||
BigDecimal beforeBalance = nz(account.getBalance());
|
|
||||||
BigDecimal afterBalance = beforeBalance.add(baseAmount);
|
|
||||||
BigDecimal frozenBalance = nz(account.getFrozenBalance());
|
|
||||||
|
|
||||||
account.setBalance(afterBalance);
|
|
||||||
account.setUpdateBy(username);
|
|
||||||
// 额度占用统一以账单为准:收款审核只增加余额,核销账单时才释放已用额度
|
// 额度占用统一以账单为准:收款审核只增加余额,核销账单时才释放已用额度
|
||||||
int accountRows = customerMoneyMapper.updateAccountBalance(account);
|
BalanceChangeCommand fundCmd = new BalanceChangeCommand();
|
||||||
if (accountRows == 0)
|
fundCmd.setCustomerId(receipt.getCustomerId());
|
||||||
{
|
// 流水保留收款原币;无论收什么币种,一律锁CNY账户折算入账
|
||||||
throw new ServiceException("账户余额已被其他操作变动,请刷新后重试");
|
fundCmd.setCurrency(currency);
|
||||||
}
|
fundCmd.setAccountCurrency(BASE_CURRENCY);
|
||||||
|
fundCmd.setAmount(originalAmount);
|
||||||
|
fundCmd.setExchangeRate(rate);
|
||||||
|
fundCmd.setBaseAmount(baseAmount);
|
||||||
|
fundCmd.setBizType(CustomerFundBizType.RECEIPT);
|
||||||
|
fundCmd.setBizId(receipt.getId());
|
||||||
|
fundCmd.setBizNo(receipt.getReceiptNo());
|
||||||
|
fundCmd.setReoccupyUsedCredit(false);
|
||||||
|
fundCmd.setRemark("收款单审核通过:" + receipt.getReceiptNo());
|
||||||
|
customerAccountApi.refundBalance(fundCmd);
|
||||||
|
|
||||||
// 3. 写余额流水:原币金额留痕,本位币金额参与余额变动
|
// 2. 收款单置为已确认
|
||||||
CustomerBalanceFlow flow = new CustomerBalanceFlow();
|
|
||||||
flow.setFlowNo(generateNo("LS"));
|
|
||||||
flow.setCustomerId(receipt.getCustomerId());
|
|
||||||
flow.setAccountId(account.getId());
|
|
||||||
flow.setCurrency(currency);
|
|
||||||
flow.setExchangeRate(rate);
|
|
||||||
flow.setBaseAmount(baseAmount);
|
|
||||||
flow.setBizType(CustomerBalanceFlow.BIZ_RECEIPT);
|
|
||||||
flow.setBizId(receipt.getId());
|
|
||||||
flow.setBizNo(receipt.getReceiptNo());
|
|
||||||
flow.setDirection(1);
|
|
||||||
flow.setAmount(originalAmount);
|
|
||||||
flow.setBeforeBalance(beforeBalance);
|
|
||||||
flow.setAfterBalance(afterBalance);
|
|
||||||
flow.setBeforeFrozenBalance(frozenBalance);
|
|
||||||
flow.setAfterFrozenBalance(frozenBalance);
|
|
||||||
flow.setOperatorId(userId);
|
|
||||||
flow.setCreateBy(username);
|
|
||||||
flow.setRemark("收款单审核通过:" + receipt.getReceiptNo());
|
|
||||||
customerMoneyMapper.insertFlow(flow);
|
|
||||||
|
|
||||||
// 4. 收款单置为已确认
|
|
||||||
receipt.setStatus(CustomerReceipt.STATUS_CONFIRMED);
|
receipt.setStatus(CustomerReceipt.STATUS_CONFIRMED);
|
||||||
receipt.setConfirmedAt(new Date());
|
receipt.setConfirmedAt(new Date());
|
||||||
receipt.setConfirmedBy(userId);
|
receipt.setConfirmedBy(userId);
|
||||||
@@ -268,53 +256,27 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
|||||||
return customerMoneyMapper.selectCreditFlowList(query);
|
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();
|
String username = SecurityUtils.getUsername();
|
||||||
Long userId = SecurityUtils.getUserId();
|
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);
|
boolean isTemp = CustomerCreditFlow.CREDIT_TYPE_TEMP.equals(creditType);
|
||||||
|
|
||||||
// 1. 行锁锁定CNY账户(不存在则按客户档案额度初始化创建);先处理临时额度过期
|
// 1. 参数/日期/结果合法性校验并锁定CNY账户(不存在则按客户档案额度初始化);先处理临时额度过期
|
||||||
CustomerAccount account = getOrInitCnyAccount(customerId, username);
|
CustomerAccount account = validateAdjustCommand(cmd, username);
|
||||||
expireTempCreditIfNeeded(account);
|
BigDecimal input = nz(cmd.getChangeAmount());
|
||||||
|
|
||||||
// 2. 计算调整结果
|
// 2. 计算调整结果
|
||||||
BigDecimal before = isTemp ? nz(account.getExtraCredit()) : nz(account.getTotalCredit());
|
BigDecimal before = isTemp ? nz(account.getExtraCredit()) : nz(account.getTotalCredit());
|
||||||
BigDecimal otherCredit = isTemp ? nz(account.getTotalCredit()) : nz(account.getExtraCredit());
|
|
||||||
BigDecimal after;
|
BigDecimal after;
|
||||||
switch (adjustMode)
|
switch (adjustMode)
|
||||||
{
|
{
|
||||||
@@ -329,40 +291,18 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
after = after.setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
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 effectiveDate = null;
|
||||||
Date expireDate = null;
|
Date expireDate = null;
|
||||||
if (isTemp)
|
if (isTemp)
|
||||||
{
|
{
|
||||||
LocalDate today = LocalDate.now();
|
LocalDate today = LocalDate.now();
|
||||||
LocalDate eff = cmd.getEffectiveDate() != null ? toLocalDate(cmd.getEffectiveDate()) : today;
|
LocalDate eff = cmd.getEffectiveDate() != null ? toLocalDate(cmd.getEffectiveDate()) : today;
|
||||||
if (eff.isAfter(today))
|
|
||||||
{
|
|
||||||
throw new ServiceException("生效日期不能晚于今天,需要未来生效请在生效当天再设置");
|
|
||||||
}
|
|
||||||
effectiveDate = toSqlDate(eff);
|
effectiveDate = toSqlDate(eff);
|
||||||
if (cmd.getExpireDate() != null)
|
if (cmd.getExpireDate() != null)
|
||||||
{
|
{
|
||||||
LocalDate exp = toLocalDate(cmd.getExpireDate());
|
expireDate = toSqlDate(toLocalDate(cmd.getExpireDate()));
|
||||||
if (!exp.isAfter(eff))
|
|
||||||
{
|
|
||||||
throw new ServiceException("失效日期必须晚于生效日期");
|
|
||||||
}
|
|
||||||
expireDate = toSqlDate(exp);
|
|
||||||
}
|
}
|
||||||
account.setExtraCredit(after);
|
account.setExtraCredit(after);
|
||||||
account.setExtraEffectiveDate(effectiveDate);
|
account.setExtraEffectiveDate(effectiveDate);
|
||||||
@@ -408,295 +348,278 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
|||||||
flow.setRemark(cmd.getRemark());
|
flow.setRemark(cmd.getRemark());
|
||||||
flow.setDelFlag("0");
|
flow.setDelFlag("0");
|
||||||
customerMoneyMapper.insertCreditFlow(flow);
|
customerMoneyMapper.insertCreditFlow(flow);
|
||||||
return 1;
|
return flow;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ================= 账单联动:额度占用 / 余额核销 ================= */
|
/* ================= 额度调整:申请 / 审批 ================= */
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
public List<CustomerCreditApply> selectCreditApplyList(CustomerCreditApply query)
|
||||||
public void occupyCredit(Long customerId, String currency, BigDecimal amount)
|
|
||||||
{
|
{
|
||||||
if (customerId == null || amount == null || amount.compareTo(BigDecimal.ZERO) <= 0)
|
return creditApplyMapper.selectApplyList(query);
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
String ccny = StringUtils.hasText(currency) ? currency.trim().toUpperCase() : BASE_CURRENCY;
|
|
||||||
CustomerAccount account = lockAccountForBill(customerId, ccny, SecurityUtils.getUsername());
|
@Override
|
||||||
if (account == null)
|
public CustomerCreditApply selectCreditApplyById(Long applyId)
|
||||||
{
|
{
|
||||||
return;
|
return creditApplyMapper.selectApplyById(applyId);
|
||||||
}
|
|
||||||
account.setUsedCredit(nz(account.getUsedCredit()).add(amount).setScale(MONEY_SCALE, RoundingMode.HALF_UP));
|
|
||||||
account.setUpdateBy(SecurityUtils.getUsername());
|
|
||||||
if (customerMoneyMapper.updateAccountBalance(account) == 0)
|
|
||||||
{
|
|
||||||
throw new ServiceException("账户已被其他操作变动,请刷新后重试");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void releaseCredit(Long customerId, String currency, BigDecimal amount)
|
public int applyCreditAdjust(CustomerCreditApply apply)
|
||||||
{
|
{
|
||||||
if (customerId == null || amount == null || amount.compareTo(BigDecimal.ZERO) <= 0)
|
if (apply == null || apply.getCustomerId() == null)
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String ccny = StringUtils.hasText(currency) ? currency.trim().toUpperCase() : BASE_CURRENCY;
|
|
||||||
CustomerAccount account = lockAccountForBill(customerId, ccny, SecurityUtils.getUsername());
|
|
||||||
if (account == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
account.setUsedCredit(nz(account.getUsedCredit()).subtract(amount).max(BigDecimal.ZERO)
|
|
||||||
.setScale(MONEY_SCALE, RoundingMode.HALF_UP));
|
|
||||||
account.setUpdateBy(SecurityUtils.getUsername());
|
|
||||||
if (customerMoneyMapper.updateAccountBalance(account) == 0)
|
|
||||||
{
|
|
||||||
throw new ServiceException("账户已被其他操作变动,请刷新后重试");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
|
||||||
public CustomerBalanceFlow payBill(Long customerId, String currency, BigDecimal amount,
|
|
||||||
Long bizId, String bizNo, String remark)
|
|
||||||
{
|
|
||||||
if (customerId == null)
|
|
||||||
{
|
{
|
||||||
throw new ServiceException("客户ID不能为空");
|
throw new ServiceException("客户ID不能为空");
|
||||||
}
|
}
|
||||||
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0)
|
// 构造内部调整命令,复用统一的参数/日期/额度合法性校验(只校验,不写入)
|
||||||
{
|
CustomerCreditFlow cmd = new CustomerCreditFlow();
|
||||||
throw new ServiceException("核销金额必须大于0");
|
cmd.setCustomerId(apply.getCustomerId());
|
||||||
}
|
cmd.setCreditType(apply.getCreditType());
|
||||||
String ccny = StringUtils.hasText(currency) ? currency.trim().toUpperCase() : BASE_CURRENCY;
|
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();
|
String username = SecurityUtils.getUsername();
|
||||||
Long userId = SecurityUtils.getUserId();
|
Long userId = SecurityUtils.getUserId();
|
||||||
CustomerAccount account = lockAccountForBill(customerId, ccny, username);
|
|
||||||
if (account == null)
|
|
||||||
{
|
|
||||||
throw new ServiceException("客户没有" + ccny + "账户,无法用余额核销");
|
|
||||||
}
|
|
||||||
|
|
||||||
BigDecimal beforeBalance = nz(account.getBalance());
|
// 1. 参数校验 + 日期校验 + 按当前账户额度预演结果,提前拦截不可能通过的申请(不写入账户)
|
||||||
if (beforeBalance.compareTo(amount) < 0)
|
validateAdjustCommand(cmd, username);
|
||||||
{
|
apply.setChangeAmount(cmd.getChangeAmount());
|
||||||
throw new ServiceException("账户余额不足,当前" + ccny + "余额:" + beforeBalance.toPlainString());
|
apply.setReason(cmd.getReason());
|
||||||
}
|
|
||||||
BigDecimal afterBalance = beforeBalance.subtract(amount);
|
|
||||||
BigDecimal frozenBalance = nz(account.getFrozenBalance());
|
|
||||||
// 核销账单 = 应收被清偿,同步释放等额授信占用
|
|
||||||
account.setBalance(afterBalance);
|
|
||||||
account.setUsedCredit(nz(account.getUsedCredit()).subtract(amount).max(BigDecimal.ZERO)
|
|
||||||
.setScale(MONEY_SCALE, RoundingMode.HALF_UP));
|
|
||||||
account.setUpdateBy(username);
|
|
||||||
if (customerMoneyMapper.updateAccountBalance(account) == 0)
|
|
||||||
{
|
|
||||||
throw new ServiceException("账户余额已被其他操作变动,请刷新后重试");
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomerBalanceFlow flow = newBalanceFlow(account, ccny, amount, -1,
|
// 2. 待审批申请落库,不动账户额度
|
||||||
beforeBalance, afterBalance, frozenBalance, frozenBalance,
|
apply.setApplyNo(generateNo("EDSQ"));
|
||||||
bizId, bizNo, username, userId, remark);
|
apply.setStatus(CustomerCreditApply.STATUS_PENDING);
|
||||||
customerMoneyMapper.insertFlow(flow);
|
apply.setApplyBy(username);
|
||||||
return flow;
|
apply.setApplyUserId(userId);
|
||||||
|
apply.setApplyTime(new Date());
|
||||||
|
apply.setCreateBy(username);
|
||||||
|
return creditApplyMapper.insertApply(apply);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public CustomerBalanceFlow reverseBillPay(Long customerId, String currency, BigDecimal amount,
|
public int approveCreditApply(Long applyId, String approveRemark)
|
||||||
Long bizId, String bizNo, String remark)
|
|
||||||
{
|
{
|
||||||
if (customerId == null || amount == null || amount.compareTo(BigDecimal.ZERO) <= 0)
|
if (applyId == null)
|
||||||
{
|
{
|
||||||
throw new ServiceException("反核销金额不正确");
|
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 ccny = StringUtils.hasText(currency) ? currency.trim().toUpperCase() : BASE_CURRENCY;
|
|
||||||
String username = SecurityUtils.getUsername();
|
String username = SecurityUtils.getUsername();
|
||||||
Long userId = SecurityUtils.getUserId();
|
Long userId = SecurityUtils.getUserId();
|
||||||
CustomerAccount account = lockAccountForBill(customerId, ccny, username);
|
|
||||||
if (account == null)
|
// 以审批时账户的实际额度重新计算并调整(申请期间额度可能已变动)
|
||||||
|
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("客户" + ccny + "账户不存在,无法反核销");
|
throw new ServiceException("申请状态已变化,审批失败");
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
BigDecimal beforeBalance = nz(account.getBalance());
|
@Override
|
||||||
BigDecimal afterBalance = beforeBalance.add(amount);
|
@Transactional(rollbackFor = Exception.class)
|
||||||
BigDecimal frozenBalance = nz(account.getFrozenBalance());
|
public int rejectCreditApply(Long applyId, String rejectReason)
|
||||||
// 账单恢复为未核销,授信占用同步加回(允许与存量数据一样出现超出总额度的占用状态)
|
|
||||||
account.setBalance(afterBalance);
|
|
||||||
account.setUsedCredit(nz(account.getUsedCredit()).add(amount).setScale(MONEY_SCALE, RoundingMode.HALF_UP));
|
|
||||||
account.setUpdateBy(username);
|
|
||||||
if (customerMoneyMapper.updateAccountBalance(account) == 0)
|
|
||||||
{
|
{
|
||||||
throw new ServiceException("账户余额已被其他操作变动,请刷新后重试");
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomerBalanceFlow flow = newBalanceFlow(account, ccny, amount, 1,
|
@Override
|
||||||
beforeBalance, afterBalance, frozenBalance, frozenBalance,
|
@Transactional(rollbackFor = Exception.class)
|
||||||
bizId, bizNo, username, userId, remark);
|
public int cancelCreditApply(Long applyId)
|
||||||
customerMoneyMapper.insertFlow(flow);
|
{
|
||||||
return flow;
|
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不存在则初始化(先处理临时额度过期),外币账户不存在返回null */
|
/**
|
||||||
private CustomerAccount lockAccountForBill(Long customerId, String currency, String username)
|
* 额度调整参数/日期/结果合法性校验(申请与审批共用)。
|
||||||
|
* 锁定CNY账户做预演但不写入,返回当前账户;规整后的金额/原因/日期回写到 cmd。
|
||||||
|
*/
|
||||||
|
private CustomerAccount validateAdjustCommand(CustomerCreditFlow cmd, String username)
|
||||||
{
|
{
|
||||||
if (BASE_CURRENCY.equals(currency))
|
if (cmd == null || cmd.getCustomerId() == null)
|
||||||
{
|
{
|
||||||
CustomerAccount account = getOrInitCnyAccount(customerId, username);
|
throw new ServiceException("客户ID不能为空");
|
||||||
expireTempCreditIfNeeded(account);
|
}
|
||||||
|
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 = customerAccountApi.lockCnyAccount(cmd.getCustomerId());
|
||||||
|
customerAccountApi.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;
|
return account;
|
||||||
}
|
}
|
||||||
return customerMoneyMapper.selectAccountForUpdate(customerId, currency);
|
|
||||||
}
|
|
||||||
|
|
||||||
private CustomerBalanceFlow newBalanceFlow(CustomerAccount account, String currency, BigDecimal amount,
|
|
||||||
int direction, BigDecimal beforeBalance, BigDecimal afterBalance,
|
|
||||||
BigDecimal beforeFrozen, BigDecimal afterFrozen,
|
|
||||||
Long bizId, String bizNo, String username, Long userId, String remark)
|
|
||||||
{
|
|
||||||
CustomerBalanceFlow flow = new CustomerBalanceFlow();
|
|
||||||
flow.setFlowNo(generateNo("LS"));
|
|
||||||
flow.setCustomerId(account.getCustomerId());
|
|
||||||
flow.setAccountId(account.getId());
|
|
||||||
flow.setCurrency(currency);
|
|
||||||
flow.setExchangeRate(BigDecimal.ONE);
|
|
||||||
flow.setBaseAmount(amount);
|
|
||||||
flow.setBizType(CustomerBalanceFlow.BIZ_BILL_PAY);
|
|
||||||
flow.setBizId(bizId);
|
|
||||||
flow.setBizNo(bizNo);
|
|
||||||
flow.setDirection(direction);
|
|
||||||
flow.setAmount(amount);
|
|
||||||
flow.setBeforeBalance(beforeBalance);
|
|
||||||
flow.setAfterBalance(afterBalance);
|
|
||||||
flow.setBeforeFrozenBalance(beforeFrozen);
|
|
||||||
flow.setAfterFrozenBalance(afterFrozen);
|
|
||||||
flow.setOperatorId(userId);
|
|
||||||
flow.setCreateBy(username);
|
|
||||||
flow.setRemark(remark);
|
|
||||||
return flow;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ================= 额度私有方法 ================= */
|
/* ================= 额度私有方法 ================= */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 新建客户时默认初始化CNY账户:余额0,总额度默认1000 CNY
|
* 新建客户时默认初始化CNY账户(委托公共资金服务统一收口)
|
||||||
* (客户档案已填信用额度且大于0时按档案值),并写 INIT 流水;已存在则跳过。
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public int initDefaultCnyAccount(Long customerId)
|
public int initDefaultCnyAccount(Long customerId)
|
||||||
{
|
{
|
||||||
if (customerId == null)
|
return customerAccountApi.initDefaultCnyAccount(customerId);
|
||||||
{
|
|
||||||
throw new ServiceException("客户ID不能为空");
|
|
||||||
}
|
|
||||||
String username = SecurityUtils.getUsername();
|
|
||||||
CustomerAccount account = customerMoneyMapper.selectAccountForUpdate(customerId, BASE_CURRENCY);
|
|
||||||
if (account != null)
|
|
||||||
{
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
BmsCustomer customer = bmsCustomerMapper.selectRawCreditByCustomerId(customerId);
|
|
||||||
if (customer == null)
|
|
||||||
{
|
|
||||||
throw new ServiceException("客户不存在");
|
|
||||||
}
|
|
||||||
BigDecimal initCredit = nz(customer.getCreditEd());
|
|
||||||
if (initCredit.compareTo(BigDecimal.ZERO) <= 0)
|
|
||||||
{
|
|
||||||
initCredit = DEFAULT_CREDIT;
|
|
||||||
}
|
|
||||||
BigDecimal initTemp = nz(customer.getTempEd());
|
|
||||||
account = createCnyAccount(customerId, initCredit, initTemp, username);
|
|
||||||
// 档案额度与实际初始额度保持一致
|
|
||||||
bmsCustomerMapper.updateCustomerCredit(customerId, initCredit, initTemp, username);
|
|
||||||
writeInitFlow(account, CustomerCreditFlow.CREDIT_TYPE_CREDIT,
|
|
||||||
initCredit, null, null, username, "新客户默认初始化额度1000 CNY");
|
|
||||||
if (initTemp.compareTo(BigDecimal.ZERO) > 0)
|
|
||||||
{
|
|
||||||
writeInitFlow(account, CustomerCreditFlow.CREDIT_TYPE_TEMP,
|
|
||||||
initTemp, null, null, username, "新客户初始化临时额度");
|
|
||||||
}
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 行锁获取客户CNY账户,不存在则按客户档案当前额度初始化创建 */
|
/** 临时额度是否已过期:有失效日期且今天已晚于失效日(失效日当天仍有效)。仅只读判断,不落库 */
|
||||||
private CustomerAccount getOrInitCnyAccount(Long customerId, String username)
|
|
||||||
{
|
|
||||||
CustomerAccount account = customerMoneyMapper.selectAccountForUpdate(customerId, BASE_CURRENCY);
|
|
||||||
if (account == null)
|
|
||||||
{
|
|
||||||
BmsCustomer customer = bmsCustomerMapper.selectRawCreditByCustomerId(customerId);
|
|
||||||
if (customer == null)
|
|
||||||
{
|
|
||||||
throw new ServiceException("客户不存在");
|
|
||||||
}
|
|
||||||
BigDecimal initCredit = nz(customer.getCreditEd());
|
|
||||||
BigDecimal initTemp = nz(customer.getTempEd());
|
|
||||||
account = createCnyAccount(customerId, initCredit, initTemp, username);
|
|
||||||
writeInitFlow(account, CustomerCreditFlow.CREDIT_TYPE_CREDIT,
|
|
||||||
initCredit, null, null, username, "首次创建CNY账户,从客户档案同步初始额度");
|
|
||||||
if (initTemp.compareTo(BigDecimal.ZERO) > 0)
|
|
||||||
{
|
|
||||||
writeInitFlow(account, CustomerCreditFlow.CREDIT_TYPE_TEMP,
|
|
||||||
initTemp, null, null, username, "首次创建CNY账户,从客户档案同步临时额度");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return account;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 创建CNY账户(余额0,已用0) */
|
|
||||||
private CustomerAccount createCnyAccount(Long customerId, BigDecimal totalCredit,
|
|
||||||
BigDecimal extraCredit, String username)
|
|
||||||
{
|
|
||||||
CustomerAccount account = new CustomerAccount();
|
|
||||||
account.setCustomerId(customerId);
|
|
||||||
account.setCurrency(BASE_CURRENCY);
|
|
||||||
account.setBalance(BigDecimal.ZERO);
|
|
||||||
account.setFrozenBalance(BigDecimal.ZERO);
|
|
||||||
account.setTotalCredit(totalCredit);
|
|
||||||
account.setExtraCredit(extraCredit);
|
|
||||||
account.setUsedCredit(BigDecimal.ZERO);
|
|
||||||
account.setTax(BigDecimal.ZERO);
|
|
||||||
account.setVersion(0);
|
|
||||||
account.setCreateBy(username);
|
|
||||||
customerMoneyMapper.insertAccount(account);
|
|
||||||
return account;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 账户首次初始化时写额度 INIT 流水(从客户档案额度同步)
|
|
||||||
*/
|
|
||||||
private void writeInitFlow(CustomerAccount account, String creditType,
|
|
||||||
BigDecimal initAmount, Date effectiveDate, Date expireDate,
|
|
||||||
String username, String remark)
|
|
||||||
{
|
|
||||||
CustomerCreditFlow flow = new CustomerCreditFlow();
|
|
||||||
flow.setFlowNo(generateNo("EDLS"));
|
|
||||||
flow.setCustomerId(account.getCustomerId());
|
|
||||||
flow.setAccountId(account.getId());
|
|
||||||
flow.setCreditType(creditType);
|
|
||||||
flow.setChangeType(CustomerCreditFlow.CHANGE_INIT);
|
|
||||||
flow.setDirection(initAmount.compareTo(BigDecimal.ZERO) > 0 ? 1 : 0);
|
|
||||||
flow.setBeforeAmount(BigDecimal.ZERO);
|
|
||||||
flow.setChangeAmount(initAmount);
|
|
||||||
flow.setAfterAmount(initAmount);
|
|
||||||
flow.setUsedCredit(nz(account.getUsedCredit()));
|
|
||||||
flow.setEffectiveDate(effectiveDate);
|
|
||||||
flow.setExpireDate(expireDate);
|
|
||||||
flow.setReason("账户初始化");
|
|
||||||
flow.setOperatorId(SecurityUtils.getUserId());
|
|
||||||
flow.setCreateBy(username);
|
|
||||||
flow.setRemark(remark);
|
|
||||||
flow.setDelFlag("0");
|
|
||||||
customerMoneyMapper.insertCreditFlow(flow);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 临时额度是否已过期:有失效日期且今天已晚于失效日(失效日当天仍有效) */
|
|
||||||
private boolean isTempCreditExpired(CustomerAccount account)
|
private boolean isTempCreditExpired(CustomerAccount account)
|
||||||
{
|
{
|
||||||
return account != null
|
return account != null
|
||||||
@@ -705,50 +628,6 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
|||||||
&& LocalDate.now().isAfter(toLocalDate(account.getExtraExpireDate()));
|
&& LocalDate.now().isAfter(toLocalDate(account.getExtraExpireDate()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 临时额度到期自动失效:额度清零 -> 回写客户档案 -> 写EXPIRE流水。
|
|
||||||
* 入参必须是事务内行锁查出的账户。
|
|
||||||
*/
|
|
||||||
private void expireTempCreditIfNeeded(CustomerAccount account)
|
|
||||||
{
|
|
||||||
if (account == null || !isTempCreditExpired(account))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String username = SecurityUtils.getUsername();
|
|
||||||
BigDecimal before = nz(account.getExtraCredit());
|
|
||||||
account.setExtraCredit(BigDecimal.ZERO);
|
|
||||||
account.setUpdateBy(username);
|
|
||||||
int rows = customerMoneyMapper.updateAccountCredit(account);
|
|
||||||
if (rows == 0)
|
|
||||||
{
|
|
||||||
throw new ServiceException("账户额度已被其他操作变动,请刷新后重试");
|
|
||||||
}
|
|
||||||
account.setVersion(account.getVersion() + 1);
|
|
||||||
bmsCustomerMapper.updateCustomerCredit(account.getCustomerId(),
|
|
||||||
nz(account.getTotalCredit()), BigDecimal.ZERO, username);
|
|
||||||
|
|
||||||
CustomerCreditFlow flow = new CustomerCreditFlow();
|
|
||||||
flow.setFlowNo(generateNo("EDLS"));
|
|
||||||
flow.setCustomerId(account.getCustomerId());
|
|
||||||
flow.setAccountId(account.getId());
|
|
||||||
flow.setCreditType(CustomerCreditFlow.CREDIT_TYPE_TEMP);
|
|
||||||
flow.setChangeType(CustomerCreditFlow.CHANGE_EXPIRE);
|
|
||||||
flow.setDirection(-1);
|
|
||||||
flow.setBeforeAmount(before);
|
|
||||||
flow.setChangeAmount(before);
|
|
||||||
flow.setAfterAmount(BigDecimal.ZERO);
|
|
||||||
flow.setUsedCredit(nz(account.getUsedCredit()));
|
|
||||||
flow.setEffectiveDate(account.getExtraEffectiveDate());
|
|
||||||
flow.setExpireDate(account.getExtraExpireDate());
|
|
||||||
flow.setReason("临时额度到期自动失效");
|
|
||||||
flow.setOperatorId(SecurityUtils.getUserId());
|
|
||||||
flow.setCreateBy(username);
|
|
||||||
flow.setRemark("系统在访问账户时检测到临时额度已过失效日期,自动置零");
|
|
||||||
flow.setDelFlag("0");
|
|
||||||
customerMoneyMapper.insertCreditFlow(flow);
|
|
||||||
}
|
|
||||||
|
|
||||||
private LocalDate toLocalDate(Date date)
|
private LocalDate toLocalDate(Date date)
|
||||||
{
|
{
|
||||||
return date.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate();
|
return date.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate();
|
||||||
|
|||||||
@@ -31,8 +31,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
case l.object_type
|
case l.object_type
|
||||||
when 'BILL' then b.bill_no
|
when 'BILL' then b.bill_no
|
||||||
when 'EXPORT' then coalesce(e.file_name, b.bill_no)
|
when 'EXPORT' then coalesce(e.file_name, b.bill_no)
|
||||||
when 'RECEIPT' then r.receipt_no
|
when 'BALANCE_WRITEOFF' then bw.writeoff_no
|
||||||
when 'WRITEOFF' then w.writeoff_no
|
|
||||||
else null
|
else null
|
||||||
end as object_display,
|
end as object_display,
|
||||||
l.from_status, l.to_status, l.before_data, l.after_data, l.operator_id, l.operator_name,
|
l.from_status, l.to_status, l.before_data, l.after_data, l.operator_id, l.operator_name,
|
||||||
@@ -40,8 +39,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
from ar_bill_log l
|
from ar_bill_log l
|
||||||
left join ar_bill b on b.ar_bill_id = l.ar_bill_id
|
left join ar_bill b on b.ar_bill_id = l.ar_bill_id
|
||||||
left join ar_bill_export e on e.export_id = case when l.object_type = 'EXPORT' then cast(l.object_id as unsigned) end
|
left join ar_bill_export e on e.export_id = case when l.object_type = 'EXPORT' then cast(l.object_id as unsigned) end
|
||||||
left join ar_receipt r on r.receipt_id = case when l.object_type = 'RECEIPT' then cast(l.object_id as unsigned) end
|
left join ar_balance_writeoff bw on bw.writeoff_id = case when l.object_type = 'BALANCE_WRITEOFF' then cast(l.object_id as unsigned) end
|
||||||
left join ar_receipt_writeoff w on w.writeoff_id = case when l.object_type = 'WRITEOFF' then cast(l.object_id as unsigned) end
|
|
||||||
<where>
|
<where>
|
||||||
<if test="arBillId != null">and l.ar_bill_id = #{arBillId}</if>
|
<if test="arBillId != null">and l.ar_bill_id = #{arBillId}</if>
|
||||||
<if test="objectType != null and objectType != ''">and l.object_type = #{objectType}</if>
|
<if test="objectType != null and objectType != ''">and l.object_type = #{objectType}</if>
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<!DOCTYPE mapper
|
|
||||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|
||||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
|
||||||
<mapper namespace="com.ruoyi.ar.mapper.ArReceiptMapper">
|
|
||||||
<resultMap id="ArReceiptResult" type="ArReceipt">
|
|
||||||
<id property="receiptId" column="receipt_id"/>
|
|
||||||
<result property="receiptNo" column="receipt_no"/>
|
|
||||||
<result property="customerId" column="customer_id"/>
|
|
||||||
<result property="customerName" column="customer_name"/>
|
|
||||||
<result property="customerCode" column="customer_code"/>
|
|
||||||
<result property="receiptDate" column="receipt_date"/>
|
|
||||||
<result property="currency" column="currency"/>
|
|
||||||
<result property="receiptAmount" column="receipt_amount"/>
|
|
||||||
<result property="unallocatedAmount" column="unallocated_amount"/>
|
|
||||||
<result property="paymentMethod" column="payment_method"/>
|
|
||||||
<result property="bankReference" column="bank_reference"/>
|
|
||||||
<result property="status" column="status"/>
|
|
||||||
<result property="remark" column="remark"/>
|
|
||||||
<result property="createBy" column="create_by"/>
|
|
||||||
<result property="createTime" column="create_time"/>
|
|
||||||
<result property="updateBy" column="update_by"/>
|
|
||||||
<result property="updateTime" column="update_time"/>
|
|
||||||
</resultMap>
|
|
||||||
|
|
||||||
<sql id="selectReceiptColumns">
|
|
||||||
select r.receipt_id, r.receipt_no, r.customer_id, c.customer_name, c.customer_code,
|
|
||||||
r.receipt_date, r.currency, r.receipt_amount, r.unallocated_amount,
|
|
||||||
r.payment_method, r.bank_reference, r.status, r.remark,
|
|
||||||
r.create_by, r.create_time, r.update_by, r.update_time
|
|
||||||
from ar_receipt r
|
|
||||||
left join bms_customer c on c.customer_id = r.customer_id and c.del_flag = '0'
|
|
||||||
</sql>
|
|
||||||
|
|
||||||
<!-- 加锁更新只读取收款主表,避免锁定展示关联表。 -->
|
|
||||||
<sql id="selectReceiptLockColumns">
|
|
||||||
select receipt_id, receipt_no, customer_id, receipt_date, currency, receipt_amount,
|
|
||||||
unallocated_amount, payment_method, bank_reference, status, remark,
|
|
||||||
create_by, create_time, update_by, update_time
|
|
||||||
from ar_receipt
|
|
||||||
</sql>
|
|
||||||
|
|
||||||
<select id="selectArReceiptList" parameterType="com.ruoyi.ar.domain.vo.ArReceiptQuery" resultMap="ArReceiptResult">
|
|
||||||
<include refid="selectReceiptColumns"/>
|
|
||||||
<where>
|
|
||||||
<if test="receiptNo != null and receiptNo != ''">and r.receipt_no like concat('%', #{receiptNo}, '%')</if>
|
|
||||||
<if test="customerId != null">and r.customer_id = #{customerId}</if>
|
|
||||||
<if test="currency != null and currency != ''">and r.currency = #{currency}</if>
|
|
||||||
<if test="paymentMethod != null and paymentMethod != ''">and r.payment_method = #{paymentMethod}</if>
|
|
||||||
<if test="status != null and status != ''">and r.status = #{status}</if>
|
|
||||||
<if test="receiptDateStart != null">and r.receipt_date >= #{receiptDateStart}</if>
|
|
||||||
<if test="receiptDateEnd != null">and r.receipt_date <= #{receiptDateEnd}</if>
|
|
||||||
</where>
|
|
||||||
order by r.receipt_date desc, r.receipt_id desc
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="selectArReceiptById" parameterType="long" resultMap="ArReceiptResult">
|
|
||||||
<include refid="selectReceiptColumns"/> where r.receipt_id = #{receiptId}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="selectArReceiptByIdForUpdate" parameterType="long" resultMap="ArReceiptResult">
|
|
||||||
<include refid="selectReceiptLockColumns"/> where receipt_id = #{receiptId} for update
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="selectArReceiptByNo" parameterType="string" resultMap="ArReceiptResult">
|
|
||||||
<include refid="selectReceiptColumns"/> where r.receipt_no = #{receiptNo}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="selectArReceiptByReference" resultMap="ArReceiptResult">
|
|
||||||
<include refid="selectReceiptColumns"/>
|
|
||||||
where r.payment_method = #{paymentMethod} and r.bank_reference = #{bankReference}
|
|
||||||
limit 1
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<insert id="insertArReceipt" parameterType="ArReceipt" useGeneratedKeys="true" keyProperty="receiptId">
|
|
||||||
insert into ar_receipt (
|
|
||||||
receipt_no, customer_id, receipt_date, currency, receipt_amount, unallocated_amount,
|
|
||||||
payment_method, bank_reference, status, remark, create_by, create_time, update_by, update_time
|
|
||||||
) values (
|
|
||||||
#{receiptNo}, #{customerId}, #{receiptDate}, #{currency}, #{receiptAmount}, #{unallocatedAmount},
|
|
||||||
#{paymentMethod}, #{bankReference}, #{status}, #{remark}, #{createBy}, sysdate(), #{updateBy}, sysdate()
|
|
||||||
)
|
|
||||||
</insert>
|
|
||||||
|
|
||||||
<update id="updateBalanceAndStatus">
|
|
||||||
update ar_receipt
|
|
||||||
set unallocated_amount = #{unallocatedAmount}, status = #{status},
|
|
||||||
update_by = #{operator}, update_time = sysdate()
|
|
||||||
where receipt_id = #{receiptId}
|
|
||||||
</update>
|
|
||||||
|
|
||||||
<update id="updateCancelled">
|
|
||||||
update ar_receipt set status = 'CANCELLED', remark = #{remark},
|
|
||||||
update_by = #{operator}, update_time = sysdate()
|
|
||||||
where receipt_id = #{receiptId}
|
|
||||||
</update>
|
|
||||||
</mapper>
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<!DOCTYPE mapper
|
|
||||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|
||||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
|
||||||
<mapper namespace="com.ruoyi.ar.mapper.ArReceiptWriteoffMapper">
|
|
||||||
<resultMap id="ArReceiptWriteoffResult" type="ArReceiptWriteoff">
|
|
||||||
<id property="writeoffId" column="writeoff_id"/>
|
|
||||||
<result property="receiptId" column="receipt_id"/>
|
|
||||||
<result property="arBillId" column="ar_bill_id"/>
|
|
||||||
<result property="billNo" column="bill_no"/>
|
|
||||||
<result property="receiptNo" column="receipt_no"/>
|
|
||||||
<result property="writeoffNo" column="writeoff_no"/>
|
|
||||||
<result property="writeoffAmount" column="writeoff_amount"/>
|
|
||||||
<result property="currency" column="currency"/>
|
|
||||||
<result property="writeoffDate" column="writeoff_date"/>
|
|
||||||
<result property="status" column="status"/>
|
|
||||||
<result property="remark" column="remark"/>
|
|
||||||
<result property="createBy" column="create_by"/>
|
|
||||||
<result property="createTime" column="create_time"/>
|
|
||||||
<result property="cancelBy" column="cancel_by"/>
|
|
||||||
<result property="cancelTime" column="cancel_time"/>
|
|
||||||
</resultMap>
|
|
||||||
|
|
||||||
<sql id="selectWriteoffColumns">
|
|
||||||
select w.writeoff_id, w.receipt_id, w.ar_bill_id, b.bill_no, r.receipt_no,
|
|
||||||
w.writeoff_no, w.writeoff_amount, w.currency, w.writeoff_date, w.status,
|
|
||||||
w.remark, w.create_by, w.create_time, w.cancel_by, w.cancel_time
|
|
||||||
from ar_receipt_writeoff w
|
|
||||||
left join ar_bill b on b.ar_bill_id = w.ar_bill_id
|
|
||||||
left join ar_receipt r on r.receipt_id = w.receipt_id
|
|
||||||
</sql>
|
|
||||||
|
|
||||||
<!-- 加锁更新只读取核销关系主表,展示字段不参与锁定。 -->
|
|
||||||
<sql id="selectWriteoffLockColumns">
|
|
||||||
select writeoff_id, receipt_id, ar_bill_id, writeoff_no, writeoff_amount, currency,
|
|
||||||
writeoff_date, status, remark, create_by, create_time, cancel_by, cancel_time
|
|
||||||
from ar_receipt_writeoff
|
|
||||||
</sql>
|
|
||||||
|
|
||||||
<select id="selectByBillId" parameterType="long" resultMap="ArReceiptWriteoffResult">
|
|
||||||
<include refid="selectWriteoffColumns"/> where w.ar_bill_id = #{arBillId} order by w.create_time desc
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="selectByReceiptId" parameterType="long" resultMap="ArReceiptWriteoffResult">
|
|
||||||
<include refid="selectWriteoffColumns"/> where w.receipt_id = #{receiptId} order by w.create_time desc
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="selectById" parameterType="long" resultMap="ArReceiptWriteoffResult">
|
|
||||||
<include refid="selectWriteoffColumns"/> where w.writeoff_id = #{writeoffId}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="selectByIdForUpdate" parameterType="long" resultMap="ArReceiptWriteoffResult">
|
|
||||||
<include refid="selectWriteoffLockColumns"/> where writeoff_id = #{writeoffId} for update
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<insert id="insert" parameterType="ArReceiptWriteoff" useGeneratedKeys="true" keyProperty="writeoffId">
|
|
||||||
insert into ar_receipt_writeoff (
|
|
||||||
receipt_id, ar_bill_id, writeoff_no, writeoff_amount, currency, writeoff_date,
|
|
||||||
status, remark, create_by, create_time
|
|
||||||
) values (
|
|
||||||
#{receiptId}, #{arBillId}, #{writeoffNo}, #{writeoffAmount}, #{currency}, #{writeoffDate},
|
|
||||||
#{status}, #{remark}, #{createBy}, sysdate()
|
|
||||||
)
|
|
||||||
</insert>
|
|
||||||
|
|
||||||
<update id="updateCancelled">
|
|
||||||
update ar_receipt_writeoff
|
|
||||||
set status = 'CANCELLED', remark = #{remark}, cancel_by = #{cancelBy}, cancel_time = sysdate()
|
|
||||||
where writeoff_id = #{writeoffId} and status = 'EFFECTIVE'
|
|
||||||
</update>
|
|
||||||
|
|
||||||
<select id="sumEffectiveByBillId" parameterType="long" resultType="java.math.BigDecimal">
|
|
||||||
select coalesce(sum(writeoff_amount), 0) from ar_receipt_writeoff
|
|
||||||
where ar_bill_id = #{arBillId} and status = 'EFFECTIVE'
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="sumEffectiveByReceiptId" parameterType="long" resultType="java.math.BigDecimal">
|
|
||||||
select coalesce(sum(writeoff_amount), 0) from ar_receipt_writeoff
|
|
||||||
where receipt_id = #{receiptId} and status = 'EFFECTIVE'
|
|
||||||
</select>
|
|
||||||
</mapper>
|
|
||||||
+129
@@ -0,0 +1,129 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper
|
||||||
|
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.ruoyi.customer.customermoney.mapper.CustomerCreditApplyMapper">
|
||||||
|
|
||||||
|
<resultMap type="com.ruoyi.customer.customermoney.domain.CustomerCreditApply" id="CustomerCreditApplyResult">
|
||||||
|
<id property="applyId" column="apply_id" />
|
||||||
|
<result property="applyNo" column="apply_no" />
|
||||||
|
<result property="customerId" column="customer_id" />
|
||||||
|
<result property="accountId" column="account_id" />
|
||||||
|
<result property="creditType" column="credit_type" />
|
||||||
|
<result property="adjustMode" column="adjust_mode" />
|
||||||
|
<result property="changeAmount" column="change_amount" />
|
||||||
|
<result property="effectiveDate" column="effective_date" />
|
||||||
|
<result property="expireDate" column="expire_date" />
|
||||||
|
<result property="reason" column="reason" />
|
||||||
|
<result property="status" column="status" />
|
||||||
|
<result property="applyBy" column="apply_by" />
|
||||||
|
<result property="applyUserId" column="apply_user_id" />
|
||||||
|
<result property="applyTime" column="apply_time" />
|
||||||
|
<result property="approveBy" column="approve_by" />
|
||||||
|
<result property="approveUserId" column="approve_user_id"/>
|
||||||
|
<result property="approveTime" column="approve_time" />
|
||||||
|
<result property="approveRemark" column="approve_remark" />
|
||||||
|
<result property="flowId" column="flow_id" />
|
||||||
|
<result property="createBy" column="create_by" />
|
||||||
|
<result property="createTime" column="create_time" />
|
||||||
|
<result property="updateBy" column="update_by" />
|
||||||
|
<result property="updateTime" column="update_time" />
|
||||||
|
<result property="remark" column="remark" />
|
||||||
|
<result property="delFlag" column="del_flag" />
|
||||||
|
<result property="customerCode" column="customer_code" />
|
||||||
|
<result property="customerName" column="customer_name" />
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<sql id="applyVo">
|
||||||
|
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
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<select id="selectApplyList"
|
||||||
|
parameterType="com.ruoyi.customer.customermoney.domain.CustomerCreditApply"
|
||||||
|
resultMap="CustomerCreditApplyResult">
|
||||||
|
<include refid="applyVo"/>
|
||||||
|
<where>
|
||||||
|
a.del_flag = '0'
|
||||||
|
<if test="customerId != null"> and a.customer_id = #{customerId}</if>
|
||||||
|
<if test="status != null and status != ''"> and a.status = #{status}</if>
|
||||||
|
<if test="creditType != null and creditType != ''"> and a.credit_type = #{creditType}</if>
|
||||||
|
<if test="applyNo != null and applyNo != ''"> and a.apply_no like concat('%', #{applyNo}, '%')</if>
|
||||||
|
<if test="params.beginTime != null and params.beginTime != ''"><!-- 申请时间范围 -->
|
||||||
|
and date_format(a.apply_time, '%y%m%d') >= date_format(#{params.beginTime}, '%y%m%d')
|
||||||
|
</if>
|
||||||
|
<if test="params.endTime != null and params.endTime != ''">
|
||||||
|
and date_format(a.apply_time, '%y%m%d') <= date_format(#{params.endTime}, '%y%m%d')
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
order by a.apply_id desc
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectApplyById" resultMap="CustomerCreditApplyResult">
|
||||||
|
<include refid="applyVo"/>
|
||||||
|
where a.apply_id = #{applyId} and a.del_flag = '0'
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectApplyByIdForUpdate" resultMap="CustomerCreditApplyResult">
|
||||||
|
select * from customer_credit_apply where apply_id = #{applyId} and del_flag = '0' for update
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insertApply"
|
||||||
|
parameterType="com.ruoyi.customer.customermoney.domain.CustomerCreditApply"
|
||||||
|
useGeneratedKeys="true" keyProperty="applyId">
|
||||||
|
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}
|
||||||
|
)
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="updateApproved"
|
||||||
|
parameterType="com.ruoyi.customer.customermoney.domain.CustomerCreditApply">
|
||||||
|
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>
|
||||||
|
|
||||||
|
<update id="updateRejected"
|
||||||
|
parameterType="com.ruoyi.customer.customermoney.domain.CustomerCreditApply">
|
||||||
|
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>
|
||||||
|
|
||||||
|
<update id="updateCancelled">
|
||||||
|
update customer_credit_apply
|
||||||
|
set status = 'CANCELLED',
|
||||||
|
update_by = #{updateBy},
|
||||||
|
update_time = sysdate()
|
||||||
|
where apply_id = #{applyId} and status = 'PENDING' and del_flag = '0'
|
||||||
|
</update>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@@ -114,6 +114,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<result property="accountId" column="account_id" />
|
<result property="accountId" column="account_id" />
|
||||||
<result property="creditType" column="credit_type" />
|
<result property="creditType" column="credit_type" />
|
||||||
<result property="changeType" column="change_type" />
|
<result property="changeType" column="change_type" />
|
||||||
|
<result property="bizType" column="biz_type" />
|
||||||
|
<result property="bizId" column="biz_id" />
|
||||||
|
<result property="bizNo" column="biz_no" />
|
||||||
<result property="direction" column="direction" />
|
<result property="direction" column="direction" />
|
||||||
<result property="beforeAmount" column="before_amount" />
|
<result property="beforeAmount" column="before_amount" />
|
||||||
<result property="changeAmount" column="change_amount" />
|
<result property="changeAmount" column="change_amount" />
|
||||||
@@ -133,7 +136,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
</resultMap>
|
</resultMap>
|
||||||
|
|
||||||
<sql id="selectCustomerCreditFlowVo">
|
<sql id="selectCustomerCreditFlowVo">
|
||||||
select id, flow_no, customer_id, account_id, credit_type, change_type, direction,
|
select id, flow_no, customer_id, account_id, credit_type, change_type, biz_type, biz_id, biz_no, direction,
|
||||||
before_amount, change_amount, after_amount, used_credit,
|
before_amount, change_amount, after_amount, used_credit,
|
||||||
effective_date, expire_date, reason, adjust_mode, operator_id,
|
effective_date, expire_date, reason, adjust_mode, operator_id,
|
||||||
create_by, create_time, update_by, update_time, remark, del_flag
|
create_by, create_time, update_by, update_time, remark, del_flag
|
||||||
@@ -147,6 +150,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<if test="customerId != null"> and customer_id = #{customerId}</if>
|
<if test="customerId != null"> and customer_id = #{customerId}</if>
|
||||||
<if test="creditType != null and creditType != ''"> and credit_type = #{creditType}</if>
|
<if test="creditType != null and creditType != ''"> and credit_type = #{creditType}</if>
|
||||||
<if test="changeType != null and changeType != ''"> and change_type = #{changeType}</if>
|
<if test="changeType != null and changeType != ''"> and change_type = #{changeType}</if>
|
||||||
|
<if test="bizType != null and bizType != ''"> and biz_type = #{bizType}</if>
|
||||||
|
<if test="bizId != null"> and biz_id = #{bizId}</if>
|
||||||
<if test="params.beginTime != null and params.beginTime != ''"> and date(create_time) >= #{params.beginTime}</if>
|
<if test="params.beginTime != null and params.beginTime != ''"> and date(create_time) >= #{params.beginTime}</if>
|
||||||
<if test="params.endTime != null and params.endTime != ''"> and date(create_time) <= #{params.endTime}</if>
|
<if test="params.endTime != null and params.endTime != ''"> and date(create_time) <= #{params.endTime}</if>
|
||||||
and del_flag = '0'
|
and del_flag = '0'
|
||||||
@@ -163,6 +168,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<if test="accountId != null">account_id,</if>
|
<if test="accountId != null">account_id,</if>
|
||||||
<if test="creditType != null">credit_type,</if>
|
<if test="creditType != null">credit_type,</if>
|
||||||
<if test="changeType != null">change_type,</if>
|
<if test="changeType != null">change_type,</if>
|
||||||
|
<if test="bizType != null">biz_type,</if>
|
||||||
|
<if test="bizId != null">biz_id,</if>
|
||||||
|
<if test="bizNo != null">biz_no,</if>
|
||||||
<if test="direction != null">direction,</if>
|
<if test="direction != null">direction,</if>
|
||||||
<if test="beforeAmount != null">before_amount,</if>
|
<if test="beforeAmount != null">before_amount,</if>
|
||||||
<if test="changeAmount != null">change_amount,</if>
|
<if test="changeAmount != null">change_amount,</if>
|
||||||
@@ -183,6 +191,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<if test="accountId != null">#{accountId},</if>
|
<if test="accountId != null">#{accountId},</if>
|
||||||
<if test="creditType != null">#{creditType},</if>
|
<if test="creditType != null">#{creditType},</if>
|
||||||
<if test="changeType != null">#{changeType},</if>
|
<if test="changeType != null">#{changeType},</if>
|
||||||
|
<if test="bizType != null">#{bizType},</if>
|
||||||
|
<if test="bizId != null">#{bizId},</if>
|
||||||
|
<if test="bizNo != null">#{bizNo},</if>
|
||||||
<if test="direction != null">#{direction},</if>
|
<if test="direction != null">#{direction},</if>
|
||||||
<if test="beforeAmount != null">#{beforeAmount},</if>
|
<if test="beforeAmount != null">#{beforeAmount},</if>
|
||||||
<if test="changeAmount != null">#{changeAmount},</if>
|
<if test="changeAmount != null">#{changeAmount},</if>
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
-- 3) 菜单与授权:客户账单(2435) / 账单详情(2436) / 余额核销(2437) / 反核销(2438)
|
-- 3) 菜单与授权:客户账单(2435) / 账单详情(2436) / 余额核销(2437) / 反核销(2438)
|
||||||
-- =====================================================================
|
-- =====================================================================
|
||||||
|
|
||||||
-- 余额核销单:用客户账户余额核销应收账单(与 ar_receipt_writeoff 收款核销并列,
|
-- 余额核销单:用客户账户余额核销应收账单,
|
||||||
-- 两者共享 ar_bill.written_off_amount / unwritten_amount 缓存字段)
|
-- 与客户账户余额流水配合,共享 ar_bill.written_off_amount / unwritten_amount 缓存字段
|
||||||
CREATE TABLE IF NOT EXISTS ar_balance_writeoff (
|
CREATE TABLE IF NOT EXISTS ar_balance_writeoff (
|
||||||
writeoff_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '余额核销ID',
|
writeoff_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '余额核销ID',
|
||||||
writeoff_no VARCHAR(50) NOT NULL COMMENT '核销单号(BWO开头)',
|
writeoff_no VARCHAR(50) NOT NULL COMMENT '核销单号(BWO开头)',
|
||||||
|
|||||||
@@ -98,51 +98,6 @@ CREATE TABLE IF NOT EXISTS ar_bill_detail_fee (
|
|||||||
CONSTRAINT fk_ar_detail_fee_detail FOREIGN KEY (detail_id) REFERENCES ar_bill_detail (detail_id)
|
CONSTRAINT fk_ar_detail_fee_detail FOREIGN KEY (detail_id) REFERENCES ar_bill_detail (detail_id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='应收账单费用快照';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='应收账单费用快照';
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS ar_receipt (
|
|
||||||
receipt_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '收款ID',
|
|
||||||
receipt_no VARCHAR(50) NOT NULL COMMENT '收款单号',
|
|
||||||
customer_id BIGINT(20) NOT NULL COMMENT '付款客户ID',
|
|
||||||
receipt_date DATE NOT NULL COMMENT '到账日期',
|
|
||||||
currency VARCHAR(10) NOT NULL DEFAULT 'CNY' COMMENT '币种',
|
|
||||||
receipt_amount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '到账总额',
|
|
||||||
unallocated_amount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '未分配金额缓存',
|
|
||||||
payment_method VARCHAR(30) NOT NULL COMMENT '到账方式',
|
|
||||||
bank_reference VARCHAR(100) DEFAULT NULL COMMENT '银行流水号或第三方交易号',
|
|
||||||
status VARCHAR(30) NOT NULL COMMENT 'REGISTERED/PARTIAL_WRITTEN_OFF/WRITTEN_OFF/CANCELLED',
|
|
||||||
remark VARCHAR(500) DEFAULT NULL COMMENT '收款备注或取消原因',
|
|
||||||
create_by VARCHAR(64) NOT NULL COMMENT '创建人',
|
|
||||||
create_time DATETIME NOT NULL COMMENT '创建时间',
|
|
||||||
update_by VARCHAR(64) NOT NULL COMMENT '更新人',
|
|
||||||
update_time DATETIME NOT NULL COMMENT '更新时间',
|
|
||||||
PRIMARY KEY (receipt_id),
|
|
||||||
UNIQUE KEY uk_ar_receipt_no (receipt_no),
|
|
||||||
UNIQUE KEY uk_ar_payment_reference (payment_method, bank_reference),
|
|
||||||
KEY idx_ar_receipt_customer_date (customer_id, receipt_date),
|
|
||||||
KEY idx_ar_receipt_status (status)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='应收收款表';
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS ar_receipt_writeoff (
|
|
||||||
writeoff_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '核销ID',
|
|
||||||
receipt_id BIGINT(20) NOT NULL COMMENT '收款ID',
|
|
||||||
ar_bill_id BIGINT(20) NOT NULL COMMENT '应收账单ID',
|
|
||||||
writeoff_no VARCHAR(50) NOT NULL COMMENT '核销单号',
|
|
||||||
writeoff_amount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '本次核销金额',
|
|
||||||
currency VARCHAR(10) NOT NULL COMMENT '币种',
|
|
||||||
writeoff_date DATE NOT NULL COMMENT '核销日期',
|
|
||||||
status VARCHAR(30) NOT NULL COMMENT 'EFFECTIVE/CANCELLED',
|
|
||||||
remark VARCHAR(500) DEFAULT NULL COMMENT '核销备注或取消原因',
|
|
||||||
create_by VARCHAR(64) NOT NULL COMMENT '创建人',
|
|
||||||
create_time DATETIME NOT NULL COMMENT '创建时间',
|
|
||||||
cancel_by VARCHAR(64) DEFAULT NULL COMMENT '取消人',
|
|
||||||
cancel_time DATETIME DEFAULT NULL COMMENT '取消时间',
|
|
||||||
PRIMARY KEY (writeoff_id),
|
|
||||||
UNIQUE KEY uk_ar_writeoff_no (writeoff_no),
|
|
||||||
KEY idx_ar_writeoff_receipt (receipt_id, status),
|
|
||||||
KEY idx_ar_writeoff_bill (ar_bill_id, status),
|
|
||||||
CONSTRAINT fk_ar_writeoff_receipt FOREIGN KEY (receipt_id) REFERENCES ar_receipt (receipt_id),
|
|
||||||
CONSTRAINT fk_ar_writeoff_bill FOREIGN KEY (ar_bill_id) REFERENCES ar_bill (ar_bill_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='应收收款核销表';
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS ar_bill_export (
|
CREATE TABLE IF NOT EXISTS ar_bill_export (
|
||||||
export_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '导出ID',
|
export_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '导出ID',
|
||||||
ar_bill_id BIGINT(20) NOT NULL COMMENT '应收账单ID',
|
ar_bill_id BIGINT(20) NOT NULL COMMENT '应收账单ID',
|
||||||
@@ -167,9 +122,9 @@ CREATE TABLE IF NOT EXISTS ar_bill_export (
|
|||||||
CREATE TABLE IF NOT EXISTS ar_bill_log (
|
CREATE TABLE IF NOT EXISTS ar_bill_log (
|
||||||
log_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '日志ID',
|
log_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '日志ID',
|
||||||
ar_bill_id BIGINT(20) DEFAULT NULL COMMENT '账单ID',
|
ar_bill_id BIGINT(20) DEFAULT NULL COMMENT '账单ID',
|
||||||
object_type VARCHAR(40) NOT NULL COMMENT 'BILL/RECEIPT/WRITEOFF/EXPORT',
|
object_type VARCHAR(40) NOT NULL COMMENT 'BILL/EXPORT/BALANCE_WRITEOFF',
|
||||||
object_id VARCHAR(64) NOT NULL COMMENT '通用对象ID',
|
object_id VARCHAR(64) NOT NULL COMMENT '通用对象ID',
|
||||||
operation_type VARCHAR(40) NOT NULL COMMENT '操作类型',
|
operation_type VARCHAR(40) NOT NULL COMMENT '操作类型:CONFIRM/VOID/EXPORT/BALANCE_WRITEOFF/CANCEL_BALANCE_WRITEOFF',
|
||||||
operation_desc VARCHAR(500) DEFAULT NULL COMMENT '操作说明',
|
operation_desc VARCHAR(500) DEFAULT NULL COMMENT '操作说明',
|
||||||
from_status VARCHAR(30) DEFAULT NULL COMMENT '变更前状态',
|
from_status VARCHAR(30) DEFAULT NULL COMMENT '变更前状态',
|
||||||
to_status VARCHAR(30) DEFAULT NULL COMMENT '变更后状态',
|
to_status VARCHAR(30) DEFAULT NULL COMMENT '变更后状态',
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- 应收日志兼容余额核销:仅更新字段注释,不改变字段类型和历史数据
|
||||||
|
ALTER TABLE ar_bill_log
|
||||||
|
MODIFY COLUMN object_type VARCHAR(40) NOT NULL
|
||||||
|
COMMENT 'BILL/EXPORT/BALANCE_WRITEOFF',
|
||||||
|
MODIFY COLUMN operation_type VARCHAR(40) NOT NULL
|
||||||
|
COMMENT '操作类型:CONFIRM/VOID/EXPORT/BALANCE_WRITEOFF/CANCEL_BALANCE_WRITEOFF';
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
SET NAMES utf8mb4;
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||||
SELECT 2400, N'应收管理', 0, 10, 'ar', NULL, '', '', 1, 0, 'M', '0', '0', '', 'money', 'admin', sysdate(), N'应收账单、收款与核销'
|
SELECT 2400, N'应收管理', 0, 10, 'ar', NULL, '', '', 1, 0, 'M', '0', '0', '', 'money', 'admin', sysdate(), N'应收账单、余额核销与日志'
|
||||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2400);
|
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2400);
|
||||||
|
|
||||||
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||||
@@ -24,22 +24,6 @@ INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component,
|
|||||||
SELECT 2406, N'导出账单', 2401, 5, '', NULL, 1, 0, 'F', '0', '0', 'ar:bill:export', '#', 'admin', sysdate()
|
SELECT 2406, N'导出账单', 2401, 5, '', NULL, 1, 0, 'F', '0', '0', 'ar:bill:export', '#', 'admin', sysdate()
|
||||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2406);
|
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2406);
|
||||||
|
|
||||||
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
|
||||||
SELECT 2410, N'应收收款', 2400, 2, 'receipt', 'ar/receipt/index', 'ArReceipt', 1, 0, 'C', '0', '0', 'ar:receipt:list', 'money', 'admin', sysdate(), N'收款登记与核销'
|
|
||||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2410);
|
|
||||||
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)
|
|
||||||
SELECT 2411, N'收款登记', 2410, 1, '', NULL, 1, 0, 'F', '0', '0', 'ar:receipt:add', '#', 'admin', sysdate()
|
|
||||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2411);
|
|
||||||
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)
|
|
||||||
SELECT 2412, N'批量核销', 2410, 2, '', NULL, 1, 0, 'F', '0', '0', 'ar:receipt:writeoff', '#', 'admin', sysdate()
|
|
||||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2412);
|
|
||||||
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)
|
|
||||||
SELECT 2413, N'取消核销', 2410, 3, '', NULL, 1, 0, 'F', '0', '0', 'ar:receipt:cancel', '#', 'admin', sysdate()
|
|
||||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2413);
|
|
||||||
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)
|
|
||||||
SELECT 2414, N'取消收款', 2410, 4, '', NULL, 1, 0, 'F', '0', '0', 'ar:receipt:cancel', '#', 'admin', sysdate()
|
|
||||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2414);
|
|
||||||
|
|
||||||
INSERT INTO sys_menu (
|
INSERT INTO sys_menu (
|
||||||
menu_id, menu_name, parent_id, order_num, path, component,
|
menu_id, menu_name, parent_id, order_num, path, component,
|
||||||
is_frame, is_cache, menu_type, visible, status, perms, icon,
|
is_frame, is_cache, menu_type, visible, status, perms, icon,
|
||||||
@@ -53,5 +37,5 @@ FROM DUAL
|
|||||||
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2420);
|
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2420);
|
||||||
|
|
||||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||||
SELECT 1, m.menu_id FROM (SELECT 2400 AS menu_id UNION SELECT 2401 UNION SELECT 2402 UNION SELECT 2403 UNION SELECT 2404 UNION SELECT 2405 UNION SELECT 2406 UNION SELECT 2410 UNION SELECT 2411 UNION SELECT 2412 UNION SELECT 2413 UNION SELECT 2414 UNION SELECT 2420) m
|
SELECT 1, m.menu_id FROM (SELECT 2400 AS menu_id UNION SELECT 2401 UNION SELECT 2402 UNION SELECT 2403 UNION SELECT 2404 UNION SELECT 2405 UNION SELECT 2406 UNION SELECT 2420) m
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = m.menu_id);
|
WHERE NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = m.menu_id);
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
-- 应收收款独立模块下线
|
||||||
|
-- 业务闭环改为:客户收款登记/审核 -> 客户余额 -> 余额核销账单。
|
||||||
|
-- 本脚本会永久删除 ar_receipt、ar_receipt_writeoff 及其历史数据,执行前请确认已完成备份。
|
||||||
|
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
-- 先删除子表,再删除收款主表
|
||||||
|
DROP TABLE IF EXISTS ar_receipt_writeoff;
|
||||||
|
DROP TABLE IF EXISTS ar_receipt;
|
||||||
|
|
||||||
|
-- 删除应收管理下独立“应收收款”菜单及按钮权限
|
||||||
|
DELETE FROM sys_role_menu WHERE menu_id IN (2410, 2411, 2412, 2413, 2414);
|
||||||
|
DELETE FROM sys_menu WHERE menu_id IN (2410, 2411, 2412, 2413, 2414);
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
@@ -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');
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
-- =============================================================
|
||||||
|
-- 额度流水增加业务来源关联列
|
||||||
|
-- 授信额度的占用/释放由各业务模块(应收账单确认、计费订单开单/计费变动)
|
||||||
|
-- 通过客户资金公共API ICustomerAccountApi 触发,流水需能反查来源单据
|
||||||
|
-- 2026-09-17
|
||||||
|
-- =============================================================
|
||||||
|
ALTER TABLE `customer_credit_flow`
|
||||||
|
ADD COLUMN `biz_type` VARCHAR(32) DEFAULT NULL COMMENT '业务来源类型:AR_BILL应收账单 ORDER计费订单(占用/释放流水)' AFTER `change_type`,
|
||||||
|
ADD COLUMN `biz_id` BIGINT(20) DEFAULT NULL COMMENT '关联业务单据ID' AFTER `biz_type`,
|
||||||
|
ADD COLUMN `biz_no` VARCHAR(64) DEFAULT NULL COMMENT '关联业务单据号' AFTER `biz_id`;
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import request from '@/utils/request'
|
|
||||||
|
|
||||||
export function listArReceipt(query) {
|
|
||||||
return request({ url: '/ar/receipt/list', method: 'get', params: query })
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addArReceipt(data) {
|
|
||||||
return request({ url: '/ar/receipt', method: 'post', data })
|
|
||||||
}
|
|
||||||
|
|
||||||
export function cancelArReceipt(receiptId, data = {}) {
|
|
||||||
return request({ url: '/ar/receipt/' + receiptId + '/cancel', method: 'put', data })
|
|
||||||
}
|
|
||||||
|
|
||||||
export function listArWriteoffs(receiptId) {
|
|
||||||
return request({ url: '/ar/receipt/' + receiptId + '/writeoffs', method: 'get' })
|
|
||||||
}
|
|
||||||
|
|
||||||
export function writeoffArReceipt(data) {
|
|
||||||
return request({ url: '/ar/receipt/writeoff', method: 'post', data })
|
|
||||||
}
|
|
||||||
|
|
||||||
export function cancelArWriteoff(writeoffId, data = {}) {
|
|
||||||
return request({ url: '/ar/receipt/writeoff/' + writeoffId + '/cancel', method: 'put', data })
|
|
||||||
}
|
|
||||||
@@ -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'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -68,11 +68,47 @@ export function listCreditFlow(query) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 调整客户额度(信用额度/临时额度),调整过程留额度流水 */
|
/** 提交额度调整申请(信用额度/临时额度),审批通过后才调整额度 */
|
||||||
export function adjustCredit(data) {
|
export function applyCreditAdjust(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/customersupplier/customermoney/credit/adjust',
|
url: '/customersupplier/customermoney/credit/apply',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: data
|
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'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-container ar-bill-page">
|
<div class="app-container ar-bill-page">
|
||||||
<basic-table ref="tableRef" :columns="columns" :search-columns="searchColumns" :search-label-width="82" :query="queryList" row-key="arBillId" :selection="false">
|
<basic-table ref="tableRef" :columns="columns" :search-columns="searchColumns" :search-label-width="82" :query="queryList" :default-params="defaultSearchParams" row-key="arBillId" :selection="false">
|
||||||
<template #toolbar>
|
<template #toolbar>
|
||||||
<el-col :span="1.5"><el-button type="primary" plain icon="Plus" v-hasPermi="['ar:bill:generate']" @click="generateOpen = true">生成账单</el-button></el-col>
|
<el-col :span="1.5"><el-button type="primary" plain icon="Plus" v-hasPermi="['ar:bill:generate']" @click="generateOpen = true">生成账单</el-button></el-col>
|
||||||
</template>
|
</template>
|
||||||
@@ -32,7 +32,12 @@ import GenerateBillDialog from './components/GenerateBillDialog.vue'
|
|||||||
import { AR_BILL_STATUS, AR_BILL_STATUS_OPTIONS, AR_BILL_STATUS_TYPES } from '@/constants/ar/bill'
|
import { AR_BILL_STATUS, AR_BILL_STATUS_OPTIONS, AR_BILL_STATUS_TYPES } from '@/constants/ar/bill'
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance()
|
const { proxy } = getCurrentInstance()
|
||||||
|
const route = useRoute()
|
||||||
const tableRef = ref()
|
const tableRef = ref()
|
||||||
|
// 从客户余额页“查看账单”跳入时,按当前客户预筛选(重置后仍保留该条件)
|
||||||
|
const defaultSearchParams = computed(() => ({
|
||||||
|
customerId: route.query.customerId ? Number(route.query.customerId) : undefined
|
||||||
|
}))
|
||||||
const detailOpen = ref(false)
|
const detailOpen = ref(false)
|
||||||
const detailLoading = ref(false)
|
const detailLoading = ref(false)
|
||||||
const detail = ref(null)
|
const detail = ref(null)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
import { listArBillLogs } from '@/api/ar/bill'
|
import { listArBillLogs } from '@/api/ar/bill'
|
||||||
const tableRef = ref()
|
const tableRef = ref()
|
||||||
const searchColumns = [
|
const searchColumns = [
|
||||||
{ label: '对象类型', prop: 'objectType', component: 'select', options: [{ label: '应收账单', value: 'BILL' }, { label: '账单导出', value: 'EXPORT' }, { label: '应收收款', value: 'RECEIPT' }, { label: '收款核销', value: 'WRITEOFF' }], width: '130px' },
|
{ label: '对象类型', prop: 'objectType', component: 'select', options: [{ label: '应收账单', value: 'BILL' }, { label: '账单导出', value: 'EXPORT' }, { label: '余额核销', value: 'BALANCE_WRITEOFF' }], width: '130px' },
|
||||||
{ label: '操作类型', prop: 'operationType', component: 'input', width: '140px' }, { label: '结果', prop: 'result', component: 'select', options: [{ label: '成功', value: 'SUCCESS' }, { label: '失败', value: 'FAILED' }], width: '120px' }
|
{ label: '操作类型', prop: 'operationType', component: 'input', width: '140px' }, { label: '结果', prop: 'result', component: 'select', options: [{ label: '成功', value: 'SUCCESS' }, { label: '失败', value: 'FAILED' }], width: '120px' }
|
||||||
]
|
]
|
||||||
const columns = [
|
const columns = [
|
||||||
@@ -20,8 +20,8 @@ const columns = [
|
|||||||
{ label: '结果', prop: 'result', width: 90, slot: 'result' }, { label: '时间', prop: 'createTime', width: 170, parseTime: true }
|
{ label: '结果', prop: 'result', width: 90, slot: 'result' }, { label: '时间', prop: 'createTime', width: 170, parseTime: true }
|
||||||
]
|
]
|
||||||
|
|
||||||
const objectTypeLabels = { BILL: '应收账单', EXPORT: '账单导出', RECEIPT: '应收收款', WRITEOFF: '收款核销' }
|
const objectTypeLabels = { BILL: '应收账单', EXPORT: '账单导出', BALANCE_WRITEOFF: '余额核销' }
|
||||||
const operationLabels = { CONFIRM: '确认账单', VOID: '作废账单', EXPORT: '导出账单', RECEIPT: '登记收款', CANCEL_RECEIPT: '取消收款', WRITEOFF: '收款核销', CANCEL_WRITEOFF: '取消核销' }
|
const operationLabels = { CONFIRM: '确认账单', VOID: '作废账单', EXPORT: '导出账单', BALANCE_WRITEOFF: '余额核销', CANCEL_BALANCE_WRITEOFF: '取消余额核销' }
|
||||||
const statusLabels = { DRAFT: '草稿', CONFIRMED: '已确认', PARTIAL_WRITTEN_OFF: '部分核销', WRITTEN_OFF: '已核销', VOID: '已作废', REGISTERED: '已登记', CANCELLED: '已取消', EFFECTIVE: '有效' }
|
const statusLabels = { DRAFT: '草稿', CONFIRMED: '已确认', PARTIAL_WRITTEN_OFF: '部分核销', WRITTEN_OFF: '已核销', VOID: '已作废', REGISTERED: '已登记', CANCELLED: '已取消', EFFECTIVE: '有效' }
|
||||||
function objectTypeText(value) { return objectTypeLabels[value] || value || '-' }
|
function objectTypeText(value) { return objectTypeLabels[value] || value || '-' }
|
||||||
function operationText(value) { return operationLabels[value] || value || '-' }
|
function operationText(value) { return operationLabels[value] || value || '-' }
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="app-container ar-receipt-page">
|
|
||||||
<basic-table ref="tableRef" :columns="columns" :search-columns="searchColumns" :query="queryList" row-key="receiptId" :selection="false">
|
|
||||||
<template #search-customerId="{ model }">
|
|
||||||
<customer-select v-model="model.customerId" placeholder="请选择客户" class="receipt-customer-search" />
|
|
||||||
</template>
|
|
||||||
<template #toolbar><el-col :span="1.5"><el-button type="primary" plain icon="Plus" v-hasPermi="['ar:receipt:add']" @click="openRegister">登记收款</el-button></el-col></template>
|
|
||||||
<template #col-receiptAmount="{ row }"><span class="income">{{ row.currency }} {{ money(row.receiptAmount) }}</span></template>
|
|
||||||
<template #col-unallocatedAmount="{ row }"><span class="pending">{{ money(row.unallocatedAmount) }}</span></template>
|
|
||||||
<template #col-status="{ row }"><el-tag :type="receiptStatusType(row.status)">{{ receiptStatusText(row.status) }}</el-tag></template>
|
|
||||||
<template #col-action="{ row }">
|
|
||||||
<el-button v-if="row.status !== 'CANCELLED' && Number(row.unallocatedAmount) > 0" link type="primary" v-hasPermi="['ar:receipt:writeoff']" @click="openWriteoff(row)">核销</el-button>
|
|
||||||
<el-button v-if="row.status === 'REGISTERED'" link type="warning" v-hasPermi="['ar:receipt:cancel']" @click="cancelReceipt(row)">取消收款</el-button>
|
|
||||||
<el-button link type="primary" @click="openWriteoffHistory(row)">核销记录</el-button>
|
|
||||||
</template>
|
|
||||||
</basic-table>
|
|
||||||
|
|
||||||
<el-dialog v-model="registerOpen" title="登记应收收款" width="620px" append-to-body>
|
|
||||||
<el-form ref="registerFormRef" :model="registerForm" :rules="registerRules" label-width="110px">
|
|
||||||
<el-row :gutter="12">
|
|
||||||
<el-col :span="12"><el-form-item label="客户" prop="customerId"><customer-select v-model="registerForm.customerId" placeholder="请选择客户" /></el-form-item></el-col>
|
|
||||||
<el-col :span="12"><el-form-item label="到账日期" prop="receiptDate"><el-date-picker v-model="registerForm.receiptDate" type="date" value-format="YYYY-MM-DD" style="width: 100%;" /></el-form-item></el-col>
|
|
||||||
<el-col :span="12"><el-form-item label="到账金额" prop="receiptAmount"><el-input v-model="registerForm.receiptAmount" type="number" /></el-form-item></el-col>
|
|
||||||
<el-col :span="12"><el-form-item label="币种"><el-select v-model="registerForm.currency" style="width: 100%;"><el-option label="人民币 CNY" value="CNY" /><el-option label="美元 USD" value="USD" /></el-select></el-form-item></el-col>
|
|
||||||
<el-col :span="12"><el-form-item label="收款方式"><el-select v-model="registerForm.paymentMethod" style="width: 100%;"><el-option label="银行转账" value="BANK" /><el-option label="支付宝" value="ALIPAY" /><el-option label="微信" value="WECHAT" /><el-option label="现金" value="CASH" /></el-select></el-form-item></el-col>
|
|
||||||
<el-col :span="12"><el-form-item label="银行流水号"><el-input v-model="registerForm.bankReference" /></el-form-item></el-col>
|
|
||||||
<el-col :span="24"><el-form-item label="收款单号"><el-input v-model="registerForm.receiptNo" placeholder="不填则自动生成" /></el-form-item></el-col>
|
|
||||||
<el-col :span="24"><el-form-item label="备注"><el-input v-model="registerForm.remark" type="textarea" :rows="2" /></el-form-item></el-col>
|
|
||||||
</el-row>
|
|
||||||
</el-form>
|
|
||||||
<template #footer><el-button type="primary" :loading="registerLoading" @click="submitRegister">登记</el-button><el-button @click="registerOpen = false">取消</el-button></template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<el-dialog v-model="writeoffOpen" title="批量核销收款" width="680px" append-to-body>
|
|
||||||
<el-descriptions :column="3" border size="small" class="writeoff-summary"><el-descriptions-item label="收款单号">{{ writeoffForm.receiptNo }}</el-descriptions-item><el-descriptions-item label="币种">{{ writeoffForm.currency }}</el-descriptions-item><el-descriptions-item label="可核销">{{ money(writeoffForm.unallocatedAmount) }}</el-descriptions-item></el-descriptions>
|
|
||||||
<el-form label-width="90px" class="allocation-form"><div v-for="(item, index) in writeoffForm.allocations" :key="index" class="allocation-row"><el-form-item label="账单"><el-select v-model="item.arBillId" filterable clearable :loading="billLoading" placeholder="请选择账单" class="bill-select"><el-option v-for="bill in billOptions" :key="bill.arBillId" :label="billOptionLabel(bill)" :value="bill.arBillId" /></el-select></el-form-item><el-form-item label="核销金额"><el-input v-model="item.amount" type="number" /></el-form-item><el-button v-if="writeoffForm.allocations.length > 1" link type="danger" icon="Delete" @click="removeAllocation(index)" /></div><div v-if="writeoffOpen && !billLoading && !billOptions.length" class="bill-empty">当前客户暂无可核销账单</div><el-button link type="primary" icon="Plus" @click="addAllocation">添加账单</el-button><el-form-item label="核销日期"><el-date-picker v-model="writeoffForm.writeoffDate" type="date" value-format="YYYY-MM-DD" /></el-form-item><el-form-item label="备注"><el-input v-model="writeoffForm.remark" /></el-form-item></el-form>
|
|
||||||
<template #footer><el-button type="primary" :loading="writeoffLoading" @click="submitWriteoff">确认核销</el-button><el-button @click="writeoffOpen = false">取消</el-button></template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<el-dialog v-model="historyOpen" title="核销记录" width="820px" append-to-body>
|
|
||||||
<el-table :data="historyRows" border size="small" v-loading="historyLoading">
|
|
||||||
<el-table-column prop="writeoffNo" label="核销单号" min-width="180" />
|
|
||||||
<el-table-column prop="billNo" label="账单号" min-width="180" show-overflow-tooltip />
|
|
||||||
<el-table-column prop="writeoffAmount" label="核销金额" width="120" align="right"><template #default="{ row }">{{ money(row.writeoffAmount) }}</template></el-table-column>
|
|
||||||
<el-table-column prop="writeoffDate" label="核销日期" width="120" />
|
|
||||||
<el-table-column prop="status" label="状态" width="100"><template #default="{ row }"><el-tag :type="row.status === 'EFFECTIVE' ? 'success' : 'info'">{{ row.status === 'EFFECTIVE' ? '有效' : '已取消' }}</el-tag></template></el-table-column>
|
|
||||||
<el-table-column label="操作" width="90"><template #default="{ row }"><el-button v-if="row.status === 'EFFECTIVE'" link type="danger" v-hasPermi="['ar:receipt:cancel']" @click="cancelWriteoff(row)">取消</el-button></template></el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup name="ArReceipt">
|
|
||||||
import { listArReceipt, addArReceipt, cancelArReceipt, listArWriteoffs, writeoffArReceipt, cancelArWriteoff } from '@/api/ar/receipt'
|
|
||||||
import { listArBill } from '@/api/ar/bill'
|
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance()
|
|
||||||
const tableRef = ref()
|
|
||||||
const registerOpen = ref(false), registerLoading = ref(false), registerFormRef = ref()
|
|
||||||
const writeoffOpen = ref(false), writeoffLoading = ref(false)
|
|
||||||
const billLoading = ref(false), billOptions = ref([])
|
|
||||||
const historyOpen = ref(false), historyLoading = ref(false), historyRows = ref([]), historyReceiptId = ref(null)
|
|
||||||
const today = () => proxy.parseTime(new Date(), '{y}-{m}-{d}')
|
|
||||||
const registerForm = reactive({ customerId: '', receiptDate: today(), receiptAmount: '', currency: 'CNY', paymentMethod: 'BANK', bankReference: '', receiptNo: '', remark: '' })
|
|
||||||
const registerRules = { customerId: [{ required: true, message: '请选择客户', trigger: 'change' }], receiptDate: [{ required: true, message: '到账日期不能为空', trigger: 'change' }], receiptAmount: [{ required: true, message: '到账金额不能为空', trigger: 'blur' }] }
|
|
||||||
const writeoffForm = reactive({ receiptId: null, receiptNo: '', currency: '', unallocatedAmount: 0, writeoffDate: today(), remark: '', allocations: [{ arBillId: '', amount: '' }] })
|
|
||||||
const columns = [
|
|
||||||
{ label: '收款单号', prop: 'receiptNo', minWidth: 190, showOverflowTooltip: true }, { label: '客户', prop: 'customerName', minWidth: 180, showOverflowTooltip: true, formatter: row => customerText(row) }, { label: '到账日期', prop: 'receiptDate', width: 120 },
|
|
||||||
{ label: '到账金额', prop: 'receiptAmount', width: 135, align: 'right', slot: 'receiptAmount' }, { label: '未分配', prop: 'unallocatedAmount', width: 120, align: 'right', slot: 'unallocatedAmount' },
|
|
||||||
{ label: '收款方式', prop: 'paymentMethod', width: 110, formatter: row => methodText(row.paymentMethod) }, { label: '流水号', prop: 'bankReference', minWidth: 160, showOverflowTooltip: true },
|
|
||||||
{ label: '状态', prop: 'status', width: 120, slot: 'status' }, { label: '登记人', prop: 'createBy', width: 100 }, { label: '操作', prop: 'action', width: 150, fixed: 'right', slot: 'action', showInToolbar: false }
|
|
||||||
]
|
|
||||||
const searchColumns = [
|
|
||||||
{ label: '客户', prop: 'customerId', component: 'input', width: '160px' }, { label: '收款单号', prop: 'receiptNo', component: 'input', width: '180px' },
|
|
||||||
{ label: '状态', prop: 'status', component: 'select', options: [{ label: '已登记', value: 'REGISTERED' }, { label: '部分核销', value: 'PARTIAL_WRITTEN_OFF' }, { label: '已核销', value: 'WRITTEN_OFF' }, { label: '已取消', value: 'CANCELLED' }], width: '140px' },
|
|
||||||
{ label: '到账日期', prop: 'dateRange', component: 'daterange', width: '280px' }
|
|
||||||
]
|
|
||||||
function queryList(params) { const query = { ...params }; if (query.dateRange?.length === 2) { query.receiptDateStart = query.dateRange[0]; query.receiptDateEnd = query.dateRange[1] }; delete query.dateRange; return listArReceipt(query) }
|
|
||||||
function openRegister() { Object.assign(registerForm, { customerId: '', receiptDate: today(), receiptAmount: '', currency: 'CNY', paymentMethod: 'BANK', bankReference: '', receiptNo: '', remark: '' }); registerOpen.value = true; nextTick(() => registerFormRef.value?.clearValidate()) }
|
|
||||||
function submitRegister() { registerFormRef.value.validate(valid => { if (!valid) return; registerLoading.value = true; const data = { ...registerForm, customerId: Number(registerForm.customerId), receiptAmount: String(registerForm.receiptAmount) }; addArReceipt(data).then(() => { proxy.$modal.msgSuccess('收款登记成功'); registerOpen.value = false; tableRef.value.refresh() }).finally(() => { registerLoading.value = false }) }) }
|
|
||||||
function cancelReceipt(row) { proxy.$modal.confirm(`确认取消收款「${row.receiptNo}」?`).then(() => cancelArReceipt(row.receiptId, { remark: '页面取消收款' })).then(() => { proxy.$modal.msgSuccess('收款已取消'); tableRef.value.refresh() }).catch(() => {}) }
|
|
||||||
function openWriteoff(row) { Object.assign(writeoffForm, { receiptId: row.receiptId, receiptNo: row.receiptNo, currency: row.currency, unallocatedAmount: row.unallocatedAmount, writeoffDate: today(), remark: '', allocations: [{ arBillId: '', amount: '' }] }); billOptions.value = []; writeoffOpen.value = true; loadBillOptions(row) }
|
|
||||||
function loadBillOptions(row) { billLoading.value = true; return listArBill({ pageNum: 1, pageSize: 1000, customerId: row.customerId, currency: row.currency }).then(res => { billOptions.value = (res.rows || []).filter(bill => ['CONFIRMED', 'PARTIAL_WRITTEN_OFF'].includes(bill.status) && Number(bill.unwrittenAmount) > 0) }).finally(() => { billLoading.value = false }) }
|
|
||||||
function billOptionLabel(bill) { return `${bill.billNo || '未命名账单'}(未核销:${money(bill.unwrittenAmount)})` }
|
|
||||||
function addAllocation() { writeoffForm.allocations.push({ arBillId: '', amount: '' }) }
|
|
||||||
function removeAllocation(index) { writeoffForm.allocations.splice(index, 1) }
|
|
||||||
function submitWriteoff() { const allocations = writeoffForm.allocations.filter(item => item.arBillId && item.amount).map(item => ({ arBillId: Number(item.arBillId), amount: String(item.amount) })); if (!allocations.length) { proxy.$modal.msgError('请至少填写一条账单核销金额'); return }; writeoffLoading.value = true; writeoffArReceipt({ receiptId: writeoffForm.receiptId, currency: writeoffForm.currency, writeoffDate: writeoffForm.writeoffDate, remark: writeoffForm.remark, allocations }).then(() => { proxy.$modal.msgSuccess('核销成功'); writeoffOpen.value = false; tableRef.value.refresh(); if (historyOpen.value) openWriteoffHistory({ receiptId: writeoffForm.receiptId }) }).finally(() => { writeoffLoading.value = false }) }
|
|
||||||
function openWriteoffHistory(row) { historyReceiptId.value = row.receiptId; historyOpen.value = true; historyLoading.value = true; listArWriteoffs(row.receiptId).then(res => { historyRows.value = res.data || [] }).finally(() => { historyLoading.value = false }) }
|
|
||||||
function cancelWriteoff(row) { proxy.$modal.confirm(`确认取消核销「${row.writeoffNo}」?`).then(() => cancelArWriteoff(row.writeoffId, { remark: '页面取消核销' })).then(() => { proxy.$modal.msgSuccess('核销已取消'); openWriteoffHistory({ receiptId: historyReceiptId.value }); tableRef.value.refresh() }).catch(() => {}) }
|
|
||||||
function money(value) { const num = Number(value); return Number.isNaN(num) ? '-' : num.toFixed(2) }
|
|
||||||
function customerText(row) { return row.customerName ? `${row.customerName}${row.customerCode ? `(${row.customerCode})` : ''}` : row.customerCode || '-' }
|
|
||||||
function methodText(value) { return { BANK: '银行转账', ALIPAY: '支付宝', WECHAT: '微信', CASH: '现金' }[value] || value || '-' }
|
|
||||||
function receiptStatusText(value) { return { REGISTERED: '已登记', PARTIAL_WRITTEN_OFF: '部分核销', WRITTEN_OFF: '已核销', CANCELLED: '已取消' }[value] || value || '-' }
|
|
||||||
function receiptStatusType(value) { return { REGISTERED: 'info', PARTIAL_WRITTEN_OFF: 'warning', WRITTEN_OFF: 'success', CANCELLED: 'danger' }[value] || 'info' }
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.ar-receipt-page .income { color: #67c23a; font-weight: 600; }
|
|
||||||
.ar-receipt-page .pending { color: #e6a23c; font-weight: 600; }
|
|
||||||
.writeoff-summary { margin-bottom: 18px; }
|
|
||||||
.allocation-row { display: flex; align-items: center; gap: 10px; }
|
|
||||||
.allocation-row .el-form-item { flex: 1; }
|
|
||||||
.bill-select { width: 100%; }
|
|
||||||
.bill-empty { margin: -4px 0 12px 90px; color: var(--el-text-color-secondary); font-size: 13px; }
|
|
||||||
</style>
|
|
||||||
@@ -1,301 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="app-container">
|
|
||||||
<!-- 查询条件 -->
|
|
||||||
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
|
|
||||||
<el-form-item label="账单号" prop="billNo">
|
|
||||||
<el-input v-model="queryParams.billNo" placeholder="请输入账单号" clearable style="width: 180px" @keyup.enter="handleQuery" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="账单名称" prop="billName">
|
|
||||||
<el-input v-model="queryParams.billName" placeholder="请输入账单名称" clearable style="width: 180px" @keyup.enter="handleQuery" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="客户名称" prop="customerName">
|
|
||||||
<el-input v-model="queryParams.customerName" placeholder="请输入客户名称" clearable style="width: 180px" @keyup.enter="handleQuery" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="状态" prop="status">
|
|
||||||
<el-select v-model="queryParams.status" placeholder="全部状态" clearable style="width: 140px">
|
|
||||||
<el-option v-for="opt in statusOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="币种" prop="currency">
|
|
||||||
<el-select v-model="queryParams.currency" placeholder="全部币种" clearable style="width: 120px">
|
|
||||||
<el-option v-for="c in currencyOptions" :key="c" :label="c" :value="c" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="账期">
|
|
||||||
<el-date-picker
|
|
||||||
v-model="dateRange"
|
|
||||||
value-format="YYYY-MM-DD"
|
|
||||||
type="daterange"
|
|
||||||
range-separator="-"
|
|
||||||
start-placeholder="开始"
|
|
||||||
end-placeholder="结束"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item>
|
|
||||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
|
||||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
<el-row :gutter="10" class="mb8">
|
|
||||||
<el-col :span="1.5">
|
|
||||||
<el-button icon="Search" @click="toggleSearch">{{ showSearch ? '收起' : '展开' }}</el-button>
|
|
||||||
</el-col>
|
|
||||||
<right-toolbar v-model:showSearch="showSearch" />
|
|
||||||
</el-row>
|
|
||||||
|
|
||||||
<el-table v-loading="loading" :data="billList" border>
|
|
||||||
<el-table-column label="账单号" align="center" prop="billNo" min-width="170" show-overflow-tooltip />
|
|
||||||
<el-table-column label="账单名称" align="center" prop="billName" min-width="150" show-overflow-tooltip />
|
|
||||||
<el-table-column label="客户" align="center" prop="customerName" min-width="150" show-overflow-tooltip />
|
|
||||||
<el-table-column label="账期开始" align="center" prop="billPeriodStart" width="110">
|
|
||||||
<template #default="scope">{{ fmtDate(scope.row.billPeriodStart) }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="账期结束" align="center" prop="billPeriodEnd" width="110">
|
|
||||||
<template #default="scope">{{ fmtDate(scope.row.billPeriodEnd) }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="账单日期" align="center" prop="billDate" width="110">
|
|
||||||
<template #default="scope">{{ fmtDate(scope.row.billDate) }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="订单数" align="center" prop="orderCount" width="80" />
|
|
||||||
<el-table-column label="应收金额" align="right" width="130">
|
|
||||||
<template #default="scope">
|
|
||||||
<span class="amount">{{ scope.row.currency }} {{ fmtMoney(scope.row.totalAmount) }}</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="已核销" align="right" width="120">
|
|
||||||
<template #default="scope"><span class="written">{{ fmtMoney(scope.row.writtenOffAmount) }}</span></template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="未核销" align="right" width="120">
|
|
||||||
<template #default="scope"><span class="unwritten">{{ fmtMoney(scope.row.unwrittenAmount) }}</span></template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="状态" align="center" width="100">
|
|
||||||
<template #default="scope">
|
|
||||||
<el-tag :type="statusTag(scope.row.status)">{{ statusLabel(scope.row.status) }}</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" align="center" width="80" fixed="right">
|
|
||||||
<template #default="scope">
|
|
||||||
<el-button link type="primary" icon="View" @click="handleView(scope.row)"
|
|
||||||
v-hasPermi="['customer:bill:query']">详情</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
|
|
||||||
<pagination
|
|
||||||
v-show="total > 0"
|
|
||||||
:total="total"
|
|
||||||
v-model:page="queryParams.pageNum"
|
|
||||||
v-model:limit="queryParams.pageSize"
|
|
||||||
@pagination="getList"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 账单详情抽屉(只读) -->
|
|
||||||
<el-drawer v-model="detailOpen" size="72%" :title="drawerTitle" destroy-on-close>
|
|
||||||
<div v-loading="detailLoading" class="bill-detail">
|
|
||||||
<template v-if="detail">
|
|
||||||
<el-descriptions :column="3" border size="small">
|
|
||||||
<el-descriptions-item label="账单号">{{ detail.billNo }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="账单名称" :span="2">{{ detail.billName || '-' }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="客户">{{ detail.customerName }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="客户编码">{{ detail.customerCode || '-' }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="状态">
|
|
||||||
<el-tag :type="statusTag(detail.status)">{{ statusLabel(detail.status) }}</el-tag>
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="账期开始">{{ fmtDate(detail.billPeriodStart) }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="账期结束">{{ fmtDate(detail.billPeriodEnd) }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="账单日期">{{ fmtDate(detail.billDate) }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="币种">{{ detail.currency }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="订单数">{{ detail.orderCount }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="应收金额">
|
|
||||||
<span class="amount">{{ fmtMoney(detail.totalAmount) }}</span>
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="已核销金额">
|
|
||||||
<span class="written">{{ fmtMoney(detail.writtenOffAmount) }}</span>
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="未核销金额">
|
|
||||||
<span class="unwritten">{{ fmtMoney(detail.unwrittenAmount) }}</span>
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="确认人">{{ detail.confirmedBy || '-' }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="确认时间">{{ fmtDateTime(detail.confirmedTime) }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="收款银行">
|
|
||||||
{{ [detail.bankName, detail.bankAccountName].filter(Boolean).join(' / ') || '-' }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="备注" :span="3">{{ detail.remark || '-' }}</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
|
|
||||||
<div class="detail-title">订单明细({{ detail.details?.length || 0 }} 单)</div>
|
|
||||||
<el-table :data="detail.details || []" border size="small" row-key="detailId">
|
|
||||||
<el-table-column type="expand">
|
|
||||||
<template #default="props">
|
|
||||||
<div class="fee-wrap">
|
|
||||||
<div class="fee-title">费用明细</div>
|
|
||||||
<el-table :data="props.row.fees || []" border size="small">
|
|
||||||
<el-table-column label="序号" type="index" width="55" align="center" />
|
|
||||||
<el-table-column label="费用名称" prop="feeName" min-width="120" show-overflow-tooltip />
|
|
||||||
<el-table-column label="数量" prop="quantity" width="90" align="right" />
|
|
||||||
<el-table-column label="单位" prop="unit" width="70" align="center" />
|
|
||||||
<el-table-column label="单价" prop="unitPrice" width="100" align="right" />
|
|
||||||
<el-table-column label="金额" prop="amount" width="110" align="right">
|
|
||||||
<template #default="f">{{ f.row.currency }} {{ fmtMoney(f.row.amount) }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="备注" prop="remark" min-width="120" show-overflow-tooltip />
|
|
||||||
<template #empty><span class="empty-tip">无费用明细</span></template>
|
|
||||||
</el-table>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="序号" prop="detailNo" width="60" align="center" />
|
|
||||||
<el-table-column label="订单号" prop="orderNo" min-width="140" show-overflow-tooltip />
|
|
||||||
<el-table-column label="跟踪号" prop="trackingNo" min-width="140" show-overflow-tooltip />
|
|
||||||
<el-table-column label="产品" prop="productName" min-width="120" show-overflow-tooltip />
|
|
||||||
<el-table-column label="目的国" prop="countryName" width="100" align="center" />
|
|
||||||
<el-table-column label="实重" prop="actualWeight" width="80" align="right" />
|
|
||||||
<el-table-column label="体积重" prop="volumeWeight" width="80" align="right" />
|
|
||||||
<el-table-column label="计费重" prop="chargeWeight" width="80" align="right" />
|
|
||||||
<el-table-column label="金额" prop="totalAmount" width="120" align="right">
|
|
||||||
<template #default="scope">{{ scope.row.currency }} {{ fmtMoney(scope.row.totalAmount) }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</el-drawer>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup name="CustomerBill">
|
|
||||||
import { listCustomerBill, getCustomerBill } from '@/api/customersuppliermanagement/customermoney/customerbill'
|
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance()
|
|
||||||
const route = useRoute()
|
|
||||||
|
|
||||||
const statusOptions = [
|
|
||||||
{ value: 'DRAFT', label: '草稿' },
|
|
||||||
{ value: 'CONFIRMED', label: '已确认' },
|
|
||||||
{ value: 'PARTIAL_WRITTEN_OFF', label: '部分核销' },
|
|
||||||
{ value: 'WRITTEN_OFF', label: '已核销' },
|
|
||||||
{ value: 'VOID', label: '已作废' }
|
|
||||||
]
|
|
||||||
const statusTagMap = { DRAFT: 'info', CONFIRMED: 'warning', PARTIAL_WRITTEN_OFF: 'primary', WRITTEN_OFF: 'success', VOID: 'danger' }
|
|
||||||
const currencyOptions = ['CNY', 'USD', 'EUR', 'GBP', 'HKD', 'JPY']
|
|
||||||
|
|
||||||
const billList = ref([])
|
|
||||||
const loading = ref(false)
|
|
||||||
const total = ref(0)
|
|
||||||
const showSearch = ref(true)
|
|
||||||
const dateRange = ref([])
|
|
||||||
const queryParams = reactive({
|
|
||||||
pageNum: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
billNo: '',
|
|
||||||
billName: '',
|
|
||||||
customerName: '',
|
|
||||||
customerId: route.query.customerId || undefined,
|
|
||||||
status: '',
|
|
||||||
currency: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const detailOpen = ref(false)
|
|
||||||
const detailLoading = ref(false)
|
|
||||||
const detail = ref(null)
|
|
||||||
const drawerTitle = computed(() => detail.value ? `账单详情 - ${detail.value.billNo}` : '账单详情')
|
|
||||||
|
|
||||||
function statusLabel(v) {
|
|
||||||
return statusOptions.find(o => o.value === v)?.label || v || '-'
|
|
||||||
}
|
|
||||||
function statusTag(v) {
|
|
||||||
return statusTagMap[v] || 'info'
|
|
||||||
}
|
|
||||||
function fmtMoney(v) {
|
|
||||||
const n = Number(v)
|
|
||||||
return isNaN(n) ? '0.00' : n.toFixed(2)
|
|
||||||
}
|
|
||||||
function fmtDate(v) {
|
|
||||||
return v ? String(v).substring(0, 10) : '-'
|
|
||||||
}
|
|
||||||
function fmtDateTime(v) {
|
|
||||||
return v ? String(v).substring(0, 16) : '-'
|
|
||||||
}
|
|
||||||
|
|
||||||
function getList() {
|
|
||||||
loading.value = true
|
|
||||||
const params = proxy.addDateRange({ ...queryParams }, dateRange.value)
|
|
||||||
listCustomerBill(params).then(res => {
|
|
||||||
billList.value = res.rows || []
|
|
||||||
total.value = res.total || 0
|
|
||||||
}).finally(() => {
|
|
||||||
loading.value = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleQuery() {
|
|
||||||
queryParams.pageNum = 1
|
|
||||||
getList()
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetQuery() {
|
|
||||||
dateRange.value = []
|
|
||||||
proxy.resetForm('queryRef')
|
|
||||||
// 从客户余额页跳入时保留客户过滤
|
|
||||||
queryParams.customerId = route.query.customerId || undefined
|
|
||||||
handleQuery()
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleSearch() {
|
|
||||||
showSearch.value = !showSearch.value
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleView(row) {
|
|
||||||
detail.value = null
|
|
||||||
detailOpen.value = true
|
|
||||||
detailLoading.value = true
|
|
||||||
getCustomerBill(row.arBillId).then(res => {
|
|
||||||
detail.value = res.data || null
|
|
||||||
}).finally(() => {
|
|
||||||
detailLoading.value = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
getList()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.amount {
|
|
||||||
color: var(--el-color-danger);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.written {
|
|
||||||
color: var(--el-color-success);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.unwritten {
|
|
||||||
color: var(--el-color-warning);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.bill-detail {
|
|
||||||
padding: 0 16px 16px;
|
|
||||||
}
|
|
||||||
.detail-title {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 14px;
|
|
||||||
margin: 16px 0 8px;
|
|
||||||
padding-left: 8px;
|
|
||||||
border-left: 3px solid var(--el-color-primary);
|
|
||||||
}
|
|
||||||
.fee-wrap {
|
|
||||||
padding: 8px 16px;
|
|
||||||
background: var(--el-fill-color-light);
|
|
||||||
}
|
|
||||||
.fee-title {
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
.empty-tip {
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
.mb8 {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
+204
-15
@@ -14,14 +14,14 @@
|
|||||||
<el-button
|
<el-button
|
||||||
icon="Document"
|
icon="Document"
|
||||||
@click="goBillView"
|
@click="goBillView"
|
||||||
v-hasPermi="['customer:bill:list']"
|
v-hasPermi="['ar:bill:list']"
|
||||||
>查看账单</el-button>
|
>查看账单</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon="Money"
|
icon="Money"
|
||||||
@click="openAdjustDialog()"
|
@click="openAdjustDialog()"
|
||||||
v-hasPermi="['customer:credit:adjust']"
|
v-hasPermi="['customer:credit:adjust']"
|
||||||
>额度调整</el-button>
|
>额度调整申请</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -339,6 +339,97 @@
|
|||||||
@pagination="getCreditFlowList"
|
@pagination="getCreditFlowList"
|
||||||
/>
|
/>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<!-- ====== 额度审批(信用/额外额度调整申请) ====== -->
|
||||||
|
<el-tab-pane label="额度审批" name="creditApply">
|
||||||
|
<el-alert type="warning" :closable="false" class="credit-apply-tip">
|
||||||
|
信用额度 / 额外额度调整需先提交申请,审批通过后额度才会调整并写入额度流水。
|
||||||
|
</el-alert>
|
||||||
|
<el-form :model="applyQuery" :inline="true" class="search-form">
|
||||||
|
<el-form-item label="额度类型" prop="creditType">
|
||||||
|
<el-select v-model="applyQuery.creditType" placeholder="全部" clearable style="width: 130px;">
|
||||||
|
<el-option v-for="opt in creditTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-select v-model="applyQuery.status" placeholder="全部" clearable style="width: 130px;">
|
||||||
|
<el-option v-for="opt in applyStatusOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="Search" @click="getApplyList">搜索</el-button>
|
||||||
|
<el-button icon="Refresh" @click="resetApplyQuery">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-table :data="applyList" border v-loading="applyLoading">
|
||||||
|
<el-table-column label="申请单号" align="center" prop="applyNo" width="180" />
|
||||||
|
<el-table-column label="额度类型" align="center" prop="creditType" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag :type="scope.row.creditType === 'TEMP' ? 'warning' : ''">
|
||||||
|
{{ formatOption(creditTypeOptions, scope.row.creditType) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="调整方式" align="center" prop="adjustMode" width="90">
|
||||||
|
<template #default="scope">{{ formatOption(adjustModeOptions, scope.row.adjustMode) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="调整金额" align="center" prop="changeAmount" width="110">
|
||||||
|
<template #default="scope">{{ formatMoney(scope.row.changeAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="生效日期" align="center" prop="effectiveDate" width="100">
|
||||||
|
<template #default="scope">{{ scope.row.effectiveDate || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="失效日期" align="center" prop="expireDate" width="100">
|
||||||
|
<template #default="scope">{{ scope.row.expireDate || '长期' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="调整原因" align="center" prop="reason" min-width="150" show-overflow-tooltip />
|
||||||
|
<el-table-column label="状态" align="center" prop="status" width="90">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag :type="applyStatusTag(scope.row.status)" size="small">
|
||||||
|
{{ formatOption(applyStatusOptions, scope.row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="申请人" align="center" prop="applyBy" width="100" />
|
||||||
|
<el-table-column label="申请时间" align="center" prop="applyTime" width="150">
|
||||||
|
<template #default="scope">{{ formatDateTime(scope.row.applyTime) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="审批人" align="center" prop="approveBy" width="100">
|
||||||
|
<template #default="scope">{{ scope.row.approveBy || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="审批时间" align="center" prop="approveTime" width="150">
|
||||||
|
<template #default="scope">{{ scope.row.approveTime ? formatDateTime(scope.row.approveTime) : '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="审批意见" align="center" prop="approveRemark" min-width="140" show-overflow-tooltip>
|
||||||
|
<template #default="scope">{{ scope.row.approveRemark || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" align="center" width="150" fixed="right">
|
||||||
|
<template #default="scope">
|
||||||
|
<template v-if="scope.row.status === 'PENDING'">
|
||||||
|
<el-button link type="success" size="small"
|
||||||
|
@click="handleApproveApply(scope.row)"
|
||||||
|
v-hasPermi="['customer:credit:approve']">通过</el-button>
|
||||||
|
<el-button link type="danger" size="small"
|
||||||
|
@click="handleRejectApply(scope.row)"
|
||||||
|
v-hasPermi="['customer:credit:approve']">驳回</el-button>
|
||||||
|
<el-button link type="warning" size="small"
|
||||||
|
@click="handleCancelApply(scope.row)"
|
||||||
|
v-hasPermi="['customer:credit:adjust']">撤销</el-button>
|
||||||
|
</template>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<pagination
|
||||||
|
v-show="applyTotal > 0"
|
||||||
|
:total="applyTotal"
|
||||||
|
v-model:page="applyQuery.pageNum"
|
||||||
|
v-model:limit="applyQuery.pageSize"
|
||||||
|
@pagination="getApplyList"
|
||||||
|
/>
|
||||||
|
</el-tab-pane>
|
||||||
<!-- ====== 账单核销(用账户余额核销应收账单) ====== -->
|
<!-- ====== 账单核销(用账户余额核销应收账单) ====== -->
|
||||||
<el-tab-pane label="账单核销" name="billWriteoff">
|
<el-tab-pane label="账单核销" name="billWriteoff">
|
||||||
<el-alert type="info" :closable="false" class="writeoff-summary">
|
<el-alert type="info" :closable="false" class="writeoff-summary">
|
||||||
@@ -348,7 +439,7 @@
|
|||||||
<span>账户余额:<b class="money-in">CNY {{ formatMoney(currentAccount.balance) }}</b></span>
|
<span>账户余额:<b class="money-in">CNY {{ formatMoney(currentAccount.balance) }}</b></span>
|
||||||
<el-button type="primary" link size="small" icon="Refresh" @click="loadBillWriteoffData">刷新</el-button>
|
<el-button type="primary" link size="small" icon="Refresh" @click="loadBillWriteoffData">刷新</el-button>
|
||||||
<el-button type="primary" plain size="small" @click="goBillView"
|
<el-button type="primary" plain size="small" @click="goBillView"
|
||||||
v-hasPermi="['customer:bill:list']">查看全部账单</el-button>
|
v-hasPermi="['ar:bill:list']">查看全部账单</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-alert>
|
</el-alert>
|
||||||
|
|
||||||
@@ -621,9 +712,9 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- ====== 额度调整弹窗 ====== -->
|
<!-- ====== 额度调整申请弹窗 ====== -->
|
||||||
<el-dialog
|
<el-dialog
|
||||||
:title="creditDialog.form.creditType === 'TEMP' ? '临时额度调整' : '信用额度调整'"
|
:title="(creditDialog.form.creditType === 'TEMP' ? '额外额度调整申请' : '信用额度调整申请') + '(提交后需审批)'"
|
||||||
v-model="creditDialog.visible"
|
v-model="creditDialog.visible"
|
||||||
width="600px"
|
width="600px"
|
||||||
append-to-body
|
append-to-body
|
||||||
@@ -724,7 +815,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button type="primary" :loading="creditSubmitLoading" @click="submitAdjustCredit">确 定</el-button>
|
<el-button type="primary" :loading="creditSubmitLoading" @click="submitAdjustCredit">提交申请</el-button>
|
||||||
<el-button @click="creditDialog.visible = false">取 消</el-button>
|
<el-button @click="creditDialog.visible = false">取 消</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@@ -804,7 +895,7 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { saveAs } from 'file-saver'
|
import { saveAs } from 'file-saver'
|
||||||
import { getCustomer } from "@/api/customersuppliermanagement/customerinfo/customer.js"
|
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 { listUnsettledBill, listBalanceWriteoff, balanceWriteoff, cancelBalanceWriteoff } from "@/api/customersuppliermanagement/customermoney/customerbill.js"
|
||||||
import { convertToBase } from "@/api/data/exchangeRate"
|
import { convertToBase } from "@/api/data/exchangeRate"
|
||||||
import { getToken } from "@/utils/auth"
|
import { getToken } from "@/utils/auth"
|
||||||
@@ -881,7 +972,9 @@ const creditTypeOptions = [
|
|||||||
const creditChangeTypeOptions = [
|
const creditChangeTypeOptions = [
|
||||||
{ value: 'INIT', label: '初始化' },
|
{ value: 'INIT', label: '初始化' },
|
||||||
{ value: 'ADJUST', label: '手工调整' },
|
{ value: 'ADJUST', label: '手工调整' },
|
||||||
{ value: 'EXPIRE', label: '到期失效' }
|
{ value: 'EXPIRE', label: '到期失效' },
|
||||||
|
{ value: 'OCCUPY', label: '授信占用' },
|
||||||
|
{ value: 'RELEASE', label: '授信释放' }
|
||||||
]
|
]
|
||||||
const adjustModeOptions = [
|
const adjustModeOptions = [
|
||||||
{ value: 'SET', label: '设置为' },
|
{ value: 'SET', label: '设置为' },
|
||||||
@@ -889,7 +982,7 @@ const adjustModeOptions = [
|
|||||||
{ value: 'SUB', label: '减少' }
|
{ value: 'SUB', label: '减少' }
|
||||||
]
|
]
|
||||||
function creditChangeTag(v) {
|
function creditChangeTag(v) {
|
||||||
return { INIT: '', ADJUST: 'primary', EXPIRE: 'danger' }[v] || 'info'
|
return { INIT: '', ADJUST: 'primary', EXPIRE: 'danger', OCCUPY: 'warning', RELEASE: 'success' }[v] || 'info'
|
||||||
}
|
}
|
||||||
function formatMoney(val) {
|
function formatMoney(val) {
|
||||||
const num = Number(val)
|
const num = Number(val)
|
||||||
@@ -1056,12 +1149,12 @@ function submitAdjustCredit() {
|
|||||||
payload.expireDate = f.expireDate || null
|
payload.expireDate = f.expireDate || null
|
||||||
}
|
}
|
||||||
creditSubmitLoading.value = true
|
creditSubmitLoading.value = true
|
||||||
adjustCredit(payload).then(() => {
|
applyCreditAdjust(payload).then(() => {
|
||||||
proxy.$modal.msgSuccess('额度调整成功')
|
proxy.$modal.msgSuccess('申请已提交,等待审批')
|
||||||
creditDialog.visible = false
|
creditDialog.visible = false
|
||||||
loadCustomer()
|
if (creditApplyLoaded) {
|
||||||
loadAccounts()
|
getApplyList()
|
||||||
getCreditFlowList()
|
}
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
creditSubmitLoading.value = false
|
creditSubmitLoading.value = false
|
||||||
})
|
})
|
||||||
@@ -1346,6 +1439,88 @@ function resetCreditFlowQuery() {
|
|||||||
getCreditFlowList()
|
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 = {
|
const billStatusMap = {
|
||||||
DRAFT: { label: '草稿', type: 'info' },
|
DRAFT: { label: '草稿', type: 'info' },
|
||||||
@@ -1366,7 +1541,7 @@ function formatDate(val) {
|
|||||||
}
|
}
|
||||||
function goBillView() {
|
function goBillView() {
|
||||||
router.push({
|
router.push({
|
||||||
path: '/customersuppliermanagement/customerbill',
|
path: '/ar/bill',
|
||||||
query: { customerId: route.query.customerId || '' }
|
query: { customerId: route.query.customerId || '' }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1510,6 +1685,10 @@ function submitCancelWriteoff() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watch(activeTab, (tab) => {
|
watch(activeTab, (tab) => {
|
||||||
|
if (tab === 'creditApply' && !creditApplyLoaded) {
|
||||||
|
creditApplyLoaded = true
|
||||||
|
getApplyList()
|
||||||
|
}
|
||||||
if (tab === 'billWriteoff' && !billWriteoffLoaded) {
|
if (tab === 'billWriteoff' && !billWriteoffLoaded) {
|
||||||
billWriteoffLoaded = true
|
billWriteoffLoaded = true
|
||||||
loadBillWriteoffData()
|
loadBillWriteoffData()
|
||||||
@@ -1632,6 +1811,13 @@ function initPage() {
|
|||||||
flowTotal.value = 0
|
flowTotal.value = 0
|
||||||
creditFlowList.value = []
|
creditFlowList.value = []
|
||||||
creditFlowTotal.value = 0
|
creditFlowTotal.value = 0
|
||||||
|
applyList.value = []
|
||||||
|
applyTotal.value = 0
|
||||||
|
applyQuery.customerId = customerId
|
||||||
|
applyQuery.creditType = ''
|
||||||
|
applyQuery.status = ''
|
||||||
|
applyQuery.pageNum = 1
|
||||||
|
creditApplyLoaded = false
|
||||||
unsettledList.value = []
|
unsettledList.value = []
|
||||||
writeoffList.value = []
|
writeoffList.value = []
|
||||||
writeoffTotal.value = 0
|
writeoffTotal.value = 0
|
||||||
@@ -1716,6 +1902,9 @@ watch(() => route.query.customerId, (newId, oldId) => {
|
|||||||
.credit-tip {
|
.credit-tip {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
.credit-apply-tip {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
.credit-preview {
|
.credit-preview {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
|
|||||||
@@ -124,7 +124,11 @@
|
|||||||
<el-table-column label="客户名称" align="center" prop="customerName" />
|
<el-table-column label="客户名称" align="center" prop="customerName" />
|
||||||
<el-table-column label="对外代码" align="center" prop="customerCodeDw" />
|
<el-table-column label="对外代码" align="center" prop="customerCodeDw" />
|
||||||
<el-table-column label="统一社会信用代码" align="center" prop="creditCode" />
|
<el-table-column label="统一社会信用代码" align="center" prop="creditCode" />
|
||||||
<el-table-column label="业务类型" align="center" prop="customerType" />
|
<el-table-column 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 label="启用状态" align="center" prop="isEnable">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<dict-tag :options="sys_normal_disable" :value="scope.row.isEnable" />
|
<dict-tag :options="sys_normal_disable" :value="scope.row.isEnable" />
|
||||||
@@ -154,7 +158,14 @@
|
|||||||
<el-table-column label="重量阈值" align="center" prop="weightThreshold" />
|
<el-table-column label="重量阈值" align="center" prop="weightThreshold" />
|
||||||
<el-table-column label="抛比" align="center" prop="throwRatio" />
|
<el-table-column label="抛比" align="center" prop="throwRatio" />
|
||||||
<el-table-column label="信用额度" align="center" prop="creditEd" />
|
<el-table-column label="信用额度" align="center" prop="creditEd" />
|
||||||
<el-table-column label="绑定产品ID" align="center" prop="productIds" />
|
<el-table-column 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) }}
|
||||||
|
</span>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="备注说明" align="center" prop="remark" />
|
<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 label="操作" align="center" class-name="small-padding fixed-width" width="240">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
@@ -178,6 +189,7 @@
|
|||||||
|
|
||||||
<script setup name="Customer">
|
<script setup name="Customer">
|
||||||
import { listCustomer, delCustomer } from "@/api/customersuppliermanagement/customerinfo/customer.js"
|
import { listCustomer, delCustomer } from "@/api/customersuppliermanagement/customerinfo/customer.js"
|
||||||
|
import { useProducts, parseProductIds } from '@/components/ProductSelect/useProducts'
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance()
|
const { proxy } = getCurrentInstance()
|
||||||
|
|
||||||
@@ -189,7 +201,14 @@ const single = ref(true)
|
|||||||
const multiple = ref(true)
|
const multiple = ref(true)
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
|
|
||||||
const { sys_normal_disable, settlement_type } = useDict("sys_normal_disable", "settlement_type")
|
const { sys_normal_disable, settlement_type, customer_type } = useDict("sys_normal_disable", "settlement_type", "customer_type")
|
||||||
|
|
||||||
|
/** 绑定产品ID串 -> 产品名称展示(产品代码+名称;查不到的产品回显原ID) */
|
||||||
|
const { ensureProducts, getProductLabel } = useProducts()
|
||||||
|
function productNames(productIds) {
|
||||||
|
return parseProductIds(productIds).map(id => getProductLabel(id)).join(',')
|
||||||
|
}
|
||||||
|
ensureProducts()
|
||||||
|
|
||||||
/** 格式化余额(数据源 customer_account 余额表) */
|
/** 格式化余额(数据源 customer_account 余额表) */
|
||||||
function formatBalance(val) {
|
function formatBalance(val) {
|
||||||
|
|||||||
Reference in New Issue
Block a user