Merge branch 'master' of http://git.lionquexpress.com/wuweihua/BMS
This commit is contained in:
+2
-1
@@ -22,6 +22,7 @@ import com.ruoyi.ar.domain.ArBillLog;
|
||||
import com.ruoyi.ar.domain.vo.ArBillActionRequest;
|
||||
import com.ruoyi.ar.domain.vo.ArBillGenerateQuery;
|
||||
import com.ruoyi.ar.domain.vo.ArBillGenerateRequest;
|
||||
import com.ruoyi.ar.domain.vo.ArBillQuery;
|
||||
import com.ruoyi.ar.service.IArBillService;
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
@@ -43,7 +44,7 @@ public class ArBillController extends BaseController
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('ar:bill:list')")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(ArBill query)
|
||||
public TableDataInfo list(ArBillQuery query)
|
||||
{
|
||||
startPage();
|
||||
List<ArBill> list = billService.selectArBillList(query);
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package com.ruoyi.web.controller.customer.customerinfo;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
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.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.customer.customerinfo.domain.BmsCustomerPriceSet;
|
||||
import com.ruoyi.customer.customerinfo.domain.BmsPriceSetOption;
|
||||
import com.ruoyi.customer.customerinfo.service.IBmsCustomerPriceSetService;
|
||||
|
||||
/**
|
||||
* 客户套餐绑定Controller(客户管理-客户编辑-绑定套餐)
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-16
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/customersupplier/customerinfo/priceset")
|
||||
public class BmsCustomerPriceSetController extends BaseController
|
||||
{
|
||||
@Autowired
|
||||
private IBmsCustomerPriceSetService customerPriceSetService;
|
||||
|
||||
/**
|
||||
* 查询客户已绑定的套餐列表
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('customersupplier:customerinfo:list')")
|
||||
@GetMapping("/bind/list")
|
||||
public AjaxResult bindList(Long customerId)
|
||||
{
|
||||
List<BmsCustomerPriceSet> list = customerPriceSetService.selectBindList(customerId);
|
||||
return success(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询可绑定的生效套餐选项
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('customersupplier:customerinfo:list')")
|
||||
@GetMapping("/option/list")
|
||||
public AjaxResult optionList(BmsPriceSetOption query)
|
||||
{
|
||||
return success(customerPriceSetService.selectOptionList(query));
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存客户套餐绑定(已绑定则更新/更换套餐,未绑定则新增)
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('customersupplier:customerinfo:edit')")
|
||||
@Log(title = "客户套餐绑定", businessType = BusinessType.INSERT)
|
||||
@PostMapping("/bind")
|
||||
public AjaxResult saveBind(@RequestBody BmsCustomerPriceSet bind)
|
||||
{
|
||||
bind.setCreateBy(getUsername());
|
||||
bind.setUpdateBy(getUsername());
|
||||
return toAjax(customerPriceSetService.saveBind(bind));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解绑客户套餐
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('customersupplier:customerinfo:edit')")
|
||||
@Log(title = "客户套餐解绑", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/bind/{customerPricesetIds}")
|
||||
public AjaxResult removeBind(@PathVariable Long[] customerPricesetIds)
|
||||
{
|
||||
return toAjax(customerPriceSetService.deleteBindByIds(customerPricesetIds));
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package com.ruoyi.web.controller.customer.customermoney;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
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.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import com.ruoyi.ar.domain.ArBalanceWriteoff;
|
||||
import com.ruoyi.ar.service.IArBalanceWriteoffService;
|
||||
import com.ruoyi.ar.service.IArBillService;
|
||||
import com.ruoyi.ar.domain.vo.ArBillQuery;
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
|
||||
/**
|
||||
* 客户余额核销应收账单 / 账单只读查询 Controller
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-16
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/customersupplier/customerbill")
|
||||
public class CustomerBillController extends BaseController
|
||||
{
|
||||
@Autowired
|
||||
private IArBalanceWriteoffService balanceWriteoffService;
|
||||
|
||||
@Autowired
|
||||
private IArBillService billService;
|
||||
|
||||
/**
|
||||
* 查询客户可核销账单(已确认/部分核销,未核销额大于0)
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('customersupplier:customerinfo:list')")
|
||||
@GetMapping("/unsettled/{customerId}")
|
||||
public AjaxResult unsettled(@PathVariable("customerId") Long customerId)
|
||||
{
|
||||
return success(balanceWriteoffService.selectUnsettledBills(customerId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询余额核销记录(分页)
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('customersupplier:customerinfo:list')")
|
||||
@GetMapping("/writeoff/list")
|
||||
public TableDataInfo writeoffList(ArBalanceWriteoff query)
|
||||
{
|
||||
if (query == null || query.getCustomerId() == null || query.getCustomerId() == 0L)
|
||||
{
|
||||
return getDataTable(Collections.emptyList());
|
||||
}
|
||||
startPage();
|
||||
List<ArBalanceWriteoff> list = balanceWriteoffService.selectWriteoffList(query);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用客户账户余额核销账单
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('customer:bill:writeoff')")
|
||||
@Log(title = "余额核销账单", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/writeoff")
|
||||
public AjaxResult writeoff(@RequestBody ArBalanceWriteoff cmd)
|
||||
{
|
||||
cmd.setCreateBy(getUsername());
|
||||
return success(balanceWriteoffService.writeoff(cmd));
|
||||
}
|
||||
|
||||
/**
|
||||
* 反核销(取消一笔有效余额核销)
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('customer:bill:writeoff:cancel')")
|
||||
@Log(title = "余额核销反核销", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/writeoff/{writeoffId}/cancel")
|
||||
public AjaxResult cancelWriteoff(@PathVariable("writeoffId") Long writeoffId,
|
||||
@RequestBody(required = false) ArBalanceWriteoff body)
|
||||
{
|
||||
balanceWriteoffService.cancelWriteoff(writeoffId, body == null ? null : body.getCancelRemark());
|
||||
return success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 账单查看:分页查询应收账单(只读,供客户侧账单查看页使用)
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('customer:bill:list')")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo billList(ArBillQuery query)
|
||||
{
|
||||
startPage();
|
||||
return getDataTable(billService.selectArBillList(query));
|
||||
}
|
||||
|
||||
/**
|
||||
* 账单查看:账单详情(含订单明细/费用明细)
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('customer:bill:query')")
|
||||
@GetMapping("/{arBillId}")
|
||||
public AjaxResult billInfo(@PathVariable("arBillId") Long arBillId)
|
||||
{
|
||||
return success(billService.selectArBillById(arBillId));
|
||||
}
|
||||
}
|
||||
+29
@@ -17,6 +17,7 @@ import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.customer.customermoney.domain.CustomerBalanceFlow;
|
||||
import com.ruoyi.customer.customermoney.domain.CustomerCreditFlow;
|
||||
import com.ruoyi.customer.customermoney.domain.CustomerReceipt;
|
||||
import com.ruoyi.customer.customermoney.service.ICustomerMoneyService;
|
||||
|
||||
@@ -118,4 +119,32 @@ public class CustomerMoneyController extends BaseController
|
||||
List<CustomerBalanceFlow> list = customerMoneyService.selectFlowList(query);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询额度流水列表
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('customersupplier:customerinfo:list')")
|
||||
@GetMapping("/credit/flow/list")
|
||||
public TableDataInfo creditFlowList(CustomerCreditFlow query)
|
||||
{
|
||||
if (query == null || query.getCustomerId() == null || query.getCustomerId() == 0L) {
|
||||
return getDataTable(Collections.emptyList());
|
||||
}
|
||||
startPage();
|
||||
List<CustomerCreditFlow> list = customerMoneyService.selectCreditFlowList(query);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调整客户额度(信用额度/临时额度),调整过程留额度流水
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('customer:credit:adjust')")
|
||||
@Log(title = "客户额度调整", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/credit/adjust")
|
||||
public AjaxResult adjustCredit(@RequestBody CustomerCreditFlow flow)
|
||||
{
|
||||
flow.setCreateBy(getUsername());
|
||||
flow.setOperatorId(getUserId());
|
||||
return toAjax(customerMoneyService.adjustCredit(flow));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* 应收账单余额核销 ar_balance_writeoff
|
||||
*
|
||||
* <p>用客户账户余额核销应收账单,与 ar_receipt_writeoff(收款核销)并列,
|
||||
* 两者共享 ar_bill.written_off_amount / unwritten_amount 缓存。</p>
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-16
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ArBalanceWriteoff extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 有效 */
|
||||
public static final String STATUS_EFFECTIVE = "EFFECTIVE";
|
||||
/** 已取消(反核销) */
|
||||
public static final String STATUS_CANCELLED = "CANCELLED";
|
||||
|
||||
/** 余额核销ID */
|
||||
private Long writeoffId;
|
||||
|
||||
/** 核销单号(BWO开头) */
|
||||
private String writeoffNo;
|
||||
|
||||
/** 应收账单ID */
|
||||
private Long arBillId;
|
||||
|
||||
/** 账单号快照 */
|
||||
private String billNo;
|
||||
|
||||
/** 客户ID */
|
||||
private Long customerId;
|
||||
|
||||
/** 客户账户ID */
|
||||
private Long accountId;
|
||||
|
||||
/** 币种 */
|
||||
private String currency;
|
||||
|
||||
/** 本次核销金额 */
|
||||
private BigDecimal writeoffAmount;
|
||||
|
||||
/** 核销扣款余额流水ID */
|
||||
private Long balanceFlowId;
|
||||
|
||||
/** 反核销回补余额流水ID */
|
||||
private Long reverseFlowId;
|
||||
|
||||
/** 核销日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date writeoffDate;
|
||||
|
||||
/** 状态:EFFECTIVE/CANCELLED */
|
||||
private String status;
|
||||
|
||||
/** 核销备注 */
|
||||
private String remark;
|
||||
|
||||
/** 反核销人 */
|
||||
private String cancelBy;
|
||||
|
||||
/** 反核销时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date cancelTime;
|
||||
|
||||
/** 反核销原因 */
|
||||
private String cancelRemark;
|
||||
|
||||
/* ===== 列表关联展示字段(来自 ar_bill) ===== */
|
||||
|
||||
/** 账单名称 */
|
||||
private String billName;
|
||||
|
||||
/** 客户名称快照 */
|
||||
private String customerName;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
@@ -21,10 +22,13 @@ public class ArBill extends BaseEntity
|
||||
private Long customerId;
|
||||
private String customerCode;
|
||||
private String customerName;
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date billPeriodStart;
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date billPeriodEnd;
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date billDate;
|
||||
private String currency;
|
||||
|
||||
@@ -15,7 +15,7 @@ public class ArBillDetail implements Serializable
|
||||
|
||||
private Long detailId;
|
||||
private Long arBillId;
|
||||
private String prcBillId;
|
||||
private String prcOrderBillId;
|
||||
private Integer detailNo;
|
||||
private String orderNo;
|
||||
private String trackingNo;
|
||||
|
||||
@@ -14,7 +14,7 @@ public class ArBillDetailFee implements Serializable
|
||||
|
||||
private Long detailFeeId;
|
||||
private Long detailId;
|
||||
private String prcFeeId;
|
||||
private String prcIncomeBillFeeId;
|
||||
private Integer feeNo;
|
||||
private String feeType;
|
||||
private String feeName;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.ruoyi.ar.domain.vo;
|
||||
|
||||
import java.util.Date;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import com.ruoyi.ar.domain.ArBill;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/** 应收账单列表查询参数。 */
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ArBillQuery extends ArBill
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 账单日期查询起始日。 */
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||
private Date billDateStart;
|
||||
|
||||
/** 账单日期查询结束日。 */
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||
private Date billDateEnd;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.ruoyi.ar.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.ruoyi.ar.domain.ArBalanceWriteoff;
|
||||
|
||||
/**
|
||||
* 应收账单余额核销 Mapper
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-16
|
||||
*/
|
||||
public interface ArBalanceWriteoffMapper
|
||||
{
|
||||
/** 新增余额核销单 */
|
||||
int insertArBalanceWriteoff(ArBalanceWriteoff writeoff);
|
||||
|
||||
/** 回写扣款账户与余额流水ID */
|
||||
int updateFlowRefs(@Param("writeoffId") Long writeoffId,
|
||||
@Param("accountId") Long accountId,
|
||||
@Param("balanceFlowId") Long balanceFlowId);
|
||||
|
||||
/** 按ID查询(行锁) */
|
||||
ArBalanceWriteoff selectByIdForUpdate(@Param("writeoffId") Long writeoffId);
|
||||
|
||||
/** 反核销:置为CANCELLED并登记回补流水与原因 */
|
||||
int updateCancelled(@Param("writeoffId") Long writeoffId,
|
||||
@Param("reverseFlowId") Long reverseFlowId,
|
||||
@Param("cancelBy") String cancelBy,
|
||||
@Param("cancelRemark") String cancelRemark);
|
||||
|
||||
/** 查询核销记录列表(关联账单名称/客户名称) */
|
||||
List<ArBalanceWriteoff> selectWriteoffList(ArBalanceWriteoff query);
|
||||
}
|
||||
@@ -5,10 +5,11 @@ import java.util.Date;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.ruoyi.ar.domain.ArBill;
|
||||
import com.ruoyi.ar.domain.vo.ArBillQuery;
|
||||
|
||||
public interface ArBillMapper
|
||||
{
|
||||
List<ArBill> selectArBillList(ArBill query);
|
||||
List<ArBill> selectArBillList(ArBillQuery query);
|
||||
|
||||
ArBill selectArBillById(Long arBillId);
|
||||
|
||||
@@ -19,6 +20,9 @@ public interface ArBillMapper
|
||||
|
||||
ArBill selectArBillByIdForUpdate(Long arBillId);
|
||||
|
||||
/** 查询客户可核销账单(已确认/部分核销),按账单日期升序,供余额核销选择 */
|
||||
List<ArBill> selectUnsettledByCustomer(@Param("customerId") Long customerId);
|
||||
|
||||
int insertArBill(ArBill bill);
|
||||
|
||||
int updateArBillStatus(@Param("arBillId") Long arBillId, @Param("status") String status,
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.ruoyi.ar.service;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.ar.domain.ArBalanceWriteoff;
|
||||
import com.ruoyi.ar.domain.ArBill;
|
||||
|
||||
/**
|
||||
* 应收账单余额核销 Service接口
|
||||
*
|
||||
* <p>资金来源为客户账户余额(customer_account.balance),与收款核销
|
||||
* (ar_receipt_writeoff)并列;核销时三方联动:账户余额扣减、已用额度释放、
|
||||
* 账单已核销/未核销金额及状态更新。</p>
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-16
|
||||
*/
|
||||
public interface IArBalanceWriteoffService
|
||||
{
|
||||
/** 查询客户可核销账单(已确认/部分核销,未核销额大于0) */
|
||||
List<ArBill> selectUnsettledBills(Long customerId);
|
||||
|
||||
/** 查询余额核销记录 */
|
||||
List<ArBalanceWriteoff> selectWriteoffList(ArBalanceWriteoff query);
|
||||
|
||||
/** 用客户账户余额核销账单 */
|
||||
ArBalanceWriteoff writeoff(ArBalanceWriteoff cmd);
|
||||
|
||||
/** 反核销(取消一笔有效余额核销,余额回补、账单金额回退) */
|
||||
void cancelWriteoff(Long writeoffId, String remark);
|
||||
}
|
||||
@@ -8,11 +8,12 @@ import com.ruoyi.ar.domain.ArBillLog;
|
||||
import com.ruoyi.ar.domain.vo.ArBillGenerateRequest;
|
||||
import com.ruoyi.ar.domain.vo.ArBillGenerateQuery;
|
||||
import com.ruoyi.ar.domain.vo.ArBillActionRequest;
|
||||
import com.ruoyi.ar.domain.vo.ArBillQuery;
|
||||
import com.ruoyi.price.domain.PrcOrderBill;
|
||||
|
||||
public interface IArBillService
|
||||
{
|
||||
List<ArBill> selectArBillList(ArBill query);
|
||||
List<ArBill> selectArBillList(ArBillQuery query);
|
||||
|
||||
ArBill selectArBillById(Long arBillId);
|
||||
|
||||
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
package com.ruoyi.ar.service.impl;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
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.ar.domain.ArBalanceWriteoff;
|
||||
import com.ruoyi.ar.domain.ArBill;
|
||||
import com.ruoyi.ar.domain.ArBillLog;
|
||||
import com.ruoyi.ar.enums.ArBillStatus;
|
||||
import com.ruoyi.ar.enums.ArLogResult;
|
||||
import com.ruoyi.ar.mapper.ArBalanceWriteoffMapper;
|
||||
import com.ruoyi.ar.mapper.ArBillMapper;
|
||||
import com.ruoyi.ar.service.IArBalanceWriteoffService;
|
||||
import com.ruoyi.ar.service.IArBillLogService;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.common.utils.uuid.IdUtils;
|
||||
import com.ruoyi.customer.customermoney.domain.CustomerBalanceFlow;
|
||||
import com.ruoyi.customer.customermoney.service.ICustomerMoneyService;
|
||||
|
||||
/**
|
||||
* 应收账单余额核销 Service实现
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-16
|
||||
*/
|
||||
@Service
|
||||
public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService
|
||||
{
|
||||
private static final DateTimeFormatter NO_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");
|
||||
private static final int MONEY_SCALE = 2;
|
||||
|
||||
@Autowired
|
||||
private ArBillMapper billMapper;
|
||||
|
||||
@Autowired
|
||||
private ArBalanceWriteoffMapper writeoffMapper;
|
||||
|
||||
@Autowired
|
||||
private IArBillLogService logService;
|
||||
|
||||
@Autowired
|
||||
private ICustomerMoneyService customerMoneyService;
|
||||
|
||||
@Override
|
||||
public List<ArBill> selectUnsettledBills(Long customerId)
|
||||
{
|
||||
if (customerId == null)
|
||||
{
|
||||
return List.of();
|
||||
}
|
||||
return billMapper.selectUnsettledByCustomer(customerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ArBalanceWriteoff> selectWriteoffList(ArBalanceWriteoff query)
|
||||
{
|
||||
return writeoffMapper.selectWriteoffList(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public ArBalanceWriteoff writeoff(ArBalanceWriteoff cmd)
|
||||
{
|
||||
if (cmd == null || cmd.getArBillId() == null)
|
||||
{
|
||||
throw new ServiceException("请选择要核销的账单");
|
||||
}
|
||||
BigDecimal amount = cmd.getWriteoffAmount();
|
||||
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0)
|
||||
{
|
||||
throw new ServiceException("核销金额必须大于0");
|
||||
}
|
||||
amount = amount.setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||
|
||||
// 1. 行锁锁定账单(加锁顺序:账单 -> 账户,与反核销保持一致)
|
||||
ArBill bill = billMapper.selectArBillByIdForUpdate(cmd.getArBillId());
|
||||
if (bill == null)
|
||||
{
|
||||
throw new ServiceException("应收账单不存在");
|
||||
}
|
||||
ArBillStatus status = ArBillStatus.of(bill.getStatus());
|
||||
if (status == null || !status.canWriteOff())
|
||||
{
|
||||
throw new ServiceException("当前账单状态不允许核销:" + (status == null ? bill.getStatus() : status.getLabel()));
|
||||
}
|
||||
if (cmd.getCustomerId() != null && !cmd.getCustomerId().equals(bill.getCustomerId()))
|
||||
{
|
||||
throw new ServiceException("账单与所选客户不匹配");
|
||||
}
|
||||
BigDecimal unwritten = nvl(bill.getUnwrittenAmount());
|
||||
if (amount.compareTo(unwritten) > 0)
|
||||
{
|
||||
throw new ServiceException("核销金额超过账单未核销金额:" + unwritten.toPlainString());
|
||||
}
|
||||
|
||||
String operator = SecurityUtils.getUsername();
|
||||
String requestId = IdUtils.fastSimpleUUID();
|
||||
String currency = StringUtils.hasText(bill.getCurrency()) ? bill.getCurrency().trim().toUpperCase() : "CNY";
|
||||
Date writeoffDate = cmd.getWriteoffDate() != null ? cmd.getWriteoffDate() : new Date();
|
||||
String fromStatus = bill.getStatus();
|
||||
|
||||
// 2. 先落核销单(账户与流水引用拿到后回写)
|
||||
ArBalanceWriteoff writeoff = new ArBalanceWriteoff();
|
||||
writeoff.setWriteoffNo(generateNo());
|
||||
writeoff.setArBillId(bill.getArBillId());
|
||||
writeoff.setBillNo(bill.getBillNo());
|
||||
writeoff.setCustomerId(bill.getCustomerId());
|
||||
writeoff.setCurrency(currency);
|
||||
writeoff.setWriteoffAmount(amount);
|
||||
writeoff.setWriteoffDate(writeoffDate);
|
||||
writeoff.setStatus(ArBalanceWriteoff.STATUS_EFFECTIVE);
|
||||
writeoff.setRemark(StringUtils.hasText(cmd.getRemark()) ? cmd.getRemark().trim() : null);
|
||||
writeoff.setCreateBy(operator);
|
||||
writeoffMapper.insertArBalanceWriteoff(writeoff);
|
||||
|
||||
// 3. 扣减账户余额并释放等额已用额度,写 BILL_PAY 扣款流水(余额不足在此处拦截)
|
||||
String flowRemark = "余额核销账单:" + bill.getBillNo()
|
||||
+ (StringUtils.hasText(writeoff.getRemark()) ? "(" + writeoff.getRemark() + ")" : "");
|
||||
CustomerBalanceFlow flow = customerMoneyService.payBill(bill.getCustomerId(), currency,
|
||||
amount, writeoff.getWriteoffId(), bill.getBillNo(), flowRemark);
|
||||
writeoffMapper.updateFlowRefs(writeoff.getWriteoffId(), flow.getAccountId(), flow.getId());
|
||||
|
||||
// 4. 更新账单已核销/未核销缓存与状态(乐观条件更新,状态被并发改动则整体回滚)
|
||||
BigDecimal written = nvl(bill.getWrittenOffAmount()).add(amount);
|
||||
BigDecimal newUnwritten = unwritten.subtract(amount);
|
||||
String nextStatus = billStatus(written, nvl(bill.getTotalAmount()), fromStatus);
|
||||
if (billMapper.updateArBillAmounts(bill.getArBillId(), written, newUnwritten,
|
||||
nextStatus, operator, fromStatus) != 1)
|
||||
{
|
||||
throw new ServiceException("账单状态已发生变化,请刷新后重试");
|
||||
}
|
||||
|
||||
recordLog(bill.getArBillId(), writeoff.getWriteoffId(), "BALANCE_WRITEOFF",
|
||||
"账户余额核销账单 " + amount.toPlainString() + " " + currency,
|
||||
fromStatus, nextStatus, operator, requestId);
|
||||
writeoff.setAccountId(flow.getAccountId());
|
||||
writeoff.setBalanceFlowId(flow.getId());
|
||||
return writeoff;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void cancelWriteoff(Long writeoffId, String remark)
|
||||
{
|
||||
if (writeoffId == null)
|
||||
{
|
||||
throw new ServiceException("核销记录不存在");
|
||||
}
|
||||
// 1. 锁核销单 -> 锁账单(与核销相同的加锁顺序)
|
||||
ArBalanceWriteoff writeoff = writeoffMapper.selectByIdForUpdate(writeoffId);
|
||||
if (writeoff == null)
|
||||
{
|
||||
throw new ServiceException("核销记录不存在");
|
||||
}
|
||||
if (!ArBalanceWriteoff.STATUS_EFFECTIVE.equals(writeoff.getStatus()))
|
||||
{
|
||||
throw new ServiceException("只有有效状态的核销记录可以反核销");
|
||||
}
|
||||
ArBill bill = billMapper.selectArBillByIdForUpdate(writeoff.getArBillId());
|
||||
if (bill == null)
|
||||
{
|
||||
throw new ServiceException("核销关联的账单不存在");
|
||||
}
|
||||
|
||||
String operator = SecurityUtils.getUsername();
|
||||
String requestId = IdUtils.fastSimpleUUID();
|
||||
String cancelRemark = StringUtils.hasText(remark) ? remark.trim() : "取消余额核销";
|
||||
String fromStatus = bill.getStatus();
|
||||
|
||||
// 2. 余额回补、已用额度重新占用,写反向 BILL_PAY 流水
|
||||
String flowRemark = "反核销回补账单:" + bill.getBillNo() + "(" + cancelRemark + ")";
|
||||
CustomerBalanceFlow reverseFlow = customerMoneyService.reverseBillPay(writeoff.getCustomerId(),
|
||||
writeoff.getCurrency(), nvl(writeoff.getWriteoffAmount()), writeoffId, bill.getBillNo(), flowRemark);
|
||||
|
||||
// 3. 账单核销金额回退,状态按剩余已核销额重算(归零则回到已确认)
|
||||
BigDecimal written = nvl(bill.getWrittenOffAmount()).subtract(nvl(writeoff.getWriteoffAmount()));
|
||||
if (written.compareTo(BigDecimal.ZERO) < 0)
|
||||
{
|
||||
written = BigDecimal.ZERO;
|
||||
}
|
||||
BigDecimal newUnwritten = nvl(bill.getTotalAmount()).subtract(written);
|
||||
String nextStatus = billStatus(written, nvl(bill.getTotalAmount()), fromStatus);
|
||||
if (billMapper.updateArBillAmounts(bill.getArBillId(), written, newUnwritten,
|
||||
nextStatus, operator, fromStatus) != 1)
|
||||
{
|
||||
throw new ServiceException("账单状态已发生变化,请刷新后重试");
|
||||
}
|
||||
|
||||
// 4. 核销单置为CANCELLED并登记回补流水
|
||||
if (writeoffMapper.updateCancelled(writeoffId, reverseFlow.getId(), operator, cancelRemark) != 1)
|
||||
{
|
||||
throw new ServiceException("核销记录状态已变化,请刷新后重试");
|
||||
}
|
||||
recordLog(bill.getArBillId(), writeoffId, "CANCEL_BALANCE_WRITEOFF",
|
||||
"反核销回补账单:" + cancelRemark,
|
||||
fromStatus, nextStatus, operator, requestId);
|
||||
}
|
||||
|
||||
/** 账单状态重算:已核销归零(非草稿)-> 已确认;足额 -> 已核销;否则部分核销 */
|
||||
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 void recordLog(Long billId, Long writeoffId, String operationType, String desc,
|
||||
String fromStatus, String toStatus, String operator, String requestId)
|
||||
{
|
||||
ArBillLog log = new ArBillLog();
|
||||
log.setArBillId(billId);
|
||||
log.setObjectType("BALANCE_WRITEOFF");
|
||||
log.setObjectId(String.valueOf(writeoffId));
|
||||
log.setOperationType(operationType);
|
||||
log.setOperationDesc(desc);
|
||||
log.setFromStatus(fromStatus);
|
||||
log.setToStatus(toStatus);
|
||||
log.setOperatorId(operator);
|
||||
log.setOperatorName(operator);
|
||||
log.setOperationSource("WEB");
|
||||
log.setRequestId(requestId);
|
||||
log.setResult(ArLogResult.SUCCESS.getCode());
|
||||
logService.record(log);
|
||||
}
|
||||
|
||||
private String generateNo()
|
||||
{
|
||||
return "BWO" + LocalDateTime.now().format(NO_FORMATTER)
|
||||
+ ThreadLocalRandom.current().nextInt(100, 1000);
|
||||
}
|
||||
|
||||
private BigDecimal nvl(BigDecimal value)
|
||||
{
|
||||
return value == null ? BigDecimal.ZERO : value;
|
||||
}
|
||||
}
|
||||
+18
-6
@@ -50,6 +50,7 @@ import com.ruoyi.ar.service.IArBillService;
|
||||
import com.ruoyi.ar.domain.vo.ArBillActionRequest;
|
||||
import com.ruoyi.ar.domain.vo.ArBillGenerateQuery;
|
||||
import com.ruoyi.ar.domain.vo.ArBillGenerateRequest;
|
||||
import com.ruoyi.ar.domain.vo.ArBillQuery;
|
||||
import com.ruoyi.common.config.RuoYiConfig;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
@@ -57,6 +58,7 @@ import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.file.FileUtils;
|
||||
import com.ruoyi.common.utils.file.FileUploadUtils;
|
||||
import com.ruoyi.common.utils.uuid.IdUtils;
|
||||
import com.ruoyi.customer.customermoney.service.ICustomerMoneyService;
|
||||
import com.ruoyi.price.domain.PrcIncomeBillFee;
|
||||
import com.ruoyi.price.domain.PrcOrderBill;
|
||||
import com.ruoyi.price.mapper.PrcIncomeBillFeeMapper;
|
||||
@@ -79,9 +81,10 @@ public class ArBillServiceImpl implements IArBillService
|
||||
@Autowired private IArBillLogService logService;
|
||||
@Autowired private PrcOrderBillMapper sourceBillMapper;
|
||||
@Autowired private PrcIncomeBillFeeMapper sourceFeeMapper;
|
||||
@Autowired private ICustomerMoneyService customerMoneyService;
|
||||
|
||||
@Override
|
||||
public List<ArBill> selectArBillList(ArBill query)
|
||||
public List<ArBill> selectArBillList(ArBillQuery query)
|
||||
{
|
||||
List<ArBill> bills = billMapper.selectArBillList(query);
|
||||
for (ArBill bill : bills)
|
||||
@@ -224,7 +227,7 @@ public class ArBillServiceImpl implements IArBillService
|
||||
Map<String, ArBillDetail> detailsBySourceBill = new HashMap<>();
|
||||
for (ArBillDetail detail : detailMapper.selectByBillId(bill.getArBillId()))
|
||||
{
|
||||
detailsBySourceBill.put(detail.getPrcBillId(), detail);
|
||||
detailsBySourceBill.put(detail.getPrcOrderBillId(), detail);
|
||||
}
|
||||
List<ArBillDetailFee> detailFees = new ArrayList<>();
|
||||
List<PrcIncomeBillFee> sourceFees = sourceBillIds.isEmpty()
|
||||
@@ -276,6 +279,9 @@ public class ArBillServiceImpl implements IArBillService
|
||||
throw failure(arBillId, "BILL", String.valueOf(arBillId), "CONFIRM", bill.getStatus(),
|
||||
"账单状态已发生变化,请刷新后重试", operator, requestId);
|
||||
}
|
||||
// 账单确认:应收形成,按账单金额占用客户授信额度(账户不存在时仅CNY会自动初始化)
|
||||
customerMoneyService.occupyCredit(bill.getCustomerId(), bill.getCurrency(),
|
||||
nvl(bill.getTotalAmount()));
|
||||
recordSuccess(bill, "CONFIRM", "确认应收账单", ArBillStatus.DRAFT.getCode(), ArBillStatus.CONFIRMED.getCode(), requestId, operator);
|
||||
return selectArBillById(arBillId);
|
||||
}
|
||||
@@ -303,9 +309,9 @@ public class ArBillServiceImpl implements IArBillService
|
||||
List<String> sourceBillIds = new ArrayList<>();
|
||||
for (ArBillDetail detail : detailMapper.selectByBillId(arBillId))
|
||||
{
|
||||
if (!StringUtils.isEmpty(detail.getPrcBillId()))
|
||||
if (!StringUtils.isEmpty(detail.getPrcOrderBillId()))
|
||||
{
|
||||
sourceBillIds.add(detail.getPrcBillId());
|
||||
sourceBillIds.add(detail.getPrcOrderBillId());
|
||||
}
|
||||
}
|
||||
if (billMapper.updateArBillStatus(arBillId, ArBillStatus.VOID.getCode(), remark, operator, currentStatus.getCode()) != 1)
|
||||
@@ -313,6 +319,12 @@ public class ArBillServiceImpl implements IArBillService
|
||||
throw failure(arBillId, "BILL", String.valueOf(arBillId), "VOID", bill.getStatus(),
|
||||
"账单状态已发生变化,请刷新后重试", operator, requestId);
|
||||
}
|
||||
// 账单作废:已确认账单占用的授信额度全额释放(可作废的CONFIRMED账单必然零核销)
|
||||
if (currentStatus == ArBillStatus.CONFIRMED)
|
||||
{
|
||||
customerMoneyService.releaseCredit(bill.getCustomerId(), bill.getCurrency(),
|
||||
nvl(bill.getTotalAmount()));
|
||||
}
|
||||
if (!sourceBillIds.isEmpty())
|
||||
{
|
||||
int releasedCount = sourceBillMapper.releaseArBillOrders(sourceBillIds, operator,
|
||||
@@ -459,7 +471,7 @@ public class ArBillServiceImpl implements IArBillService
|
||||
{
|
||||
ArBillDetail detail = new ArBillDetail();
|
||||
detail.setArBillId(bill.getArBillId());
|
||||
detail.setPrcBillId(source.getBillId());
|
||||
detail.setPrcOrderBillId(source.getBillId());
|
||||
detail.setDetailNo(detailNo);
|
||||
detail.setOrderNo(source.getOrderNo());
|
||||
detail.setTrackingNo(source.getTrackingNo());
|
||||
@@ -487,7 +499,7 @@ public class ArBillServiceImpl implements IArBillService
|
||||
{
|
||||
ArBillDetailFee fee = new ArBillDetailFee();
|
||||
fee.setDetailId(detail.getDetailId());
|
||||
fee.setPrcFeeId(source.getFeeId());
|
||||
fee.setPrcIncomeBillFeeId(source.getFeeId());
|
||||
fee.setFeeNo(feeNo);
|
||||
fee.setFeeType(source.getFeeType());
|
||||
fee.setFeeName(source.getFeeName());
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package com.ruoyi.customer.customerinfo.domain;
|
||||
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 客户套餐绑定对象 prc_customer_price_set
|
||||
* 客户管理-客户编辑-绑定套餐 使用;关联 prc_price_set 的字段仅用于页面展示
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-16
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class BmsCustomerPriceSet extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 客户价格套餐ID */
|
||||
private Long customerPricesetId;
|
||||
|
||||
/** 价格套餐ID */
|
||||
private Long priceSetId;
|
||||
|
||||
/** 客户ID */
|
||||
private Long customerId;
|
||||
|
||||
/** 客户代码 */
|
||||
private String customerCode;
|
||||
|
||||
/** 客户名称 */
|
||||
private String customerName;
|
||||
|
||||
/** 绑定优先级,数字越小越优先 */
|
||||
private Integer bindPriority;
|
||||
|
||||
/** 价格开始生效日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date prBegindate;
|
||||
|
||||
/** 价格失效日期(空=长期) */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date prEnddate;
|
||||
|
||||
/** 绑定备注 */
|
||||
private String priceSetNote;
|
||||
|
||||
/* -------- 关联 prc_price_set 展示字段 -------- */
|
||||
|
||||
/** 套餐编码 */
|
||||
private String priceSetCode;
|
||||
|
||||
/** 套餐名称 */
|
||||
private String priceSetName;
|
||||
|
||||
/** 计价类型 FIXED=固定价 DISCOUNT=折扣 */
|
||||
private String setType;
|
||||
|
||||
/** 套餐状态 D草稿 A审核中 E生效 R驳回 S停用 */
|
||||
private String priceSetStatus;
|
||||
|
||||
/** 套餐生效日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date setEffectiveDate;
|
||||
|
||||
/** 套餐失效日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date setExpireDate;
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package com.ruoyi.customer.customerinfo.domain;
|
||||
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 价格套餐(prc_price_set)简要对象
|
||||
* 仅用于客户编辑-绑定套餐的套餐下拉选择,不参与套餐维护
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-16
|
||||
*/
|
||||
@Data
|
||||
public class BmsPriceSetOption
|
||||
{
|
||||
/** 价格套餐ID */
|
||||
private Long priceSetId;
|
||||
|
||||
/** 套餐编码 */
|
||||
private String priceSetCode;
|
||||
|
||||
/** 套餐名称 */
|
||||
private String priceSetName;
|
||||
|
||||
/** 计价类型 FIXED=固定价 DISCOUNT=折扣 */
|
||||
private String setType;
|
||||
|
||||
/** 套餐状态 D草稿 A审核中 E生效 R驳回 S停用 */
|
||||
private String status;
|
||||
|
||||
/** 生效日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date effectiveDate;
|
||||
|
||||
/** 失效日期(空=长期) */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date expireDate;
|
||||
|
||||
/** 关键字(编码/名称模糊) */
|
||||
private String keyword;
|
||||
}
|
||||
+23
@@ -23,6 +23,15 @@ public interface BmsCustomerMapper
|
||||
*/
|
||||
public BmsCustomer selectBmsCustomerByCustomerId(Long customerId);
|
||||
|
||||
/**
|
||||
* 只取客户档案物理额度列(credit_ed 在通用查询里是账户可用额度的子查询别名,
|
||||
* 账户首次初始化时必须读取档案原始值)
|
||||
*
|
||||
* @param customerId 客户ID
|
||||
* @return 仅含 creditEd/tempEd 的客户对象
|
||||
*/
|
||||
public BmsCustomer selectRawCreditByCustomerId(Long customerId);
|
||||
|
||||
/**
|
||||
* 查询客户主列表
|
||||
*
|
||||
@@ -103,6 +112,20 @@ public interface BmsCustomerMapper
|
||||
*/
|
||||
public int deleteBmsCustomerProfileAttachmentByCustomerId(Long customerId);
|
||||
|
||||
/**
|
||||
* 同步客户档案额度字段(额度以客户余额账户为准,调整后回写档案)
|
||||
*
|
||||
* @param customerId 客户ID
|
||||
* @param creditEd 信用额度
|
||||
* @param tempEd 临时额度
|
||||
* @param updateBy 更新人
|
||||
* @return 结果
|
||||
*/
|
||||
public int updateCustomerCredit(@org.apache.ibatis.annotations.Param("customerId") Long customerId,
|
||||
@org.apache.ibatis.annotations.Param("creditEd") java.math.BigDecimal creditEd,
|
||||
@org.apache.ibatis.annotations.Param("tempEd") java.math.BigDecimal tempEd,
|
||||
@org.apache.ibatis.annotations.Param("updateBy") String updateBy);
|
||||
|
||||
/**
|
||||
* 批量删除客户附件
|
||||
*
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.ruoyi.customer.customerinfo.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.ruoyi.customer.customerinfo.domain.BmsCustomerPriceSet;
|
||||
import com.ruoyi.customer.customerinfo.domain.BmsPriceSetOption;
|
||||
|
||||
/**
|
||||
* 客户套餐绑定 数据层(客户管理侧独立实现,操作 prc_customer_price_set)
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-16
|
||||
*/
|
||||
public interface BmsCustomerPriceSetMapper
|
||||
{
|
||||
/** 按客户查绑定列表(join prc_price_set 取套餐展示字段) */
|
||||
public List<BmsCustomerPriceSet> selectBindListByCustomerId(@Param("customerId") Long customerId);
|
||||
|
||||
/** 按主键查绑定 */
|
||||
public BmsCustomerPriceSet selectBindById(Long customerPricesetId);
|
||||
|
||||
/** 按客户ID查绑定(uk_customer_one_set 保证至多一条) */
|
||||
public BmsCustomerPriceSet selectBindByCustomerId(@Param("customerId") Long customerId);
|
||||
|
||||
/** 新增绑定 */
|
||||
public int insertBind(BmsCustomerPriceSet bind);
|
||||
|
||||
/** 更新绑定(允许更换套餐 price_set_id 与生效区间/优先级/备注) */
|
||||
public int updateBind(BmsCustomerPriceSet bind);
|
||||
|
||||
/** 按主键批量解绑 */
|
||||
public int deleteBindByIds(Long[] customerPricesetIds);
|
||||
|
||||
/** 套餐下拉选项(只列生效中套餐,可按关键字/类型过滤) */
|
||||
public List<BmsPriceSetOption> selectPriceSetOptionList(BmsPriceSetOption query);
|
||||
|
||||
/** 按主键查套餐简要信息 */
|
||||
public BmsPriceSetOption selectPriceSetOptionById(@Param("priceSetId") Long priceSetId);
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.ruoyi.customer.customerinfo.service;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.customer.customerinfo.domain.BmsCustomerPriceSet;
|
||||
import com.ruoyi.customer.customerinfo.domain.BmsPriceSetOption;
|
||||
|
||||
/**
|
||||
* 客户套餐绑定 服务层(客户管理侧)
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-16
|
||||
*/
|
||||
public interface IBmsCustomerPriceSetService
|
||||
{
|
||||
/** 查询某客户的套餐绑定列表 */
|
||||
public List<BmsCustomerPriceSet> selectBindList(Long customerId);
|
||||
|
||||
/** 查询可绑定的生效套餐选项 */
|
||||
public List<BmsPriceSetOption> selectOptionList(BmsPriceSetOption query);
|
||||
|
||||
/**
|
||||
* 保存客户套餐绑定(一个客户一条记录):已绑定则更新(支持更换套餐),未绑定则新增
|
||||
*/
|
||||
public int saveBind(BmsCustomerPriceSet bind);
|
||||
|
||||
/** 按绑定主键解绑 */
|
||||
public int deleteBindByIds(Long[] customerPricesetIds);
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
package com.ruoyi.customer.customerinfo.service.impl;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.customer.customerinfo.domain.BmsCustomer;
|
||||
import com.ruoyi.customer.customerinfo.domain.BmsCustomerPriceSet;
|
||||
import com.ruoyi.customer.customerinfo.domain.BmsPriceSetOption;
|
||||
import com.ruoyi.customer.customerinfo.mapper.BmsCustomerPriceSetMapper;
|
||||
import com.ruoyi.customer.customerinfo.service.IBmsCustomerPriceSetService;
|
||||
import com.ruoyi.customer.customerinfo.service.IBmsCustomerService;
|
||||
|
||||
/**
|
||||
* 客户套餐绑定 服务实现(客户管理侧,独立于价格套餐维护模块)
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-16
|
||||
*/
|
||||
@Service
|
||||
public class BmsCustomerPriceSetServiceImpl implements IBmsCustomerPriceSetService
|
||||
{
|
||||
@Autowired
|
||||
private BmsCustomerPriceSetMapper bindMapper;
|
||||
|
||||
@Autowired
|
||||
private IBmsCustomerService customerService;
|
||||
|
||||
@Override
|
||||
public List<BmsCustomerPriceSet> selectBindList(Long customerId)
|
||||
{
|
||||
if (customerId == null)
|
||||
{
|
||||
return List.of();
|
||||
}
|
||||
return bindMapper.selectBindListByCustomerId(customerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BmsPriceSetOption> selectOptionList(BmsPriceSetOption query)
|
||||
{
|
||||
return bindMapper.selectPriceSetOptionList(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int saveBind(BmsCustomerPriceSet bind)
|
||||
{
|
||||
if (bind.getCustomerId() == null)
|
||||
{
|
||||
throw new ServiceException("客户ID不能为空");
|
||||
}
|
||||
if (bind.getPriceSetId() == null)
|
||||
{
|
||||
throw new ServiceException("请选择要绑定的套餐");
|
||||
}
|
||||
BmsCustomer customer = customerService.selectBmsCustomerByCustomerId(bind.getCustomerId());
|
||||
if (customer == null)
|
||||
{
|
||||
throw new ServiceException("客户不存在或已被删除");
|
||||
}
|
||||
BmsPriceSetOption set = bindMapper.selectPriceSetOptionById(bind.getPriceSetId());
|
||||
if (set == null)
|
||||
{
|
||||
throw new ServiceException("所选套餐不存在");
|
||||
}
|
||||
if (!"E".equals(set.getStatus()))
|
||||
{
|
||||
throw new ServiceException("仅生效中的套餐允许绑定,当前套餐状态不可用");
|
||||
}
|
||||
if (bind.getPrBegindate() == null)
|
||||
{
|
||||
bind.setPrBegindate(set.getEffectiveDate());
|
||||
}
|
||||
if (bind.getPrBegindate() == null)
|
||||
{
|
||||
throw new ServiceException("绑定生效日期不能为空");
|
||||
}
|
||||
if (bind.getPrEnddate() != null && bind.getPrBegindate().after(bind.getPrEnddate()))
|
||||
{
|
||||
throw new ServiceException("失效日期不能早于生效日期");
|
||||
}
|
||||
if (bind.getBindPriority() == null || bind.getBindPriority() < 1)
|
||||
{
|
||||
bind.setBindPriority(1);
|
||||
}
|
||||
|
||||
bind.setCustomerCode(customer.getCustomerCode());
|
||||
bind.setCustomerName(customer.getCustomerName());
|
||||
|
||||
BmsCustomerPriceSet existing = bindMapper.selectBindByCustomerId(bind.getCustomerId());
|
||||
try
|
||||
{
|
||||
if (existing != null)
|
||||
{
|
||||
bind.setCustomerPricesetId(existing.getCustomerPricesetId());
|
||||
return bindMapper.updateBind(bind);
|
||||
}
|
||||
return bindMapper.insertBind(bind);
|
||||
}
|
||||
catch (DuplicateKeyException e)
|
||||
{
|
||||
throw new ServiceException("该客户已存在套餐绑定,请勿重复绑定");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteBindByIds(Long[] customerPricesetIds)
|
||||
{
|
||||
if (customerPricesetIds == null || customerPricesetIds.length == 0)
|
||||
{
|
||||
throw new ServiceException("请选择要解绑的记录");
|
||||
}
|
||||
return bindMapper.deleteBindByIds(customerPricesetIds);
|
||||
}
|
||||
}
|
||||
+6
@@ -14,6 +14,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import com.ruoyi.customer.customerinfo.mapper.BmsCustomerMapper;
|
||||
import com.ruoyi.customer.customerinfo.domain.BmsCustomer;
|
||||
import com.ruoyi.customer.customerinfo.service.IBmsCustomerService;
|
||||
import com.ruoyi.customer.customermoney.service.ICustomerMoneyService;
|
||||
|
||||
/**
|
||||
* 客户主Service业务层处理
|
||||
@@ -27,6 +28,9 @@ public class BmsCustomerServiceImpl implements IBmsCustomerService
|
||||
@Autowired
|
||||
private BmsCustomerMapper bmsCustomerMapper;
|
||||
|
||||
@Autowired
|
||||
private ICustomerMoneyService customerMoneyService;
|
||||
|
||||
/**
|
||||
* 查询客户主
|
||||
*
|
||||
@@ -226,6 +230,8 @@ public class BmsCustomerServiceImpl implements IBmsCustomerService
|
||||
saveBmsCustomerAttachments(bmsCustomer.getCustomerId(), bmsCustomer.getAttachmentList());
|
||||
saveBmsCustomerCompanies(bmsCustomer.getCustomerId(), bmsCustomer.getCompanyList());
|
||||
saveBmsCustomerContacts(bmsCustomer.getCustomerId(), bmsCustomer.getContactList());
|
||||
// 新建客户即初始化默认CNY账户:余额0、总额度1000 CNY,写INIT流水
|
||||
customerMoneyService.initDefaultCnyAccount(bmsCustomer.getCustomerId());
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
+11
-1
@@ -1,6 +1,8 @@
|
||||
package com.ruoyi.customer.customermoney.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;
|
||||
@@ -35,9 +37,17 @@ public class CustomerAccount extends BaseEntity
|
||||
/** 总额度 */
|
||||
private BigDecimal totalCredit;
|
||||
|
||||
/** 额外额度 */
|
||||
/** 额外额度(临时额度) */
|
||||
private BigDecimal extraCredit;
|
||||
|
||||
/** 临时额度生效日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date extraEffectiveDate;
|
||||
|
||||
/** 临时额度失效日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date extraExpireDate;
|
||||
|
||||
/** 已用额度 */
|
||||
private BigDecimal usedCredit;
|
||||
|
||||
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package com.ruoyi.customer.customermoney.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;
|
||||
|
||||
/**
|
||||
* 客户额度流水 customer_credit_flow
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-16
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class CustomerCreditFlow extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 额度类型:信用额度(长期) */
|
||||
public static final String CREDIT_TYPE_CREDIT = "CREDIT";
|
||||
/** 额度类型:临时额度 */
|
||||
public static final String CREDIT_TYPE_TEMP = "TEMP";
|
||||
|
||||
/** 变动类型:初始化 */
|
||||
public static final String CHANGE_INIT = "INIT";
|
||||
/** 变动类型:手工调整 */
|
||||
public static final String CHANGE_ADJUST = "ADJUST";
|
||||
/** 变动类型:到期自动失效 */
|
||||
public static final String CHANGE_EXPIRE = "EXPIRE";
|
||||
|
||||
/** 调整方式:设置为 */
|
||||
public static final String MODE_SET = "SET";
|
||||
/** 调整方式:增加 */
|
||||
public static final String MODE_ADD = "ADD";
|
||||
/** 调整方式:减少 */
|
||||
public static final String MODE_SUB = "SUB";
|
||||
|
||||
/** 主键 */
|
||||
private Long id;
|
||||
|
||||
/** 流水号 */
|
||||
private String flowNo;
|
||||
|
||||
/** 客户ID */
|
||||
private Long customerId;
|
||||
|
||||
/** 客户账户ID */
|
||||
private Long accountId;
|
||||
|
||||
/** 额度类型:CREDIT信用额度 TEMP临时额度 */
|
||||
private String creditType;
|
||||
|
||||
/** 变动类型:INIT初始化 ADJUST调整 EXPIRE到期失效 */
|
||||
private String changeType;
|
||||
|
||||
/** 变动方向:1增加 -1减少 0持平 */
|
||||
private Integer direction;
|
||||
|
||||
/** 变动前额度 */
|
||||
private BigDecimal beforeAmount;
|
||||
|
||||
/** 变动额度(恒为非负数,方向看 direction) */
|
||||
private BigDecimal changeAmount;
|
||||
|
||||
/** 变动后额度 */
|
||||
private BigDecimal afterAmount;
|
||||
|
||||
/** 变动时已用额度快照 */
|
||||
private BigDecimal usedCredit;
|
||||
|
||||
/** 生效日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date effectiveDate;
|
||||
|
||||
/** 失效日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date expireDate;
|
||||
|
||||
/** 调整原因 */
|
||||
private String reason;
|
||||
|
||||
/** 调整方式(仅ADJUST流水有值):SET/ADD/SUB */
|
||||
private String adjustMode;
|
||||
|
||||
/** 操作人ID */
|
||||
private Long operatorId;
|
||||
|
||||
/** 删除标志(0存在 1删除) */
|
||||
private String delFlag;
|
||||
}
|
||||
+12
@@ -4,6 +4,7 @@ import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
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.domain.CustomerReceipt;
|
||||
|
||||
/**
|
||||
@@ -33,6 +34,17 @@ public interface CustomerMoneyMapper
|
||||
/** 乐观锁更新余额 */
|
||||
int updateAccountBalance(CustomerAccount account);
|
||||
|
||||
/** 乐观锁更新额度(信用额度/临时额度/有效期) */
|
||||
int updateAccountCredit(CustomerAccount account);
|
||||
|
||||
/* ================= 额度流水 ================= */
|
||||
|
||||
/** 查询额度流水 */
|
||||
List<CustomerCreditFlow> selectCreditFlowList(CustomerCreditFlow query);
|
||||
|
||||
/** 新增额度流水 */
|
||||
int insertCreditFlow(CustomerCreditFlow flow);
|
||||
|
||||
/* ================= 客户收款 ================= */
|
||||
|
||||
/** 查询收款单列表 */
|
||||
|
||||
+53
@@ -1,8 +1,10 @@
|
||||
package com.ruoyi.customer.customermoney.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import com.ruoyi.customer.customermoney.domain.CustomerAccount;
|
||||
import com.ruoyi.customer.customermoney.domain.CustomerBalanceFlow;
|
||||
import com.ruoyi.customer.customermoney.domain.CustomerCreditFlow;
|
||||
import com.ruoyi.customer.customermoney.domain.CustomerReceipt;
|
||||
|
||||
/**
|
||||
@@ -16,6 +18,15 @@ public interface ICustomerMoneyService
|
||||
/** 查询客户各币种账户 */
|
||||
List<CustomerAccount> selectAccountList(Long customerId);
|
||||
|
||||
/**
|
||||
* 新建客户时初始化默认CNY账户(余额0,默认总额度1000 CNY),并写 INIT 额度流水。
|
||||
* 已存在CNY账户时不重复创建。
|
||||
*
|
||||
* @param customerId 客户ID
|
||||
* @return 1=已初始化或已存在
|
||||
*/
|
||||
int initDefaultCnyAccount(Long customerId);
|
||||
|
||||
/** 查询收款单列表 */
|
||||
List<CustomerReceipt> selectReceiptList(CustomerReceipt query);
|
||||
|
||||
@@ -36,4 +47,46 @@ public interface ICustomerMoneyService
|
||||
|
||||
/** 查询余额流水列表 */
|
||||
List<CustomerBalanceFlow> selectFlowList(CustomerBalanceFlow query);
|
||||
|
||||
/** 查询额度流水列表 */
|
||||
List<CustomerCreditFlow> selectCreditFlowList(CustomerCreditFlow query);
|
||||
|
||||
/**
|
||||
* 调整客户额度(信用额度/临时额度)
|
||||
* 事务内:锁定CNY账户(不存在则初始化)-> 校验(不能为负、不能低于已用额度)
|
||||
* -> 乐观锁更新额度 -> 回写客户档案额度字段 -> 写额度流水
|
||||
*/
|
||||
int adjustCredit(CustomerCreditFlow flow);
|
||||
|
||||
/**
|
||||
* 账单确认时占用授信额度(used_credit += amount)。该币种账户不存在时跳过。
|
||||
* 调用方必须已持有账单行锁,本方法内部再锁账户(加锁顺序:账单 -> 账户)。
|
||||
*/
|
||||
void occupyCredit(Long customerId, String currency, BigDecimal amount);
|
||||
|
||||
/**
|
||||
* 账单作废时释放授信额度(used_credit -= amount,不小于0)。该币种账户不存在时跳过。
|
||||
* 调用方必须已持有账单行锁。
|
||||
*/
|
||||
void releaseCredit(Long customerId, String currency, BigDecimal amount);
|
||||
|
||||
/**
|
||||
* 用账户余额核销账单:校验余额充足 -> balance -= amount、used_credit -= amount
|
||||
* -> 写一条 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(重新占用授信)
|
||||
* -> 写一条 BILL_PAY 回补流水(direction=1)。返回已落库的流水(含ID)。
|
||||
*
|
||||
* @param bizId 余额核销单ID
|
||||
* @param bizNo 账单号
|
||||
*/
|
||||
CustomerBalanceFlow reverseBillPay(Long customerId, String currency, BigDecimal amount,
|
||||
Long bizId, String bizNo, String remark);
|
||||
}
|
||||
|
||||
+522
-23
@@ -2,6 +2,7 @@ package com.ruoyi.customer.customermoney.service.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;
|
||||
@@ -14,10 +15,12 @@ import org.springframework.util.CollectionUtils;
|
||||
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.domain.BmsCustomerAttachment;
|
||||
import com.ruoyi.customer.customerinfo.mapper.BmsCustomerMapper;
|
||||
import com.ruoyi.customer.customermoney.domain.CustomerAccount;
|
||||
import com.ruoyi.customer.customermoney.domain.CustomerBalanceFlow;
|
||||
import com.ruoyi.customer.customermoney.domain.CustomerCreditFlow;
|
||||
import com.ruoyi.customer.customermoney.domain.CustomerReceipt;
|
||||
import com.ruoyi.customer.customermoney.mapper.CustomerMoneyMapper;
|
||||
import com.ruoyi.customer.customermoney.service.ICustomerMoneyService;
|
||||
@@ -37,6 +40,8 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
||||
|
||||
/** 本位币币种 */
|
||||
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;
|
||||
|
||||
@@ -50,9 +55,25 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
||||
private IBmsExchangeRateService exchangeRateService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<CustomerAccount> selectAccountList(Long customerId)
|
||||
{
|
||||
return customerMoneyMapper.selectAccountListByCustomerId(customerId);
|
||||
List<CustomerAccount> list = customerMoneyMapper.selectAccountListByCustomerId(customerId);
|
||||
// 查询时顺带处理临时额度到期自动失效(仅CNY账户),保证卡片展示为最新值
|
||||
for (int i = 0; i < list.size(); i++)
|
||||
{
|
||||
CustomerAccount account = list.get(i);
|
||||
if (BASE_CURRENCY.equals(account.getCurrency()) && isTempCreditExpired(account))
|
||||
{
|
||||
CustomerAccount locked = customerMoneyMapper.selectAccountForUpdate(customerId, BASE_CURRENCY);
|
||||
expireTempCreditIfNeeded(locked);
|
||||
if (locked != null)
|
||||
{
|
||||
list.set(i, locked);
|
||||
}
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -167,23 +188,8 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
||||
? receipt.getBaseAmount()
|
||||
: originalAmount.multiply(rate).setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||
|
||||
// 1. 行锁锁定客户本位币(CNY)账户,不存在则自动初始化。无论收什么币种,一律折算 CNY 入账
|
||||
CustomerAccount account = customerMoneyMapper.selectAccountForUpdate(receipt.getCustomerId(), BASE_CURRENCY);
|
||||
if (account == null)
|
||||
{
|
||||
account = new CustomerAccount();
|
||||
account.setCustomerId(receipt.getCustomerId());
|
||||
account.setCurrency(BASE_CURRENCY);
|
||||
account.setBalance(BigDecimal.ZERO);
|
||||
account.setFrozenBalance(BigDecimal.ZERO);
|
||||
account.setTotalCredit(BigDecimal.ZERO);
|
||||
account.setExtraCredit(BigDecimal.ZERO);
|
||||
account.setUsedCredit(BigDecimal.ZERO);
|
||||
account.setTax(BigDecimal.ZERO);
|
||||
account.setVersion(0);
|
||||
account.setCreateBy(username);
|
||||
customerMoneyMapper.insertAccount(account);
|
||||
}
|
||||
// 1. 行锁锁定客户本位币(CNY)账户,不存在则按客户档案额度自动初始化。无论收什么币种,一律折算 CNY 入账
|
||||
CustomerAccount account = getOrInitCnyAccount(receipt.getCustomerId(), username);
|
||||
|
||||
// 2. 本位币余额变动(预充值/混合增加余额;还款核销释放已用额度)
|
||||
BigDecimal beforeBalance = nz(account.getBalance());
|
||||
@@ -191,12 +197,8 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
||||
BigDecimal frozenBalance = nz(account.getFrozenBalance());
|
||||
|
||||
account.setBalance(afterBalance);
|
||||
if (CustomerReceipt.TYPE_REPAY.equals(receipt.getReceiptType()))
|
||||
{
|
||||
account.setUsedCredit(nz(account.getUsedCredit()).subtract(baseAmount).max(BigDecimal.ZERO));
|
||||
}
|
||||
account.setUpdateBy(username);
|
||||
|
||||
// 额度占用统一以账单为准:收款审核只增加余额,核销账单时才释放已用额度
|
||||
int accountRows = customerMoneyMapper.updateAccountBalance(account);
|
||||
if (accountRows == 0)
|
||||
{
|
||||
@@ -260,6 +262,503 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
||||
return customerMoneyMapper.selectFlowList(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CustomerCreditFlow> selectCreditFlowList(CustomerCreditFlow query)
|
||||
{
|
||||
return customerMoneyMapper.selectCreditFlowList(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int adjustCredit(CustomerCreditFlow cmd)
|
||||
{
|
||||
if (cmd == null || cmd.getCustomerId() == null)
|
||||
{
|
||||
throw new ServiceException("客户ID不能为空");
|
||||
}
|
||||
Long customerId = cmd.getCustomerId();
|
||||
String creditType = cmd.getCreditType();
|
||||
if (!CustomerCreditFlow.CREDIT_TYPE_CREDIT.equals(creditType)
|
||||
&& !CustomerCreditFlow.CREDIT_TYPE_TEMP.equals(creditType))
|
||||
{
|
||||
throw new ServiceException("额度类型不正确");
|
||||
}
|
||||
String adjustMode = cmd.getAdjustMode();
|
||||
if (!CustomerCreditFlow.MODE_SET.equals(adjustMode)
|
||||
&& !CustomerCreditFlow.MODE_ADD.equals(adjustMode)
|
||||
&& !CustomerCreditFlow.MODE_SUB.equals(adjustMode))
|
||||
{
|
||||
throw new ServiceException("调整方式不正确");
|
||||
}
|
||||
BigDecimal input = nz(cmd.getChangeAmount());
|
||||
if (input.compareTo(BigDecimal.ZERO) <= 0)
|
||||
{
|
||||
throw new ServiceException("调整金额必须大于0");
|
||||
}
|
||||
if (!StringUtils.hasText(cmd.getReason()))
|
||||
{
|
||||
throw new ServiceException("请填写调整原因");
|
||||
}
|
||||
String reason = cmd.getReason().trim();
|
||||
if (reason.length() > 255)
|
||||
{
|
||||
throw new ServiceException("调整原因不能超过255个字");
|
||||
}
|
||||
String username = SecurityUtils.getUsername();
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
boolean isTemp = CustomerCreditFlow.CREDIT_TYPE_TEMP.equals(creditType);
|
||||
|
||||
// 1. 行锁锁定CNY账户(不存在则按客户档案额度初始化创建);先处理临时额度过期
|
||||
CustomerAccount account = getOrInitCnyAccount(customerId, username);
|
||||
expireTempCreditIfNeeded(account);
|
||||
|
||||
// 2. 计算调整结果
|
||||
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("额度未发生变化,无需调整");
|
||||
}
|
||||
|
||||
// 3. 临时额度有效期校验(不允许预约未来生效,失效日必须晚于生效日)
|
||||
Date effectiveDate = null;
|
||||
Date expireDate = null;
|
||||
if (isTemp)
|
||||
{
|
||||
LocalDate today = LocalDate.now();
|
||||
LocalDate eff = cmd.getEffectiveDate() != null ? toLocalDate(cmd.getEffectiveDate()) : today;
|
||||
if (eff.isAfter(today))
|
||||
{
|
||||
throw new ServiceException("生效日期不能晚于今天,需要未来生效请在生效当天再设置");
|
||||
}
|
||||
effectiveDate = toSqlDate(eff);
|
||||
if (cmd.getExpireDate() != null)
|
||||
{
|
||||
LocalDate exp = toLocalDate(cmd.getExpireDate());
|
||||
if (!exp.isAfter(eff))
|
||||
{
|
||||
throw new ServiceException("失效日期必须晚于生效日期");
|
||||
}
|
||||
expireDate = toSqlDate(exp);
|
||||
}
|
||||
account.setExtraCredit(after);
|
||||
account.setExtraEffectiveDate(effectiveDate);
|
||||
account.setExtraExpireDate(expireDate);
|
||||
}
|
||||
else
|
||||
{
|
||||
account.setTotalCredit(after);
|
||||
}
|
||||
account.setUpdateBy(username);
|
||||
|
||||
// 4. 乐观锁更新额度
|
||||
int rows = customerMoneyMapper.updateAccountCredit(account);
|
||||
if (rows == 0)
|
||||
{
|
||||
throw new ServiceException("账户额度已被其他操作变动,请刷新后重试");
|
||||
}
|
||||
account.setVersion(account.getVersion() + 1);
|
||||
|
||||
// 5. 回写客户档案额度字段(额度唯一数据源为账户,档案仅做同步展示)
|
||||
bmsCustomerMapper.updateCustomerCredit(customerId,
|
||||
nz(account.getTotalCredit()), nz(account.getExtraCredit()), username);
|
||||
|
||||
// 6. 写额度流水
|
||||
CustomerCreditFlow flow = new CustomerCreditFlow();
|
||||
flow.setFlowNo(generateNo("EDLS"));
|
||||
flow.setCustomerId(customerId);
|
||||
flow.setAccountId(account.getId());
|
||||
flow.setCreditType(creditType);
|
||||
flow.setChangeType(CustomerCreditFlow.CHANGE_ADJUST);
|
||||
int direction = after.compareTo(before) > 0 ? 1 : -1;
|
||||
flow.setDirection(direction);
|
||||
flow.setBeforeAmount(before);
|
||||
flow.setChangeAmount(after.subtract(before).abs());
|
||||
flow.setAfterAmount(after);
|
||||
flow.setUsedCredit(nz(account.getUsedCredit()));
|
||||
flow.setEffectiveDate(effectiveDate);
|
||||
flow.setExpireDate(expireDate);
|
||||
flow.setReason(reason);
|
||||
flow.setAdjustMode(adjustMode);
|
||||
flow.setOperatorId(userId);
|
||||
flow.setCreateBy(username);
|
||||
flow.setRemark(cmd.getRemark());
|
||||
flow.setDelFlag("0");
|
||||
customerMoneyMapper.insertCreditFlow(flow);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ================= 账单联动:额度占用 / 余额核销 ================= */
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void occupyCredit(Long customerId, String currency, BigDecimal amount)
|
||||
{
|
||||
if (customerId == null || amount == null || amount.compareTo(BigDecimal.ZERO) <= 0)
|
||||
{
|
||||
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()).add(amount).setScale(MONEY_SCALE, RoundingMode.HALF_UP));
|
||||
account.setUpdateBy(SecurityUtils.getUsername());
|
||||
if (customerMoneyMapper.updateAccountBalance(account) == 0)
|
||||
{
|
||||
throw new ServiceException("账户已被其他操作变动,请刷新后重试");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void releaseCredit(Long customerId, String currency, BigDecimal amount)
|
||||
{
|
||||
if (customerId == null || amount == null || amount.compareTo(BigDecimal.ZERO) <= 0)
|
||||
{
|
||||
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不能为空");
|
||||
}
|
||||
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0)
|
||||
{
|
||||
throw new ServiceException("核销金额必须大于0");
|
||||
}
|
||||
String ccny = StringUtils.hasText(currency) ? currency.trim().toUpperCase() : BASE_CURRENCY;
|
||||
String username = SecurityUtils.getUsername();
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
CustomerAccount account = lockAccountForBill(customerId, ccny, username);
|
||||
if (account == null)
|
||||
{
|
||||
throw new ServiceException("客户没有" + ccny + "账户,无法用余额核销");
|
||||
}
|
||||
|
||||
BigDecimal beforeBalance = nz(account.getBalance());
|
||||
if (beforeBalance.compareTo(amount) < 0)
|
||||
{
|
||||
throw new ServiceException("账户余额不足,当前" + ccny + "余额:" + beforeBalance.toPlainString());
|
||||
}
|
||||
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,
|
||||
beforeBalance, afterBalance, frozenBalance, frozenBalance,
|
||||
bizId, bizNo, username, userId, remark);
|
||||
customerMoneyMapper.insertFlow(flow);
|
||||
return flow;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public CustomerBalanceFlow reverseBillPay(Long customerId, String currency, BigDecimal amount,
|
||||
Long bizId, String bizNo, String remark)
|
||||
{
|
||||
if (customerId == null || amount == null || amount.compareTo(BigDecimal.ZERO) <= 0)
|
||||
{
|
||||
throw new ServiceException("反核销金额不正确");
|
||||
}
|
||||
String ccny = StringUtils.hasText(currency) ? currency.trim().toUpperCase() : BASE_CURRENCY;
|
||||
String username = SecurityUtils.getUsername();
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
CustomerAccount account = lockAccountForBill(customerId, ccny, username);
|
||||
if (account == null)
|
||||
{
|
||||
throw new ServiceException("客户" + ccny + "账户不存在,无法反核销");
|
||||
}
|
||||
|
||||
BigDecimal beforeBalance = nz(account.getBalance());
|
||||
BigDecimal afterBalance = beforeBalance.add(amount);
|
||||
BigDecimal frozenBalance = nz(account.getFrozenBalance());
|
||||
// 账单恢复为未核销,授信占用同步加回(允许与存量数据一样出现超出总额度的占用状态)
|
||||
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("账户余额已被其他操作变动,请刷新后重试");
|
||||
}
|
||||
|
||||
CustomerBalanceFlow flow = newBalanceFlow(account, ccny, amount, 1,
|
||||
beforeBalance, afterBalance, frozenBalance, frozenBalance,
|
||||
bizId, bizNo, username, userId, remark);
|
||||
customerMoneyMapper.insertFlow(flow);
|
||||
return flow;
|
||||
}
|
||||
|
||||
/** 账单联动场景锁定账户:CNY不存在则初始化(先处理临时额度过期),外币账户不存在返回null */
|
||||
private CustomerAccount lockAccountForBill(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 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
|
||||
* (客户档案已填信用额度且大于0时按档案值),并写 INIT 流水;已存在则跳过。
|
||||
*/
|
||||
@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;
|
||||
}
|
||||
|
||||
/** 行锁获取客户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)
|
||||
{
|
||||
return account != null
|
||||
&& nz(account.getExtraCredit()).compareTo(BigDecimal.ZERO) > 0
|
||||
&& account.getExtraExpireDate() != null
|
||||
&& 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)
|
||||
{
|
||||
return date.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate();
|
||||
}
|
||||
|
||||
private Date toSqlDate(LocalDate localDate)
|
||||
{
|
||||
return java.sql.Date.valueOf(localDate);
|
||||
}
|
||||
|
||||
/* ================= 私有方法 ================= */
|
||||
|
||||
/** 保存收款附件到客户附件表 */
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<?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.ArBalanceWriteoffMapper">
|
||||
|
||||
<resultMap id="ArBalanceWriteoffResult" type="ArBalanceWriteoff">
|
||||
<id property="writeoffId" column="writeoff_id"/>
|
||||
<result property="writeoffNo" column="writeoff_no"/>
|
||||
<result property="arBillId" column="ar_bill_id"/>
|
||||
<result property="billNo" column="bill_no"/>
|
||||
<result property="customerId" column="customer_id"/>
|
||||
<result property="accountId" column="account_id"/>
|
||||
<result property="currency" column="currency"/>
|
||||
<result property="writeoffAmount" column="writeoff_amount"/>
|
||||
<result property="balanceFlowId" column="balance_flow_id"/>
|
||||
<result property="reverseFlowId" column="reverse_flow_id"/>
|
||||
<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"/>
|
||||
<result property="cancelRemark" column="cancel_remark"/>
|
||||
<result property="billName" column="bill_name"/>
|
||||
<result property="customerName" column="customer_name"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectColumns">
|
||||
select w.writeoff_id, w.writeoff_no, w.ar_bill_id, w.bill_no, w.customer_id, w.account_id,
|
||||
w.currency, w.writeoff_amount, w.balance_flow_id, w.reverse_flow_id,
|
||||
w.writeoff_date, w.status, w.remark, w.create_by, w.create_time,
|
||||
w.cancel_by, w.cancel_time, w.cancel_remark,
|
||||
b.bill_name, b.customer_name
|
||||
from ar_balance_writeoff w
|
||||
left join ar_bill b on b.ar_bill_id = w.ar_bill_id
|
||||
</sql>
|
||||
|
||||
<insert id="insertArBalanceWriteoff" parameterType="ArBalanceWriteoff"
|
||||
useGeneratedKeys="true" keyProperty="writeoffId">
|
||||
insert into ar_balance_writeoff (
|
||||
writeoff_no, ar_bill_id, bill_no, customer_id, account_id, currency,
|
||||
writeoff_amount, balance_flow_id, reverse_flow_id, writeoff_date,
|
||||
status, remark, create_by, create_time
|
||||
) values (
|
||||
#{writeoffNo}, #{arBillId}, #{billNo}, #{customerId}, #{accountId}, #{currency},
|
||||
#{writeoffAmount}, #{balanceFlowId}, #{reverseFlowId}, #{writeoffDate},
|
||||
#{status}, #{remark}, #{createBy}, sysdate()
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="updateFlowRefs">
|
||||
update ar_balance_writeoff
|
||||
set account_id = #{accountId},
|
||||
balance_flow_id = #{balanceFlowId}
|
||||
where writeoff_id = #{writeoffId}
|
||||
</update>
|
||||
|
||||
<select id="selectByIdForUpdate" resultMap="ArBalanceWriteoffResult">
|
||||
<include refid="selectColumns"/>
|
||||
where w.writeoff_id = #{writeoffId}
|
||||
for update
|
||||
</select>
|
||||
|
||||
<update id="updateCancelled">
|
||||
update ar_balance_writeoff
|
||||
set status = 'CANCELLED',
|
||||
reverse_flow_id = #{reverseFlowId},
|
||||
cancel_by = #{cancelBy},
|
||||
cancel_time = sysdate(),
|
||||
cancel_remark = #{cancelRemark}
|
||||
where writeoff_id = #{writeoffId} and status = 'EFFECTIVE'
|
||||
</update>
|
||||
|
||||
<select id="selectWriteoffList" parameterType="ArBalanceWriteoff" resultMap="ArBalanceWriteoffResult">
|
||||
<include refid="selectColumns"/>
|
||||
<where>
|
||||
<if test="customerId != null">and w.customer_id = #{customerId}</if>
|
||||
<if test="arBillId != null">and w.ar_bill_id = #{arBillId}</if>
|
||||
<if test="status != null and status != ''">and w.status = #{status}</if>
|
||||
<if test="currency != null and currency != ''">and w.currency = #{currency}</if>
|
||||
<if test="writeoffNo != null and writeoffNo != ''">and w.writeoff_no like concat('%', #{writeoffNo}, '%')</if>
|
||||
<if test="billNo != null and billNo != ''">and w.bill_no like concat('%', #{billNo}, '%')</if>
|
||||
</where>
|
||||
order by w.create_time desc, w.writeoff_id desc
|
||||
</select>
|
||||
</mapper>
|
||||
@@ -6,7 +6,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<resultMap id="ArBillDetailFeeResult" type="ArBillDetailFee">
|
||||
<id property="detailFeeId" column="detail_fee_id"/>
|
||||
<result property="detailId" column="detail_id"/>
|
||||
<result property="prcFeeId" column="prc_fee_id"/>
|
||||
<result property="prcIncomeBillFeeId" column="prc_income_bill_fee_id"/>
|
||||
<result property="feeNo" column="fee_no"/>
|
||||
<result property="feeType" column="fee_type"/>
|
||||
<result property="feeName" column="fee_name"/>
|
||||
@@ -24,13 +24,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</resultMap>
|
||||
|
||||
<select id="selectByDetailId" parameterType="long" resultMap="ArBillDetailFeeResult">
|
||||
select detail_fee_id, detail_id, prc_fee_id, fee_no, fee_type, fee_name, sc_id, quantity,
|
||||
select detail_fee_id, detail_id, prc_income_bill_fee_id, fee_no, fee_type, fee_name, sc_id, quantity,
|
||||
unit, unit_price, amount, currency, calculation_formula, round_mode, remark, create_by, create_time
|
||||
from ar_bill_detail_fee where detail_id = #{detailId} order by fee_no, detail_fee_id
|
||||
</select>
|
||||
|
||||
<select id="selectByBillId" parameterType="long" resultMap="ArBillDetailFeeResult">
|
||||
select detail_fee_id, detail_id, prc_fee_id, fee_no, fee_type, fee_name, sc_id, quantity,
|
||||
select detail_fee_id, detail_id, prc_income_bill_fee_id, fee_no, fee_type, fee_name, sc_id, quantity,
|
||||
unit, unit_price, amount, currency, calculation_formula, round_mode, remark, create_by, create_time
|
||||
from ar_bill_detail_fee
|
||||
where detail_id in (select detail_id from ar_bill_detail where ar_bill_id = #{arBillId})
|
||||
@@ -39,10 +39,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
|
||||
<insert id="insert" parameterType="ArBillDetailFee" useGeneratedKeys="true" keyProperty="detailFeeId">
|
||||
insert into ar_bill_detail_fee (
|
||||
detail_id, prc_fee_id, fee_no, fee_type, fee_name, sc_id, quantity, unit, unit_price,
|
||||
detail_id, prc_income_bill_fee_id, fee_no, fee_type, fee_name, sc_id, quantity, unit, unit_price,
|
||||
amount, currency, calculation_formula, round_mode, remark, create_by, create_time
|
||||
) values (
|
||||
#{detailId}, #{prcFeeId}, #{feeNo}, #{feeType}, #{feeName}, #{scId}, #{quantity}, #{unit},
|
||||
#{detailId}, #{prcIncomeBillFeeId}, #{feeNo}, #{feeType}, #{feeName}, #{scId}, #{quantity}, #{unit},
|
||||
#{unitPrice}, #{amount}, #{currency}, #{calculationFormula}, #{roundMode}, #{remark},
|
||||
#{createBy}, sysdate()
|
||||
)
|
||||
@@ -50,11 +50,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
|
||||
<insert id="insertBatch" parameterType="java.util.List" useGeneratedKeys="true" keyProperty="detailFeeId">
|
||||
insert into ar_bill_detail_fee (
|
||||
detail_id, prc_fee_id, fee_no, fee_type, fee_name, sc_id, quantity, unit, unit_price,
|
||||
detail_id, prc_income_bill_fee_id, fee_no, fee_type, fee_name, sc_id, quantity, unit, unit_price,
|
||||
amount, currency, calculation_formula, round_mode, remark, create_by, create_time
|
||||
) values
|
||||
<foreach collection="list" item="item" separator=",">
|
||||
(#{item.detailId}, #{item.prcFeeId}, #{item.feeNo}, #{item.feeType}, #{item.feeName}, #{item.scId},
|
||||
(#{item.detailId}, #{item.prcIncomeBillFeeId}, #{item.feeNo}, #{item.feeType}, #{item.feeName}, #{item.scId},
|
||||
#{item.quantity}, #{item.unit}, #{item.unitPrice}, #{item.amount}, #{item.currency},
|
||||
#{item.calculationFormula}, #{item.roundMode}, #{item.remark}, #{item.createBy}, sysdate())
|
||||
</foreach>
|
||||
|
||||
@@ -6,7 +6,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<resultMap id="ArBillDetailResult" type="ArBillDetail">
|
||||
<id property="detailId" column="detail_id"/>
|
||||
<result property="arBillId" column="ar_bill_id"/>
|
||||
<result property="prcBillId" column="prc_bill_id"/>
|
||||
<result property="prcOrderBillId" column="prc_order_bill_id"/>
|
||||
<result property="detailNo" column="detail_no"/>
|
||||
<result property="orderNo" column="order_no"/>
|
||||
<result property="trackingNo" column="tracking_no"/>
|
||||
@@ -31,7 +31,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</resultMap>
|
||||
|
||||
<select id="selectByBillId" parameterType="long" resultMap="ArBillDetailResult">
|
||||
select detail_id, ar_bill_id, prc_bill_id, detail_no, order_no, tracking_no, receive_time,
|
||||
select detail_id, ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, receive_time,
|
||||
product_code, product_name, goods_description, country_code, country_name, dest_postcode,
|
||||
actual_weight, volume_weight, charge_weight, pkg_length, pkg_width, pkg_height,
|
||||
currency, total_amount, remark, create_by, create_time
|
||||
@@ -42,7 +42,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<collection property="fees" ofType="ArBillDetailFee">
|
||||
<id property="detailFeeId" column="fee_detail_id"/>
|
||||
<result property="detailId" column="fee_parent_detail_id"/>
|
||||
<result property="prcFeeId" column="fee_prc_fee_id"/>
|
||||
<result property="prcIncomeBillFeeId" column="fee_prc_income_bill_fee_id"/>
|
||||
<result property="feeNo" column="fee_no"/>
|
||||
<result property="feeType" column="fee_type"/>
|
||||
<result property="feeName" column="fee_name"/>
|
||||
@@ -61,11 +61,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</resultMap>
|
||||
|
||||
<select id="selectByBillIdWithFees" parameterType="long" resultMap="ArBillDetailWithFeesResult">
|
||||
select d.detail_id, d.ar_bill_id, d.prc_bill_id, d.detail_no, d.order_no, d.tracking_no, d.receive_time,
|
||||
select d.detail_id, d.ar_bill_id, d.prc_order_bill_id, d.detail_no, d.order_no, d.tracking_no, d.receive_time,
|
||||
d.product_code, d.product_name, d.goods_description, d.country_code, d.country_name, d.dest_postcode,
|
||||
d.actual_weight, d.volume_weight, d.charge_weight, d.pkg_length, d.pkg_width, d.pkg_height,
|
||||
d.currency, d.total_amount, d.remark, d.create_by, d.create_time,
|
||||
f.detail_fee_id as fee_detail_id, f.detail_id as fee_parent_detail_id, f.prc_fee_id as fee_prc_fee_id,
|
||||
f.detail_fee_id as fee_detail_id, f.detail_id as fee_parent_detail_id,
|
||||
f.prc_income_bill_fee_id as fee_prc_income_bill_fee_id,
|
||||
f.fee_no, f.fee_type, f.fee_name, f.sc_id as fee_sc_id, f.quantity as fee_quantity,
|
||||
f.unit as fee_unit, f.unit_price as fee_unit_price, f.amount as fee_amount,
|
||||
f.currency as fee_currency, f.calculation_formula as fee_calculation_formula,
|
||||
@@ -79,12 +80,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
|
||||
<insert id="insert" parameterType="ArBillDetail" useGeneratedKeys="true" keyProperty="detailId">
|
||||
insert into ar_bill_detail (
|
||||
ar_bill_id, prc_bill_id, detail_no, order_no, tracking_no, receive_time, product_code,
|
||||
ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, receive_time, product_code,
|
||||
product_name, goods_description, country_code, country_name, dest_postcode, actual_weight,
|
||||
volume_weight, charge_weight, pkg_length, pkg_width, pkg_height, currency, total_amount,
|
||||
remark, create_by, create_time
|
||||
) values (
|
||||
#{arBillId}, #{prcBillId}, #{detailNo}, #{orderNo}, #{trackingNo}, #{receiveTime},
|
||||
#{arBillId}, #{prcOrderBillId}, #{detailNo}, #{orderNo}, #{trackingNo}, #{receiveTime},
|
||||
#{productCode}, #{productName}, #{goodsDescription}, #{countryCode}, #{countryName},
|
||||
#{destPostcode}, #{actualWeight}, #{volumeWeight}, #{chargeWeight}, #{pkgLength}, #{pkgWidth},
|
||||
#{pkgHeight}, #{currency}, #{totalAmount}, #{remark}, #{createBy}, sysdate()
|
||||
@@ -93,13 +94,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
|
||||
<insert id="insertBatch" parameterType="java.util.List" useGeneratedKeys="true" keyProperty="detailId">
|
||||
insert into ar_bill_detail (
|
||||
ar_bill_id, prc_bill_id, detail_no, order_no, tracking_no, receive_time, product_code,
|
||||
ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, receive_time, product_code,
|
||||
product_name, goods_description, country_code, country_name, dest_postcode, actual_weight,
|
||||
volume_weight, charge_weight, pkg_length, pkg_width, pkg_height, currency, total_amount,
|
||||
remark, create_by, create_time
|
||||
) values
|
||||
<foreach collection="list" item="item" separator=",">
|
||||
(#{item.arBillId}, #{item.prcBillId}, #{item.detailNo}, #{item.orderNo}, #{item.trackingNo},
|
||||
(#{item.arBillId}, #{item.prcOrderBillId}, #{item.detailNo}, #{item.orderNo}, #{item.trackingNo},
|
||||
#{item.receiveTime}, #{item.productCode}, #{item.productName}, #{item.goodsDescription},
|
||||
#{item.countryCode}, #{item.countryName}, #{item.destPostcode}, #{item.actualWeight},
|
||||
#{item.volumeWeight}, #{item.chargeWeight}, #{item.pkgLength}, #{item.pkgWidth}, #{item.pkgHeight},
|
||||
|
||||
@@ -41,7 +41,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
from ar_bill
|
||||
</sql>
|
||||
|
||||
<select id="selectArBillList" parameterType="ArBill" resultMap="ArBillResult">
|
||||
<select id="selectArBillList" parameterType="com.ruoyi.ar.domain.vo.ArBillQuery" resultMap="ArBillResult">
|
||||
<include refid="selectArBillColumns"/>
|
||||
<where>
|
||||
<if test="billNo != null and billNo != ''">and bill_no like concat('%', #{billNo}, '%')</if>
|
||||
@@ -51,8 +51,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="customerId != null">and customer_id = #{customerId}</if>
|
||||
<if test="status != null and status != ''">and status = #{status}</if>
|
||||
<if test="currency != null and currency != ''">and currency = #{currency}</if>
|
||||
<if test="billPeriodStart != null">and bill_period_end >= #{billPeriodStart}</if>
|
||||
<if test="billPeriodEnd != null">and bill_period_start <= #{billPeriodEnd}</if>
|
||||
<if test="billDateStart != null">and bill_date >= #{billDateStart}</if>
|
||||
<if test="billDateEnd != null">and bill_date <= #{billDateEnd}</if>
|
||||
<if test="billPeriodEnd != null">and bill_period_end = #{billPeriodEnd}</if>
|
||||
</where>
|
||||
order by create_time desc, ar_bill_id desc
|
||||
</select>
|
||||
@@ -80,6 +81,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<include refid="selectArBillColumns"/> where ar_bill_id = #{arBillId} for update
|
||||
</select>
|
||||
|
||||
<select id="selectUnsettledByCustomer" resultMap="ArBillResult">
|
||||
<include refid="selectArBillColumns"/>
|
||||
where customer_id = #{customerId}
|
||||
and status in ('CONFIRMED', 'PARTIAL_WRITTEN_OFF')
|
||||
and unwritten_amount > 0
|
||||
order by bill_date asc, ar_bill_id asc
|
||||
</select>
|
||||
|
||||
<insert id="insertArBill" parameterType="ArBill" useGeneratedKeys="true" keyProperty="arBillId">
|
||||
insert into ar_bill (
|
||||
bill_no, bill_name, customer_id, customer_code, customer_name, bill_period_start, bill_period_end,
|
||||
|
||||
@@ -198,6 +198,25 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
and (biz_type = 'customer' or biz_type is null or biz_type = '')
|
||||
</update>
|
||||
|
||||
<!-- 额度调整后回写客户档案额度字段(数据源为客户余额账户) -->
|
||||
<update id="updateCustomerCredit">
|
||||
update bms_customer
|
||||
set credit_ed = #{creditEd},
|
||||
temp_ed = #{tempEd},
|
||||
update_by = #{updateBy},
|
||||
update_time = now()
|
||||
where customer_id = #{customerId}
|
||||
</update>
|
||||
|
||||
<!-- 只取档案物理额度列(列表查询里的 credit_ed 是账户可用额度子查询) -->
|
||||
<select id="selectRawCreditByCustomerId" resultType="BmsCustomer">
|
||||
select customer_id as customerId,
|
||||
credit_ed as creditEd,
|
||||
temp_ed as tempEd
|
||||
from bms_customer
|
||||
where customer_id = #{customerId}
|
||||
</select>
|
||||
|
||||
<delete id="deleteBmsCustomerAttachmentByAttachmentIds" parameterType="String">
|
||||
update bms_customer_attachment set del_flag = '2' where attachment_id in
|
||||
<foreach item="attachmentId" collection="attachmentIds" open="(" separator="," close=")">
|
||||
@@ -287,12 +306,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
service_user_id = #{serviceUserId},
|
||||
service_user = #{serviceUser},
|
||||
<if test="settlementType != null">settlement_type = #{settlementType},</if>
|
||||
<if test="tempEd != null">temp_ed = #{tempEd},</if>
|
||||
<if test="accountBalance != null">account_balance = #{accountBalance},</if>
|
||||
<if test="foreignCurrencyBalance != null">foreign_currency_balance = #{foreignCurrencyBalance},</if>
|
||||
<if test="weightThreshold != null">weight_threshold = #{weightThreshold},</if>
|
||||
<if test="throwRatio != null">throw_ratio = #{throwRatio},</if>
|
||||
<if test="creditEd != null">credit_ed = #{creditEd},</if>
|
||||
<if test="productIds != null">product_ids = #{productIds},</if>
|
||||
<if test="remark != null">remark = #{remark},</if>
|
||||
<if test="createBy != null">create_by = #{createBy},</if>
|
||||
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
<?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.customerinfo.mapper.BmsCustomerPriceSetMapper">
|
||||
|
||||
<resultMap type="com.ruoyi.customer.customerinfo.domain.BmsCustomerPriceSet" id="BmsCustomerPriceSetResult">
|
||||
<id property="customerPricesetId" column="customer_priceset_id" />
|
||||
<result property="priceSetId" column="price_set_id" />
|
||||
<result property="customerId" column="customer_id" />
|
||||
<result property="customerCode" column="customer_code" />
|
||||
<result property="customerName" column="customer_name" />
|
||||
<result property="bindPriority" column="bind_priority" />
|
||||
<result property="prBegindate" column="pr_begindate" />
|
||||
<result property="prEnddate" column="pr_enddate" />
|
||||
<result property="priceSetNote" column="price_set_note" />
|
||||
<result property="createBy" column="created_by" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="updateBy" column="updated_by" />
|
||||
<result property="updateTime" column="update_time" />
|
||||
<result property="priceSetCode" column="price_set_code" />
|
||||
<result property="priceSetName" column="price_set_name" />
|
||||
<result property="setType" column="set_type" />
|
||||
<result property="priceSetStatus" column="price_set_status" />
|
||||
<result property="setEffectiveDate" column="set_effective_date" />
|
||||
<result property="setExpireDate" column="set_expire_date" />
|
||||
</resultMap>
|
||||
|
||||
<resultMap type="com.ruoyi.customer.customerinfo.domain.BmsPriceSetOption" id="BmsPriceSetOptionResult">
|
||||
<id property="priceSetId" column="price_set_id" />
|
||||
<result property="priceSetCode" column="price_set_code" />
|
||||
<result property="priceSetName" column="price_set_name" />
|
||||
<result property="setType" column="set_type" />
|
||||
<result property="status" column="status" />
|
||||
<result property="effectiveDate" column="effective_date" />
|
||||
<result property="expireDate" column="expire_date" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="bindVo">
|
||||
select c.customer_priceset_id, c.price_set_id, c.customer_id, c.customer_code, c.customer_name,
|
||||
c.bind_priority, c.pr_begindate, c.pr_enddate, c.price_set_note,
|
||||
c.created_by, c.create_time, c.updated_by, c.update_time,
|
||||
s.price_set_code, s.price_set_name, s.set_type,
|
||||
s.status as price_set_status,
|
||||
s.effective_date as set_effective_date, s.expire_date as set_expire_date
|
||||
from prc_customer_price_set c
|
||||
left join prc_price_set s on s.price_set_id = c.price_set_id
|
||||
</sql>
|
||||
|
||||
<select id="selectBindListByCustomerId" resultMap="BmsCustomerPriceSetResult">
|
||||
<include refid="bindVo"/>
|
||||
where c.customer_id = #{customerId}
|
||||
order by c.bind_priority asc, c.customer_priceset_id asc
|
||||
</select>
|
||||
|
||||
<select id="selectBindById" parameterType="Long" resultMap="BmsCustomerPriceSetResult">
|
||||
<include refid="bindVo"/>
|
||||
where c.customer_priceset_id = #{customerPricesetId}
|
||||
</select>
|
||||
|
||||
<select id="selectBindByCustomerId" resultMap="BmsCustomerPriceSetResult">
|
||||
select customer_priceset_id, price_set_id, customer_id, customer_code, customer_name,
|
||||
bind_priority, pr_begindate, pr_enddate, price_set_note,
|
||||
created_by, create_time, updated_by, update_time
|
||||
from prc_customer_price_set
|
||||
where customer_id = #{customerId}
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<insert id="insertBind" parameterType="com.ruoyi.customer.customerinfo.domain.BmsCustomerPriceSet"
|
||||
useGeneratedKeys="true" keyProperty="customerPricesetId">
|
||||
insert into prc_customer_price_set (
|
||||
price_set_id, customer_id, customer_code, customer_name,
|
||||
bind_priority, pr_begindate, pr_enddate, price_set_note,
|
||||
created_by, create_time
|
||||
) values (
|
||||
#{priceSetId}, #{customerId}, #{customerCode}, #{customerName},
|
||||
#{bindPriority}, #{prBegindate}, #{prEnddate}, #{priceSetNote},
|
||||
#{createBy}, sysdate()
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="updateBind" parameterType="com.ruoyi.customer.customerinfo.domain.BmsCustomerPriceSet">
|
||||
update prc_customer_price_set
|
||||
set price_set_id = #{priceSetId},
|
||||
customer_code = #{customerCode},
|
||||
customer_name = #{customerName},
|
||||
bind_priority = #{bindPriority},
|
||||
pr_begindate = #{prBegindate},
|
||||
pr_enddate = #{prEnddate},
|
||||
price_set_note = #{priceSetNote},
|
||||
updated_by = #{updateBy},
|
||||
update_time = sysdate()
|
||||
where customer_priceset_id = #{customerPricesetId}
|
||||
</update>
|
||||
|
||||
<delete id="deleteBindByIds" parameterType="Long">
|
||||
delete from prc_customer_price_set where customer_priceset_id in
|
||||
<foreach item="id" collection="array" open="(" separator="," close=")">
|
||||
#{id}
|
||||
</foreach>
|
||||
</delete>
|
||||
|
||||
<select id="selectPriceSetOptionList"
|
||||
parameterType="com.ruoyi.customer.customerinfo.domain.BmsPriceSetOption"
|
||||
resultMap="BmsPriceSetOptionResult">
|
||||
select price_set_id, price_set_code, price_set_name, set_type, status, effective_date, expire_date
|
||||
from prc_price_set
|
||||
<where>
|
||||
status = 'E'
|
||||
<if test="setType != null and setType != ''">
|
||||
and set_type = #{setType}
|
||||
</if>
|
||||
<if test="keyword != null and keyword != ''">
|
||||
and (price_set_code like concat('%', #{keyword}, '%')
|
||||
or price_set_name like concat('%', #{keyword}, '%'))
|
||||
</if>
|
||||
</where>
|
||||
order by price_set_id desc
|
||||
</select>
|
||||
|
||||
<select id="selectPriceSetOptionById" resultMap="BmsPriceSetOptionResult">
|
||||
select price_set_id, price_set_code, price_set_name, set_type, status, effective_date, expire_date
|
||||
from prc_price_set
|
||||
where price_set_id = #{priceSetId}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -13,6 +13,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="frozenBalance" column="frozen_balance" />
|
||||
<result property="totalCredit" column="total_credit" />
|
||||
<result property="extraCredit" column="extra_credit" />
|
||||
<result property="extraEffectiveDate" column="extra_effective_date" />
|
||||
<result property="extraExpireDate" column="extra_expire_date" />
|
||||
<result property="usedCredit" column="used_credit" />
|
||||
<result property="tax" column="tax" />
|
||||
<result property="version" column="version" />
|
||||
@@ -25,7 +27,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectCustomerAccountVo">
|
||||
select id, customer_id, currency, balance, frozen_balance, total_credit, extra_credit, used_credit,
|
||||
select id, customer_id, currency, balance, frozen_balance, total_credit, extra_credit,
|
||||
extra_effective_date, extra_expire_date, used_credit,
|
||||
tax, version, create_by, create_time, update_by, update_time, remark, del_flag
|
||||
from customer_account
|
||||
</sql>
|
||||
@@ -90,6 +93,112 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
where id = #{id} and version = #{version} and del_flag = '0'
|
||||
</update>
|
||||
|
||||
<!-- 乐观锁更新额度(信用/临时额度及临时额度有效期) -->
|
||||
<update id="updateAccountCredit">
|
||||
update customer_account
|
||||
set total_credit = #{totalCredit},
|
||||
extra_credit = #{extraCredit},
|
||||
extra_effective_date = #{extraEffectiveDate},
|
||||
extra_expire_date = #{extraExpireDate},
|
||||
version = version + 1,
|
||||
update_by = #{updateBy},
|
||||
update_time = now()
|
||||
where id = #{id} and version = #{version} and del_flag = '0'
|
||||
</update>
|
||||
|
||||
<!-- ================= 客户额度流水 ================= -->
|
||||
<resultMap type="com.ruoyi.customer.customermoney.domain.CustomerCreditFlow" id="CustomerCreditFlowResult">
|
||||
<id property="id" column="id" />
|
||||
<result property="flowNo" column="flow_no" />
|
||||
<result property="customerId" column="customer_id" />
|
||||
<result property="accountId" column="account_id" />
|
||||
<result property="creditType" column="credit_type" />
|
||||
<result property="changeType" column="change_type" />
|
||||
<result property="direction" column="direction" />
|
||||
<result property="beforeAmount" column="before_amount" />
|
||||
<result property="changeAmount" column="change_amount" />
|
||||
<result property="afterAmount" column="after_amount" />
|
||||
<result property="usedCredit" column="used_credit" />
|
||||
<result property="effectiveDate" column="effective_date" />
|
||||
<result property="expireDate" column="expire_date" />
|
||||
<result property="reason" column="reason" />
|
||||
<result property="adjustMode" column="adjust_mode" />
|
||||
<result property="operatorId" column="operator_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" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectCustomerCreditFlowVo">
|
||||
select id, flow_no, customer_id, account_id, credit_type, change_type, direction,
|
||||
before_amount, change_amount, after_amount, used_credit,
|
||||
effective_date, expire_date, reason, adjust_mode, operator_id,
|
||||
create_by, create_time, update_by, update_time, remark, del_flag
|
||||
from customer_credit_flow
|
||||
</sql>
|
||||
|
||||
<select id="selectCreditFlowList" parameterType="com.ruoyi.customer.customermoney.domain.CustomerCreditFlow"
|
||||
resultMap="CustomerCreditFlowResult">
|
||||
<include refid="selectCustomerCreditFlowVo"/>
|
||||
<where>
|
||||
<if test="customerId != null"> and customer_id = #{customerId}</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="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>
|
||||
and del_flag = '0'
|
||||
</where>
|
||||
order by create_time desc, id desc
|
||||
</select>
|
||||
|
||||
<insert id="insertCreditFlow" parameterType="com.ruoyi.customer.customermoney.domain.CustomerCreditFlow"
|
||||
useGeneratedKeys="true" keyProperty="id">
|
||||
insert into customer_credit_flow
|
||||
<trim prefix="(" suffix=")" suffixOverrides=",">
|
||||
<if test="flowNo != null">flow_no,</if>
|
||||
<if test="customerId != null">customer_id,</if>
|
||||
<if test="accountId != null">account_id,</if>
|
||||
<if test="creditType != null">credit_type,</if>
|
||||
<if test="changeType != null">change_type,</if>
|
||||
<if test="direction != null">direction,</if>
|
||||
<if test="beforeAmount != null">before_amount,</if>
|
||||
<if test="changeAmount != null">change_amount,</if>
|
||||
<if test="afterAmount != null">after_amount,</if>
|
||||
<if test="usedCredit != null">used_credit,</if>
|
||||
<if test="effectiveDate != null">effective_date,</if>
|
||||
<if test="expireDate != null">expire_date,</if>
|
||||
<if test="reason != null">reason,</if>
|
||||
<if test="adjustMode != null">adjust_mode,</if>
|
||||
<if test="operatorId != null">operator_id,</if>
|
||||
<if test="createBy != null">create_by,</if>
|
||||
<if test="remark != null">remark,</if>
|
||||
<if test="delFlag != null">del_flag,</if>
|
||||
</trim>
|
||||
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
||||
<if test="flowNo != null">#{flowNo},</if>
|
||||
<if test="customerId != null">#{customerId},</if>
|
||||
<if test="accountId != null">#{accountId},</if>
|
||||
<if test="creditType != null">#{creditType},</if>
|
||||
<if test="changeType != null">#{changeType},</if>
|
||||
<if test="direction != null">#{direction},</if>
|
||||
<if test="beforeAmount != null">#{beforeAmount},</if>
|
||||
<if test="changeAmount != null">#{changeAmount},</if>
|
||||
<if test="afterAmount != null">#{afterAmount},</if>
|
||||
<if test="usedCredit != null">#{usedCredit},</if>
|
||||
<if test="effectiveDate != null">#{effectiveDate},</if>
|
||||
<if test="expireDate != null">#{expireDate},</if>
|
||||
<if test="reason != null">#{reason},</if>
|
||||
<if test="adjustMode != null">#{adjustMode},</if>
|
||||
<if test="operatorId != null">#{operatorId},</if>
|
||||
<if test="createBy != null">#{createBy},</if>
|
||||
<if test="remark != null">#{remark},</if>
|
||||
<if test="delFlag != null">#{delFlag},</if>
|
||||
</trim>
|
||||
</insert>
|
||||
|
||||
<!-- ================= 客户收款 ================= -->
|
||||
<resultMap type="com.ruoyi.customer.customermoney.domain.CustomerReceipt" id="CustomerReceiptResult">
|
||||
<id property="id" column="id" />
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
-- =====================================================================
|
||||
-- 客户余额核销应收账单 V1.0 2026-09-16
|
||||
-- 1) 余额核销单表 ar_balance_writeoff(资金来源:customer_account.balance)
|
||||
-- 2) 存量账单未核销金额重算 customer_account.used_credit 占用
|
||||
-- 3) 菜单与授权:客户账单(2435) / 账单详情(2436) / 余额核销(2437) / 反核销(2438)
|
||||
-- =====================================================================
|
||||
|
||||
-- 余额核销单:用客户账户余额核销应收账单(与 ar_receipt_writeoff 收款核销并列,
|
||||
-- 两者共享 ar_bill.written_off_amount / unwritten_amount 缓存字段)
|
||||
CREATE TABLE IF NOT EXISTS ar_balance_writeoff (
|
||||
writeoff_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '余额核销ID',
|
||||
writeoff_no VARCHAR(50) NOT NULL COMMENT '核销单号(BWO开头)',
|
||||
ar_bill_id BIGINT(20) NOT NULL COMMENT '应收账单ID',
|
||||
bill_no VARCHAR(50) NOT NULL COMMENT '账单号快照',
|
||||
customer_id BIGINT(20) NOT NULL COMMENT '客户ID',
|
||||
account_id BIGINT(20) DEFAULT NULL COMMENT '客户账户ID',
|
||||
currency VARCHAR(10) NOT NULL DEFAULT 'CNY' COMMENT '币种',
|
||||
writeoff_amount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '本次核销金额',
|
||||
balance_flow_id BIGINT(20) DEFAULT NULL COMMENT '核销扣余额流水ID',
|
||||
reverse_flow_id BIGINT(20) DEFAULT NULL COMMENT '反核销回补余额流水ID',
|
||||
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 '反核销时间',
|
||||
cancel_remark VARCHAR(500) DEFAULT NULL COMMENT '反核销原因',
|
||||
PRIMARY KEY (writeoff_id),
|
||||
UNIQUE KEY uk_ar_balance_writeoff_no (writeoff_no),
|
||||
KEY idx_abw_bill (ar_bill_id, status),
|
||||
KEY idx_abw_customer (customer_id, status),
|
||||
KEY idx_abw_account (account_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='应收账单余额核销表';
|
||||
|
||||
-- 存量数据:为仅有账单、没有账户的客户补建账户(理论上CNY账户均已自动开户)
|
||||
INSERT INTO customer_account (customer_id, currency, balance, frozen_balance, total_credit, extra_credit, used_credit, tax, version, create_by, create_time, del_flag)
|
||||
SELECT b.customer_id, b.currency, 0, 0, 0, 0, 0, 0, 0, 'system', NOW(), '0'
|
||||
FROM (
|
||||
SELECT customer_id, currency FROM ar_bill
|
||||
WHERE status IN ('CONFIRMED','PARTIAL_WRITTEN_OFF')
|
||||
GROUP BY customer_id, currency
|
||||
) b
|
||||
LEFT JOIN customer_account a
|
||||
ON a.customer_id = b.customer_id
|
||||
AND a.currency COLLATE utf8mb4_general_ci = b.currency
|
||||
AND a.del_flag = '0'
|
||||
WHERE a.id IS NULL;
|
||||
|
||||
-- 已用额度 = 该客户该币种 已确认/部分核销账单的未核销金额合计
|
||||
UPDATE customer_account a
|
||||
JOIN (
|
||||
SELECT customer_id, currency, SUM(unwritten_amount) AS used_total
|
||||
FROM ar_bill
|
||||
WHERE status IN ('CONFIRMED','PARTIAL_WRITTEN_OFF')
|
||||
GROUP BY customer_id, currency
|
||||
) t ON t.customer_id = a.customer_id
|
||||
AND t.currency COLLATE utf8mb4_general_ci = a.currency
|
||||
SET a.used_credit = t.used_total,
|
||||
a.version = a.version + 1,
|
||||
a.update_by = 'system',
|
||||
a.update_time = NOW()
|
||||
WHERE a.del_flag = '0';
|
||||
|
||||
-- 菜单(父菜单 2309 = 客户/供应商管理;如环境ID不同请自行调整)
|
||||
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
VALUES
|
||||
(2435, '客户账单', 2309, 3, 'customerbill', 'customersuppliermanagement/customerinfo/customerbill/index', '', 1, 0, 'C', '0', '0', 'customer:bill:list', 'documentation', 'admin', NOW(), '客户应收账单只读查询'),
|
||||
(2436, '账单详情', 2435, 1, '', NULL, '', 1, 0, 'F', '0', '0', 'customer:bill:query', '#', 'admin', NOW(), ''),
|
||||
(2437, '余额核销', 2435, 2, '', NULL, '', 1, 0, 'F', '0', '0', 'customer:bill:writeoff', '#', 'admin', NOW(), '用客户账户余额核销应收账单'),
|
||||
(2438, '取消余额核销', 2435, 3, '', NULL, '', 1, 0, 'F', '0', '0', 'customer:bill:writeoff:cancel', '#', 'admin', NOW(), '反核销');
|
||||
|
||||
-- 普通角色授权(按实际角色调整)
|
||||
INSERT INTO sys_role_menu (role_id, menu_id) VALUES
|
||||
(2, 2435), (2, 2436), (2, 2437), (2, 2438);
|
||||
@@ -43,7 +43,7 @@ CREATE TABLE IF NOT EXISTS ar_bill (
|
||||
CREATE TABLE IF NOT EXISTS ar_bill_detail (
|
||||
detail_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '账单订单明细ID',
|
||||
ar_bill_id BIGINT(20) NOT NULL COMMENT '应收账单ID',
|
||||
prc_bill_id VARCHAR(36) NOT NULL COMMENT '来源订单计费ID,保持来源类型',
|
||||
prc_order_bill_id VARCHAR(36) NOT NULL COMMENT '来源计费订单ID,对应 prc_order_bill.bill_id',
|
||||
detail_no INT(11) NOT NULL COMMENT '账单内顺序号',
|
||||
order_no VARCHAR(64) NOT NULL COMMENT '订单号快照',
|
||||
tracking_no VARCHAR(64) DEFAULT NULL COMMENT '运单号快照',
|
||||
@@ -66,16 +66,16 @@ CREATE TABLE IF NOT EXISTS ar_bill_detail (
|
||||
create_by VARCHAR(64) NOT NULL COMMENT '创建人',
|
||||
create_time DATETIME NOT NULL COMMENT '创建时间',
|
||||
PRIMARY KEY (detail_id),
|
||||
UNIQUE KEY uk_ar_bill_detail_source (ar_bill_id, prc_bill_id),
|
||||
UNIQUE KEY uk_ar_bill_detail_source (ar_bill_id, prc_order_bill_id),
|
||||
UNIQUE KEY uk_ar_bill_detail_no (ar_bill_id, detail_no),
|
||||
KEY idx_ar_bill_detail_source (prc_bill_id),
|
||||
KEY idx_ar_bill_detail_source (prc_order_bill_id),
|
||||
CONSTRAINT fk_ar_bill_detail_bill FOREIGN KEY (ar_bill_id) REFERENCES ar_bill (ar_bill_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='应收账单订单快照';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ar_bill_detail_fee (
|
||||
detail_fee_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '账单费用明细ID',
|
||||
detail_id BIGINT(20) NOT NULL COMMENT '账单订单明细ID',
|
||||
prc_fee_id VARCHAR(36) NOT NULL COMMENT '来源费用ID,保持来源类型',
|
||||
prc_income_bill_fee_id VARCHAR(36) NOT NULL COMMENT '来源收入费用ID,对应 prc_income_bill_fee.fee_id',
|
||||
fee_no INT(11) NOT NULL COMMENT '订单内费用顺序号',
|
||||
fee_type VARCHAR(30) NOT NULL COMMENT '费用类型快照',
|
||||
fee_name VARCHAR(100) DEFAULT NULL COMMENT '费用名称快照',
|
||||
@@ -91,9 +91,9 @@ CREATE TABLE IF NOT EXISTS ar_bill_detail_fee (
|
||||
create_by VARCHAR(64) NOT NULL COMMENT '创建人',
|
||||
create_time DATETIME NOT NULL COMMENT '创建时间',
|
||||
PRIMARY KEY (detail_fee_id),
|
||||
UNIQUE KEY uk_ar_detail_prc_fee (detail_id, prc_fee_id),
|
||||
UNIQUE KEY uk_ar_detail_prc_income_fee (detail_id, prc_income_bill_fee_id),
|
||||
KEY idx_ar_detail_fee_type (fee_type),
|
||||
KEY idx_ar_detail_prc_fee (prc_fee_id),
|
||||
KEY idx_ar_detail_prc_income_fee (prc_income_bill_fee_id),
|
||||
KEY idx_ar_detail_fee_order (detail_id, fee_no, detail_fee_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='应收账单费用快照';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
-- 应收账单:订单占用状态 + 单日期截止日迁移
|
||||
-- 适用于已经执行过 ar/ar_bill.sql、price/bill.sql 的数据库。
|
||||
-- 若数据库中的应收明细仍使用旧来源ID列名,请先执行 ar_bill_source_id_rename.sql。
|
||||
-- 请在业务低峰期执行;本脚本只做字段、索引和历史状态初始化,不删除账单明细。
|
||||
SET NAMES utf8mb4;
|
||||
SET @db_name := DATABASE();
|
||||
@@ -60,11 +61,11 @@ DEALLOCATE PREPARE stmt;
|
||||
-- 3. 根据已有账单明细回填历史订单状态。作废账单不占用订单。
|
||||
UPDATE prc_order_bill o
|
||||
JOIN (
|
||||
SELECT DISTINCT d.prc_bill_id
|
||||
SELECT DISTINCT d.prc_order_bill_id
|
||||
FROM ar_bill_detail d
|
||||
JOIN ar_bill b ON b.ar_bill_id = d.ar_bill_id
|
||||
WHERE b.status <> 'VOID'
|
||||
) billed ON billed.prc_bill_id = o.bill_id
|
||||
) billed ON billed.prc_order_bill_id = o.bill_id
|
||||
SET o.ar_bill_status = 'BILLED'
|
||||
WHERE COALESCE(o.ar_bill_status, 'UNBILLED') <> 'BILLED';
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
-- 应收账单来源ID字段命名迁移
|
||||
--
|
||||
-- 将历史字段名迁移为与当前来源表一致的名称:
|
||||
-- ar_bill_detail.prc_bill_id -> prc_order_bill_id
|
||||
-- ar_bill_detail_fee.prc_fee_id -> prc_income_bill_fee_id
|
||||
-- 脚本可重复执行,已完成迁移的字段和索引会自动跳过。
|
||||
SET NAMES utf8mb4;
|
||||
SET @db_name := DATABASE();
|
||||
|
||||
-- 1. 应收账单订单明细:来源计费订单ID。
|
||||
SET @has_old_detail_column := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db_name
|
||||
AND TABLE_NAME = 'ar_bill_detail'
|
||||
AND COLUMN_NAME = 'prc_bill_id'
|
||||
);
|
||||
SET @has_new_detail_column := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db_name
|
||||
AND TABLE_NAME = 'ar_bill_detail'
|
||||
AND COLUMN_NAME = 'prc_order_bill_id'
|
||||
);
|
||||
SET @sql := IF(@has_old_detail_column > 0 AND @has_new_detail_column = 0,
|
||||
'ALTER TABLE ar_bill_detail CHANGE COLUMN prc_bill_id prc_order_bill_id VARCHAR(36) NOT NULL COMMENT ''来源计费订单ID,对应 prc_order_bill.bill_id''',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 2. 应收账单费用明细:来源收入费用ID。
|
||||
SET @has_old_fee_column := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db_name
|
||||
AND TABLE_NAME = 'ar_bill_detail_fee'
|
||||
AND COLUMN_NAME = 'prc_fee_id'
|
||||
);
|
||||
SET @has_new_fee_column := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db_name
|
||||
AND TABLE_NAME = 'ar_bill_detail_fee'
|
||||
AND COLUMN_NAME = 'prc_income_bill_fee_id'
|
||||
);
|
||||
SET @sql := IF(@has_old_fee_column > 0 AND @has_new_fee_column = 0,
|
||||
'ALTER TABLE ar_bill_detail_fee CHANGE COLUMN prc_fee_id prc_income_bill_fee_id VARCHAR(36) NOT NULL COMMENT ''来源收入费用ID,对应 prc_income_bill_fee.fee_id''',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 3. 费用明细索引同步改名,避免数据库中继续保留旧字段命名。
|
||||
SET @has_old_fee_unique := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = @db_name
|
||||
AND TABLE_NAME = 'ar_bill_detail_fee'
|
||||
AND INDEX_NAME = 'uk_ar_detail_prc_fee'
|
||||
);
|
||||
SET @has_new_fee_unique := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = @db_name
|
||||
AND TABLE_NAME = 'ar_bill_detail_fee'
|
||||
AND INDEX_NAME = 'uk_ar_detail_prc_income_fee'
|
||||
);
|
||||
SET @sql := IF(@has_old_fee_unique > 0 AND @has_new_fee_unique = 0,
|
||||
'ALTER TABLE ar_bill_detail_fee RENAME INDEX uk_ar_detail_prc_fee TO uk_ar_detail_prc_income_fee',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @has_old_fee_index := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = @db_name
|
||||
AND TABLE_NAME = 'ar_bill_detail_fee'
|
||||
AND INDEX_NAME = 'idx_ar_detail_prc_fee'
|
||||
);
|
||||
SET @has_new_fee_index := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = @db_name
|
||||
AND TABLE_NAME = 'ar_bill_detail_fee'
|
||||
AND INDEX_NAME = 'idx_ar_detail_prc_income_fee'
|
||||
);
|
||||
SET @sql := IF(@has_old_fee_index > 0 AND @has_new_fee_index = 0,
|
||||
'ALTER TABLE ar_bill_detail_fee RENAME INDEX idx_ar_detail_prc_fee TO idx_ar_detail_prc_income_fee',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@@ -0,0 +1,36 @@
|
||||
import request from '@/utils/request.js'
|
||||
|
||||
// 查询客户已绑定的套餐列表
|
||||
export function listCustomerPriceSet(customerId) {
|
||||
return request({
|
||||
url: '/customersupplier/customerinfo/priceset/bind/list',
|
||||
method: 'get',
|
||||
params: { customerId }
|
||||
})
|
||||
}
|
||||
|
||||
// 查询可绑定的生效套餐选项
|
||||
export function listPriceSetOption(query) {
|
||||
return request({
|
||||
url: '/customersupplier/customerinfo/priceset/option/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 保存客户套餐绑定(已绑定则更新/更换,未绑定则新增)
|
||||
export function saveCustomerPriceSet(data) {
|
||||
return request({
|
||||
url: '/customersupplier/customerinfo/priceset/bind',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 解绑客户套餐
|
||||
export function delCustomerPriceSet(customerPricesetIds) {
|
||||
return request({
|
||||
url: '/customersupplier/customerinfo/priceset/bind/' + customerPricesetIds,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/** 查询客户可核销账单(已确认/部分核销) */
|
||||
export function listUnsettledBill(customerId) {
|
||||
return request({
|
||||
url: '/customersupplier/customerbill/unsettled/' + customerId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/** 查询余额核销记录 */
|
||||
export function listBalanceWriteoff(query) {
|
||||
return request({
|
||||
url: '/customersupplier/customerbill/writeoff/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
/** 用客户账户余额核销账单 */
|
||||
export function balanceWriteoff(data) {
|
||||
return request({
|
||||
url: '/customersupplier/customerbill/writeoff',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
/** 反核销(取消一笔有效余额核销) */
|
||||
export function cancelBalanceWriteoff(writeoffId, cancelRemark) {
|
||||
return request({
|
||||
url: '/customersupplier/customerbill/writeoff/' + writeoffId + '/cancel',
|
||||
method: 'put',
|
||||
data: { 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'
|
||||
})
|
||||
}
|
||||
@@ -58,3 +58,21 @@ export function listFlow(query) {
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
/** 查询额度流水列表 */
|
||||
export function listCreditFlow(query) {
|
||||
return request({
|
||||
url: '/customersupplier/customermoney/credit/flow/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
/** 调整客户额度(信用额度/临时额度),调整过程留额度流水 */
|
||||
export function adjustCredit(data) {
|
||||
return request({
|
||||
url: '/customersupplier/customermoney/credit/adjust',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<template>
|
||||
<div class="app-container ar-bill-page">
|
||||
<basic-table ref="tableRef" :columns="columns" :search-columns="searchColumns" :query="queryList" row-key="arBillId" :selection="false">
|
||||
<basic-table ref="tableRef" :columns="columns" :search-columns="searchColumns" :search-label-width="82" :query="queryList" row-key="arBillId" :selection="false">
|
||||
<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>
|
||||
</template>
|
||||
<template #search-customerId="{ model }">
|
||||
<customer-select v-model="model.customerId" placeholder="请选择客户" :page-size="1000" />
|
||||
</template>
|
||||
<template #col-status="{ row }"><el-tag :type="statusType(row.status)">{{ statusText(row.status) }}</el-tag></template>
|
||||
<template #col-totalAmount="{ row }"><span class="amount">{{ money(row.totalAmount) }}</span></template>
|
||||
<template #col-writtenOffAmount="{ row }"><span class="written">{{ money(row.writtenOffAmount) }}</span></template>
|
||||
@@ -36,18 +39,18 @@ const detail = ref(null)
|
||||
const generateOpen = ref(false)
|
||||
const statusOptions = AR_BILL_STATUS_OPTIONS
|
||||
const columns = [
|
||||
{ label: '账单号', prop: 'billNo', minWidth: 180, showOverflowTooltip: true }, { label: '账单名称', prop: 'billName', minWidth: 180, showOverflowTooltip: true }, { label: '客户', prop: 'customerName', minWidth: 130, showOverflowTooltip: true },
|
||||
{ label: '账单号', prop: 'billNo', minWidth: 180, showOverflowTooltip: true }, { label: '账单名称', prop: 'billName', minWidth: 180, showOverflowTooltip: true }, { label: '客户名称', prop: 'customerName', minWidth: 130, showOverflowTooltip: true }, { label: '客户编码', prop: 'customerCode', minWidth: 120, showOverflowTooltip: true },
|
||||
{ label: '账单截止日', prop: 'billPeriodEnd', minWidth: 120, formatter: row => `截至 ${date(row.billPeriodEnd)}` }, { label: '订单数', prop: 'orderCount', width: 80 },
|
||||
{ label: '应收总额', prop: 'totalAmount', width: 120, align: 'right', slot: 'totalAmount' }, { label: '已核销', prop: 'writtenOffAmount', width: 110, align: 'right', slot: 'writtenOffAmount' },
|
||||
{ label: '未核销', prop: 'unwrittenAmount', width: 110, align: 'right', slot: 'unwrittenAmount' }, { label: '状态', prop: 'status', width: 110, slot: 'status' },
|
||||
{ label: '账单日期', prop: 'billDate', width: 120, formatter: row => date(row.billDate) }, { label: '操作', prop: 'action', width: 260, fixed: 'right', slot: 'action', showInToolbar: false }
|
||||
]
|
||||
const searchColumns = [
|
||||
{ label: '账单号', prop: 'billNo', component: 'input', placeholder: '账单号', width: '180px' }, { label: '账单名称', prop: 'billName', component: 'input', placeholder: '账单名称', width: '180px' }, { label: '客户编码', prop: 'customerCode', component: 'input', placeholder: '客户编码', width: '140px' },
|
||||
{ label: '状态', prop: 'status', component: 'select', options: statusOptions, placeholder: '全部状态', width: '150px' }, { label: '账期', prop: 'periodRange', component: 'daterange', width: '300px' }
|
||||
{ label: '账单号', prop: 'billNo', component: 'input', placeholder: '账单号', width: '180px' }, { label: '账单名称', prop: 'billName', component: 'input', placeholder: '账单名称', width: '180px' }, { label: '客户名称', prop: 'customerId', component: 'input', placeholder: '请选择客户', width: '220px' }, { label: '客户编码', prop: 'customerCode', component: 'input', placeholder: '客户编码', width: '140px' },
|
||||
{ label: '状态', prop: 'status', component: 'select', options: statusOptions, placeholder: '全部状态', width: '150px' }, { label: '账单日期', prop: 'billDateRange', component: 'daterange', width: '300px' }, { label: '账单截止日', prop: 'billPeriodEnd', component: 'date', width: '160px' }
|
||||
]
|
||||
|
||||
function queryList(params) { const query = { ...params }; if (query.periodRange?.length === 2) { query.billPeriodStart = query.periodRange[0]; query.billPeriodEnd = query.periodRange[1] }; delete query.periodRange; return listArBill(query) }
|
||||
function queryList(params) { const query = { ...params }; if (query.billDateRange?.length === 2) { query.billDateStart = query.billDateRange[0]; query.billDateEnd = query.billDateRange[1] }; delete query.billDateRange; return listArBill(query) }
|
||||
function handleGenerateSuccess(bill) { tableRef.value.refresh(); viewBill(bill) }
|
||||
function viewBill(row) { if (!row?.arBillId) return; detailOpen.value = true; detailLoading.value = true; detail.value = null; getArBill(row.arBillId).then(res => { detail.value = res.data || null }).finally(() => { detailLoading.value = false }) }
|
||||
function confirmBill(row) { proxy.$modal.confirm(`确认账单「${row.billNo}」?确认后将进入正式账单状态,请确认订单及费用金额无误后继续。`).then(() => confirmArBill(row.arBillId)).then(() => { proxy.$modal.msgSuccess('账单已确认'); tableRef.value.refresh(); viewBill(row) }).catch(() => {}) }
|
||||
@@ -61,7 +64,61 @@ function statusType(value) { return AR_BILL_STATUS_TYPES[value] || 'info' }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ar-bill-page :deep(.basic-table > .basic-form) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 14px 20px;
|
||||
align-items: center;
|
||||
padding: 16px 18px 4px;
|
||||
margin-bottom: 18px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.ar-bill-page :deep(.basic-table > .basic-form > .el-form-item) {
|
||||
min-width: 0;
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.ar-bill-page :deep(.basic-table > .basic-form > .el-form-item:not(:last-child) .el-form-item__content) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ar-bill-page :deep(.basic-table > .basic-form > .el-form-item:not(:last-child) .el-form-item__content > *) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.ar-bill-page :deep(.basic-table > .basic-form > .el-form-item:last-child) {
|
||||
grid-column: 1 / -1;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.ar-bill-page :deep(.basic-table > .basic-form > .el-form-item:last-child .el-form-item__content) {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.ar-bill-page :deep(.basic-table > .basic-form > .el-form-item:last-child .el-button) {
|
||||
min-width: 88px;
|
||||
margin-left: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.ar-bill-page .amount { color: #f56c6c; font-weight: 600; }
|
||||
.ar-bill-page .written { color: #67c23a; }
|
||||
.ar-bill-page .unwritten { color: #e6a23c; }
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.ar-bill-page :deep(.basic-table > .basic-form) {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.ar-bill-page :deep(.basic-table > .basic-form) {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
padding: 14px 12px 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
<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>
|
||||
+233
-3
@@ -108,7 +108,11 @@
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="临时额度" prop="tempEd">
|
||||
<el-input v-model="form.tempEd" placeholder="请输入临时额度" />
|
||||
<el-input v-model="form.tempEd" :disabled="!isAdd" placeholder="请输入临时额度">
|
||||
<template #append v-if="!isAdd">
|
||||
<span style="color: #e6a23c">请在客户余额-额度管理中调整</span>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
@@ -130,7 +134,11 @@
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="信用额度" prop="creditEd">
|
||||
<el-input v-model="form.creditEd" placeholder="请输入信用额度" />
|
||||
<el-input v-model="form.creditEd" :disabled="!isAdd" placeholder="请输入信用额度">
|
||||
<template #append v-if="!isAdd">
|
||||
<span style="color: #e6a23c">请在客户余额-额度管理中调整</span>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
@@ -178,6 +186,50 @@
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- ============ 绑定套餐 ============ -->
|
||||
<el-tab-pane label="绑定套餐" name="priceSet">
|
||||
<div class="pane-toolbar">
|
||||
<el-button type="primary" plain icon="Plus" size="small" :disabled="isAdd" @click="openPriceSetDialog()">
|
||||
{{ boundPriceSets.length ? '更换套餐' : '绑定套餐' }}
|
||||
</el-button>
|
||||
<span class="pane-tip">绑定后写入客户套餐关系表 prc_customer_price_set,一个客户仅可绑定一个价格套餐,仅「生效中」的套餐可绑定</span>
|
||||
</div>
|
||||
<el-table :data="boundPriceSets" border size="small" class="bound-table">
|
||||
<el-table-column label="序号" type="index" width="55" align="center" />
|
||||
<el-table-column label="套餐编码" prop="priceSetCode" width="120" show-overflow-tooltip />
|
||||
<el-table-column label="套餐名称" prop="priceSetName" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="计价类型" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.setType === 'DISCOUNT' ? 'warning' : 'primary'" size="small">
|
||||
{{ setTypeMap[row.setType] || row.setType || '-' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="套餐状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="priceSetStatusTagType[row.priceSetStatus] || 'info'" size="small">
|
||||
{{ priceSetStatusMap[row.priceSetStatus] || '未知' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="优先级" prop="bindPriority" width="70" align="center" />
|
||||
<el-table-column label="绑定生效" prop="prBegindate" width="110" align="center" />
|
||||
<el-table-column label="绑定失效" width="100" align="center">
|
||||
<template #default="{ row }">{{ row.prEnddate || '长期' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" prop="priceSetNote" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="openPriceSetDialog(row)">更换</el-button>
|
||||
<el-button link type="danger" size="small" @click="handleUnbindPriceSet(row)">解绑</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty :description="isAdd ? '请先保存客户后再绑定套餐' : '暂未绑定套餐,点击上方「绑定套餐」'" :image-size="80" />
|
||||
</template>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- ============ 公司主体 ============ -->
|
||||
<el-tab-pane label="公司主体" name="company">
|
||||
<div class="pane-toolbar">
|
||||
@@ -574,6 +626,75 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ====== 绑定套餐对话框 ====== -->
|
||||
<el-dialog :title="priceSetDialogTitle" v-model="priceSetDialogVisible" width="560px" append-to-body>
|
||||
<el-form ref="priceSetRef" :model="priceSetForm" :rules="priceSetRules" label-width="96px">
|
||||
<el-form-item label="价格套餐" prop="priceSetId">
|
||||
<el-select
|
||||
v-model="priceSetForm.priceSetId"
|
||||
filterable
|
||||
placeholder="请选择生效中的套餐"
|
||||
style="width: 100%;"
|
||||
@change="onPriceSetChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in priceSetOptions"
|
||||
:key="opt.priceSetId"
|
||||
:label="((opt.priceSetCode || '') + ' ' + opt.priceSetName).trim()"
|
||||
:value="opt.priceSetId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="selectedPriceSet" label="套餐信息">
|
||||
<div class="ps-readonly">
|
||||
<el-tag size="small" :type="selectedPriceSet.setType === 'DISCOUNT' ? 'warning' : 'primary'">
|
||||
{{ setTypeMap[selectedPriceSet.setType] || selectedPriceSet.setType }}
|
||||
</el-tag>
|
||||
<span class="ps-readonly__range">
|
||||
套餐有效期:{{ selectedPriceSet.effectiveDate || '-' }} 至 {{ selectedPriceSet.expireDate || '长期' }}
|
||||
</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="优先级" prop="bindPriority">
|
||||
<el-input-number v-model="priceSetForm.bindPriority" :min="1" controls-position="right" style="width: 100%;" />
|
||||
<div class="pane-tip">数字越小优先级越高,1 为最高</div>
|
||||
</el-form-item>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="生效日期" prop="prBegindate">
|
||||
<el-date-picker
|
||||
v-model="priceSetForm.prBegindate"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="选择日期"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="失效日期" prop="prEnddate">
|
||||
<el-date-picker
|
||||
v-model="priceSetForm.prEnddate"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="长期有效"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="备注" prop="priceSetNote">
|
||||
<el-input v-model="priceSetForm.priceSetNote" type="textarea" :rows="2" maxlength="100" show-word-limit placeholder="绑定备注(选填)" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" :loading="priceSetSaving" @click="submitPriceSetForm">确 定</el-button>
|
||||
<el-button @click="priceSetDialogVisible = false">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -582,6 +703,10 @@ import {
|
||||
listCustomer, getCustomer, addCustomer, updateCustomer,
|
||||
delCustomerCompany, delCustomerContact
|
||||
} from '@/api/customersuppliermanagement/customerinfo/customer.js'
|
||||
import {
|
||||
listCustomerPriceSet, listPriceSetOption,
|
||||
saveCustomerPriceSet, delCustomerPriceSet
|
||||
} from '@/api/customersuppliermanagement/customerinfo/customerPriceSet.js'
|
||||
import { getToken } from '@/utils/auth'
|
||||
import axios from 'axios'
|
||||
import { saveAs } from 'file-saver'
|
||||
@@ -630,7 +755,7 @@ function buildEmptyForm() {
|
||||
foreignCurrencyBalance: null,
|
||||
weightThreshold: null,
|
||||
throwRatio: null,
|
||||
creditEd: null,
|
||||
creditEd: 1000,
|
||||
productIds: null,
|
||||
attachmentList: [],
|
||||
companyList: [],
|
||||
@@ -656,6 +781,99 @@ function removeProduct(productId) {
|
||||
form.value.productIds = ids.join(',')
|
||||
}
|
||||
|
||||
/* ---------------- 套餐绑定(prc_customer_price_set,一客户一套餐) ---------------- */
|
||||
const setTypeMap = { FIXED: '固定价', DISCOUNT: '折扣' }
|
||||
const priceSetStatusMap = { D: '草稿', A: '审核中', E: '生效', R: '驳回', S: '停用' }
|
||||
const priceSetStatusTagType = { D: 'info', A: 'warning', E: 'success', R: 'danger', S: 'info' }
|
||||
|
||||
const boundPriceSets = ref([])
|
||||
const priceSetOptions = ref([])
|
||||
const priceSetDialogVisible = ref(false)
|
||||
const priceSetDialogTitle = ref('')
|
||||
const priceSetSaving = ref(false)
|
||||
const priceSetRef = ref()
|
||||
const priceSetForm = ref({})
|
||||
|
||||
const priceSetRules = {
|
||||
priceSetId: [{ required: true, message: '请选择价格套餐', trigger: 'change' }],
|
||||
bindPriority: [{ required: true, message: '优先级不能为空', trigger: 'blur' }],
|
||||
prBegindate: [{ required: true, message: '绑定生效日期不能为空', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const selectedPriceSet = computed(() =>
|
||||
priceSetOptions.value.find(o => o.priceSetId === priceSetForm.value.priceSetId)
|
||||
)
|
||||
|
||||
/** 加载当前客户已绑定套餐 */
|
||||
function loadBoundPriceSets() {
|
||||
if (!form.value.customerId) {
|
||||
boundPriceSets.value = []
|
||||
return
|
||||
}
|
||||
listCustomerPriceSet(form.value.customerId).then(res => {
|
||||
boundPriceSets.value = res.data || []
|
||||
})
|
||||
}
|
||||
|
||||
/** 加载可绑定的生效套餐(仅加载一次) */
|
||||
function ensurePriceSetOptions() {
|
||||
if (priceSetOptions.value.length) return
|
||||
listPriceSetOption().then(res => {
|
||||
priceSetOptions.value = res.data || []
|
||||
})
|
||||
}
|
||||
|
||||
/** 打开绑定/更换套餐弹窗:row 为空=新增绑定,有值=更换 */
|
||||
function openPriceSetDialog(row) {
|
||||
if (!form.value.customerId) {
|
||||
proxy.$modal.msgWarning('请先保存客户后再绑定套餐')
|
||||
return
|
||||
}
|
||||
ensurePriceSetOptions()
|
||||
priceSetForm.value = row
|
||||
? { ...row }
|
||||
: { priceSetId: null, bindPriority: 1, prBegindate: null, prEnddate: null, priceSetNote: null }
|
||||
priceSetDialogTitle.value = row || boundPriceSets.value.length ? '更换套餐' : '绑定套餐'
|
||||
priceSetDialogVisible.value = true
|
||||
nextTick(() => proxy.$refs['priceSetRef'] && proxy.$refs['priceSetRef'].clearValidate())
|
||||
}
|
||||
|
||||
/** 选择套餐后,生效日期未填时默认带出套餐生效日期 */
|
||||
function onPriceSetChange(priceSetId) {
|
||||
const opt = priceSetOptions.value.find(o => o.priceSetId === priceSetId)
|
||||
if (opt && !priceSetForm.value.prBegindate) {
|
||||
priceSetForm.value.prBegindate = opt.effectiveDate || null
|
||||
}
|
||||
}
|
||||
|
||||
function submitPriceSetForm() {
|
||||
proxy.$refs['priceSetRef'].validate(valid => {
|
||||
if (!valid) return
|
||||
if (priceSetForm.value.prEnddate && priceSetForm.value.prBegindate > priceSetForm.value.prEnddate) {
|
||||
proxy.$modal.msgWarning('失效日期不能早于生效日期')
|
||||
return
|
||||
}
|
||||
const payload = { ...priceSetForm.value, customerId: form.value.customerId }
|
||||
priceSetSaving.value = true
|
||||
saveCustomerPriceSet(payload).then(() => {
|
||||
proxy.$modal.msgSuccess('保存成功')
|
||||
priceSetDialogVisible.value = false
|
||||
loadBoundPriceSets()
|
||||
}).finally(() => {
|
||||
priceSetSaving.value = false
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function handleUnbindPriceSet(row) {
|
||||
proxy.$modal.confirm(`确认解绑套餐「${row.priceSetName || row.priceSetCode || ''}」?解绑后该客户询价将不再命中此套餐。`)
|
||||
.then(() => delCustomerPriceSet(row.customerPricesetId))
|
||||
.then(() => {
|
||||
proxy.$modal.msgSuccess('解绑成功')
|
||||
loadBoundPriceSets()
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
/* ---------------- 用户冗余姓名 ---------------- */
|
||||
function handleUserChange(uid, user, nameField) {
|
||||
form.value[nameField] = user ? (user.nickName || user.userName || null) : null
|
||||
@@ -968,6 +1186,7 @@ function loadDetail(id) {
|
||||
form.value.attachmentList = form.value.attachmentList || []
|
||||
form.value.companyList = form.value.companyList || []
|
||||
form.value.contactList = form.value.contactList || []
|
||||
loadBoundPriceSets()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1023,6 +1242,7 @@ function initByRoute() {
|
||||
loadDetail(customerId.value).finally(() => { pageLoading.value = false })
|
||||
} else {
|
||||
form.value = buildEmptyForm()
|
||||
boundPriceSets.value = []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1077,4 +1297,14 @@ onMounted(() => {
|
||||
.bound-table {
|
||||
margin-top: 4px;
|
||||
}
|
||||
.ps-readonly {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
&__range {
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+839
-4
@@ -11,6 +11,17 @@
|
||||
<div class="head-code">客户编码:{{ customer.customerCode || route.query.customerCode || '-' }}</div>
|
||||
<div class="head-right">
|
||||
<el-tag type="success" size="small">本位币账户 CNY</el-tag>
|
||||
<el-button
|
||||
icon="Document"
|
||||
@click="goBillView"
|
||||
v-hasPermi="['customer:bill:list']"
|
||||
>查看账单</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
icon="Money"
|
||||
@click="openAdjustDialog()"
|
||||
v-hasPermi="['customer:credit:adjust']"
|
||||
>额度调整</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -31,14 +42,23 @@
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="8" :md="4">
|
||||
<el-card shadow="hover" class="balance-card">
|
||||
<div class="balance-label">总额度</div>
|
||||
<div class="balance-label">
|
||||
总额度
|
||||
<el-button link type="primary" size="small" @click="openAdjustDialog('CREDIT')"
|
||||
v-hasPermi="['customer:credit:adjust']">调整</el-button>
|
||||
</div>
|
||||
<div class="balance-value">{{ formatMoney(currentAccount.totalCredit) }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="8" :md="4">
|
||||
<el-card shadow="hover" class="balance-card">
|
||||
<div class="balance-label">额外额度</div>
|
||||
<div class="balance-label">
|
||||
额外额度
|
||||
<el-button link type="primary" size="small" @click="openAdjustDialog('TEMP')"
|
||||
v-hasPermi="['customer:credit:adjust']">调整</el-button>
|
||||
</div>
|
||||
<div class="balance-value">{{ formatMoney(currentAccount.extraCredit) }}</div>
|
||||
<div class="balance-sub">{{ tempCreditPeriodText }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="8" :md="4">
|
||||
@@ -225,6 +245,204 @@
|
||||
@pagination="getFlowList"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- ====== 额度流水 ====== -->
|
||||
<el-tab-pane label="额度流水" name="creditFlow">
|
||||
<el-form :model="creditFlowQuery" :inline="true" class="search-form">
|
||||
<el-form-item label="额度类型" prop="creditType">
|
||||
<el-select v-model="creditFlowQuery.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="changeType">
|
||||
<el-select v-model="creditFlowQuery.changeType" placeholder="全部" clearable style="width: 130px;">
|
||||
<el-option v-for="opt in creditChangeTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="日期">
|
||||
<el-date-picker
|
||||
v-model="creditFlowDateRange"
|
||||
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="getCreditFlowList">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetCreditFlowQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table :data="creditFlowList" border v-loading="creditFlowLoading">
|
||||
<el-table-column label="流水号" align="center" prop="flowNo" width="190" />
|
||||
<el-table-column label="时间" align="center" prop="createTime" width="160">
|
||||
<template #default="scope">{{ formatDateTime(scope.row.createTime) }}</template>
|
||||
</el-table-column>
|
||||
<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="changeType" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="creditChangeTag(scope.row.changeType)">
|
||||
{{ formatOption(creditChangeTypeOptions, scope.row.changeType) }}
|
||||
</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" width="80">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.direction === 1" type="success">增加</el-tag>
|
||||
<el-tag v-else-if="scope.row.direction === -1" type="danger">减少</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="变动前额度" align="center" prop="beforeAmount" width="110">
|
||||
<template #default="scope">{{ formatMoney(scope.row.beforeAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="变动额度" align="center" width="110">
|
||||
<template #default="scope">
|
||||
<span :class="scope.row.direction === 1 ? 'money-in' : 'money-out'">
|
||||
{{ scope.row.direction === 1 ? '+' : '-' }}{{ formatMoney(scope.row.changeAmount) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="变动后额度" align="center" prop="afterAmount" width="110">
|
||||
<template #default="scope">{{ formatMoney(scope.row.afterAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="已用额度" align="center" prop="usedCredit" width="100">
|
||||
<template #default="scope">{{ formatMoney(scope.row.usedCredit) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="生效日期" align="center" prop="effectiveDate" width="110">
|
||||
<template #default="scope">{{ scope.row.effectiveDate || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="失效日期" align="center" prop="expireDate" width="110">
|
||||
<template #default="scope">{{ scope.row.expireDate || '长期' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="调整原因" align="center" prop="reason" show-overflow-tooltip min-width="160" />
|
||||
<el-table-column label="操作人" align="center" prop="createBy" width="100" />
|
||||
<el-table-column label="备注" align="center" prop="remark" show-overflow-tooltip min-width="160" />
|
||||
</el-table>
|
||||
|
||||
<pagination
|
||||
v-show="creditFlowTotal > 0"
|
||||
:total="creditFlowTotal"
|
||||
v-model:page="creditFlowQuery.pageNum"
|
||||
v-model:limit="creditFlowQuery.pageSize"
|
||||
@pagination="getCreditFlowList"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<!-- ====== 账单核销(用账户余额核销应收账单) ====== -->
|
||||
<el-tab-pane label="账单核销" name="billWriteoff">
|
||||
<el-alert type="info" :closable="false" class="writeoff-summary">
|
||||
<div class="wo-summary">
|
||||
<span>待核销账单:<b>{{ unsettledList.length }}</b> 张</span>
|
||||
<span>未核销总额:<b class="money-out">CNY {{ unsettledTotal }}</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" plain size="small" @click="goBillView"
|
||||
v-hasPermi="['customer:bill:list']">查看全部账单</el-button>
|
||||
</div>
|
||||
</el-alert>
|
||||
|
||||
<div class="wo-section-title">待核销账单(已确认 / 部分核销)</div>
|
||||
<el-table :data="unsettledList" border size="small" v-loading="unsettledLoading">
|
||||
<el-table-column label="账单号" align="center" prop="billNo" 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="billPeriodStart" width="110">
|
||||
<template #default="scope">{{ formatDate(scope.row.billPeriodStart) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="账期结束" align="center" prop="billPeriodEnd" width="110">
|
||||
<template #default="scope">{{ formatDate(scope.row.billPeriodEnd) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="账单日期" align="center" prop="billDate" width="110">
|
||||
<template #default="scope">{{ formatDate(scope.row.billDate) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="应收金额" align="center" width="120">
|
||||
<template #default="scope">{{ scope.row.currency }} {{ formatMoney(scope.row.totalAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="已核销" align="center" width="110">
|
||||
<template #default="scope"><span class="money-in">{{ formatMoney(scope.row.writtenOffAmount) }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="未核销" align="center" width="110">
|
||||
<template #default="scope"><span class="money-out">{{ formatMoney(scope.row.unwrittenAmount) }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" align="center" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="billStatusTag(scope.row.status)" size="small">{{ billStatusLabel(scope.row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" width="90" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="openWriteoffDialog(scope.row)"
|
||||
v-hasPermi="['customer:bill:writeoff']">核销</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty description="暂无待核销账单" :image-size="70" />
|
||||
</template>
|
||||
</el-table>
|
||||
|
||||
<div class="wo-section-title">余额核销记录</div>
|
||||
<el-form :inline="true" class="search-form">
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="woQuery.status" placeholder="全部" clearable style="width: 130px;">
|
||||
<el-option label="有效" value="EFFECTIVE" />
|
||||
<el-option label="已反核销" value="CANCELLED" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="getWriteoffList">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetWriteoffQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table :data="writeoffList" border size="small" v-loading="writeoffLoading">
|
||||
<el-table-column label="核销单号" align="center" prop="writeoffNo" width="180" />
|
||||
<el-table-column label="账单号" align="center" prop="billNo" width="170" show-overflow-tooltip />
|
||||
<el-table-column label="账单名称" align="center" prop="billName" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="核销金额" align="center" width="120">
|
||||
<template #default="scope"><span class="money-out">-{{ formatMoney(scope.row.writeoffAmount) }} {{ scope.row.currency }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="核销日期" align="center" prop="writeoffDate" width="110">
|
||||
<template #default="scope">{{ formatDate(scope.row.writeoffDate) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" align="center" width="90">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 'EFFECTIVE' ? 'success' : 'info'" size="small">
|
||||
{{ scope.row.status === 'EFFECTIVE' ? '有效' : '已反核销' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" align="center" prop="remark" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column label="登记人" align="center" prop="createBy" width="90" />
|
||||
<el-table-column label="登记时间" align="center" prop="createTime" width="140">
|
||||
<template #default="scope">{{ formatDateTime(scope.row.createTime) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="反核销原因" align="center" prop="cancelRemark" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column label="操作" align="center" width="90" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button v-if="scope.row.status === 'EFFECTIVE'" link type="warning" size="small"
|
||||
@click="openCancelWriteoffDialog(scope.row)"
|
||||
v-hasPermi="['customer:bill:writeoff:cancel']">反核销</el-button>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination
|
||||
v-show="writeoffTotal > 0"
|
||||
:total="writeoffTotal"
|
||||
v-model:page="woQuery.pageNum"
|
||||
v-model:limit="woQuery.pageSize"
|
||||
@pagination="getWriteoffList"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
|
||||
@@ -402,6 +620,183 @@
|
||||
<el-button @click="viewDialog.visible = false">关 闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ====== 额度调整弹窗 ====== -->
|
||||
<el-dialog
|
||||
:title="creditDialog.form.creditType === 'TEMP' ? '临时额度调整' : '信用额度调整'"
|
||||
v-model="creditDialog.visible"
|
||||
width="600px"
|
||||
append-to-body
|
||||
@closed="resetCreditForm"
|
||||
>
|
||||
<el-alert type="info" :closable="false" class="credit-tip">
|
||||
<div>当前{{ creditDialog.form.creditType === 'TEMP' ? '临时额度' : '信用额度' }}:
|
||||
<b>{{ formatMoney(creditBefore) }}</b>,已用额度:<b>{{ formatMoney(currentAccount.usedCredit) }}</b></div>
|
||||
</el-alert>
|
||||
<el-form ref="creditFormRef" :model="creditDialog.form" :rules="creditRules" label-width="100px">
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="额度类型" prop="creditType">
|
||||
<el-radio-group v-model="creditDialog.form.creditType" @change="onCreditTypeChange">
|
||||
<el-radio value="CREDIT">信用额度(长期)</el-radio>
|
||||
<el-radio value="TEMP">临时额度</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="调整方式" prop="adjustMode">
|
||||
<el-radio-group v-model="creditDialog.form.adjustMode">
|
||||
<el-radio value="SET">设置为</el-radio>
|
||||
<el-radio value="ADD">增加</el-radio>
|
||||
<el-radio value="SUB">减少</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="调整金额" prop="changeAmount">
|
||||
<el-input-number
|
||||
v-model="creditDialog.form.changeAmount"
|
||||
:precision="2"
|
||||
:step="1000"
|
||||
:min="0.01"
|
||||
controls-position="right"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<template v-if="creditDialog.form.creditType === 'TEMP'">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="生效日期" prop="effectiveDate">
|
||||
<el-date-picker
|
||||
v-model="creditDialog.form.effectiveDate"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="默认今天"
|
||||
:disabled-date="afterToday"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="失效日期" prop="expireDate">
|
||||
<el-date-picker
|
||||
v-model="creditDialog.form.expireDate"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="不填为长期有效"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
<div class="rate-tip">失效日当天仍有效,次日访问账户时自动清零并记到期流水</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</template>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="调整原因" prop="reason">
|
||||
<el-input
|
||||
v-model="creditDialog.form.reason"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
maxlength="255"
|
||||
show-word-limit
|
||||
placeholder="请必填,例如:客户旺季临时授信 / 年审调整长期额度"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
|
||||
<div class="credit-preview">
|
||||
<div>调整后{{ creditDialog.form.creditType === 'TEMP' ? '临时额度' : '信用额度' }}:
|
||||
<b :class="creditAfter < 0 ? 'money-out' : 'money-in'">{{ formatMoney(creditAfter) }}</b>
|
||||
</div>
|
||||
<div>调整后授信总额:<b>{{ formatMoney(totalCreditAfter) }}</b>
|
||||
(可用 = 授信总额 - 已用 {{ formatMoney(currentAccount.usedCredit) }})</div>
|
||||
<el-alert
|
||||
v-if="creditAfter < 0"
|
||||
type="error" :closable="false"
|
||||
title="调整后额度不能为负数" class="credit-alert"
|
||||
/>
|
||||
<el-alert
|
||||
v-else-if="totalCreditAfter < (Number(currentAccount.usedCredit) || 0)"
|
||||
type="error" :closable="false"
|
||||
title="授信总额不能小于已用额度" class="credit-alert"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button type="primary" :loading="creditSubmitLoading" @click="submitAdjustCredit">确 定</el-button>
|
||||
<el-button @click="creditDialog.visible = false">取 消</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ====== 余额核销账单弹窗 ====== -->
|
||||
<el-dialog title="余额核销账单" v-model="woDialog.visible" width="560px" append-to-body>
|
||||
<el-descriptions :column="1" border size="small" v-if="woDialog.bill">
|
||||
<el-descriptions-item label="账单号">{{ woDialog.bill.billNo }}</el-descriptions-item>
|
||||
<el-descriptions-item label="账单名称">{{ woDialog.bill.billName || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="账单未核销金额">
|
||||
<span class="money-out">CNY {{ formatMoney(woDialog.bill.unwrittenAmount) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="账户可用余额">
|
||||
<span class="money-in">CNY {{ formatMoney(currentAccount.balance) }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-form :model="woDialog.form" label-width="110px" class="wo-form">
|
||||
<el-form-item label="本次核销金额" required>
|
||||
<el-input-number
|
||||
v-model="woDialog.form.writeoffAmount"
|
||||
:precision="2"
|
||||
:step="100"
|
||||
:min="0.01"
|
||||
controls-position="right"
|
||||
style="width: 220px;"
|
||||
/>
|
||||
<span class="wo-form-unit">CNY</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="核销日期">
|
||||
<el-date-picker
|
||||
v-model="woDialog.form.writeoffDate"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="默认今天"
|
||||
style="width: 220px;"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="woDialog.form.remark" type="textarea" :rows="2" maxlength="255" show-word-limit
|
||||
placeholder="选填,最多255字" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-alert type="warning" :closable="false">
|
||||
<template title>
|
||||
核销后账户余额变为 CNY {{ woAfterBalance }},同步释放等额已用额度;余额不足时无法提交。
|
||||
</template>
|
||||
</el-alert>
|
||||
<template #footer>
|
||||
<el-button type="primary" :loading="woSubmitLoading" @click="submitWriteoff">确认核销</el-button>
|
||||
<el-button @click="woDialog.visible = false">取 消</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ====== 反核销弹窗 ====== -->
|
||||
<el-dialog title="反核销(取消余额核销)" v-model="cancelWoDialog.visible" width="480px" append-to-body>
|
||||
<el-alert type="warning" :closable="false" class="credit-tip">
|
||||
<span v-if="cancelWoDialog.row">
|
||||
反核销后,核销金额 CNY {{ formatMoney(cancelWoDialog.row.writeoffAmount) }} 将退回客户账户余额,
|
||||
账单「{{ cancelWoDialog.row.billNo }}」恢复为未核销,已用额度同步加回。
|
||||
</span>
|
||||
</el-alert>
|
||||
<el-form label-width="100px" class="wo-form">
|
||||
<el-form-item label="反核销原因" required>
|
||||
<el-input v-model="cancelWoDialog.cancelRemark" type="textarea" :rows="3" maxlength="255" show-word-limit
|
||||
placeholder="请填写反核销原因(必填)" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button type="warning" :loading="cancelWoLoading" @click="submitCancelWriteoff">确认反核销</el-button>
|
||||
<el-button @click="cancelWoDialog.visible = false">取 消</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -409,11 +804,13 @@
|
||||
import axios from 'axios'
|
||||
import { saveAs } from 'file-saver'
|
||||
import { getCustomer } from "@/api/customersuppliermanagement/customerinfo/customer.js"
|
||||
import { listAccount, listReceipt, getReceipt, addReceipt, confirmReceipt, cancelReceipt, listFlow } from "@/api/customersuppliermanagement/customermoney/customermoney.js"
|
||||
import { listAccount, listReceipt, getReceipt, addReceipt, confirmReceipt, cancelReceipt, listFlow, listCreditFlow, adjustCredit } from "@/api/customersuppliermanagement/customermoney/customermoney.js"
|
||||
import { listUnsettledBill, listBalanceWriteoff, balanceWriteoff, cancelBalanceWriteoff } from "@/api/customersuppliermanagement/customermoney/customerbill.js"
|
||||
import { convertToBase } from "@/api/data/exchangeRate"
|
||||
import { getToken } from "@/utils/auth"
|
||||
|
||||
import { useRoute } from 'vue-router'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -472,9 +869,28 @@ function receiptTypeTag(v) {
|
||||
function receiptStatusTag(v) {
|
||||
return { PENDING: 'warning', CONFIRMED: 'success', CANCELLED: 'danger' }[v] || 'info'
|
||||
}
|
||||
function bizTypeTag(v) {
|
||||
const bizTypeTag = function(v) {
|
||||
return { RECEIPT: 'success', BILL_PAY: 'danger', REFUND: 'warning', ADJUST: 'info', FREEZE: 'info', UNFREEZE: '' }[v] || 'info'
|
||||
}
|
||||
|
||||
/* ================= 额度相关枚举 ================= */
|
||||
const creditTypeOptions = [
|
||||
{ value: 'CREDIT', label: '信用额度' },
|
||||
{ value: 'TEMP', label: '临时额度' }
|
||||
]
|
||||
const creditChangeTypeOptions = [
|
||||
{ value: 'INIT', label: '初始化' },
|
||||
{ value: 'ADJUST', label: '手工调整' },
|
||||
{ value: 'EXPIRE', label: '到期失效' }
|
||||
]
|
||||
const adjustModeOptions = [
|
||||
{ value: 'SET', label: '设置为' },
|
||||
{ value: 'ADD', label: '增加' },
|
||||
{ value: 'SUB', label: '减少' }
|
||||
]
|
||||
function creditChangeTag(v) {
|
||||
return { INIT: '', ADJUST: 'primary', EXPIRE: 'danger' }[v] || 'info'
|
||||
}
|
||||
function formatMoney(val) {
|
||||
const num = Number(val)
|
||||
return isNaN(num) ? '0.00' : num.toFixed(2)
|
||||
@@ -506,6 +922,152 @@ const availableCredit = computed(() => {
|
||||
return formatMoney(total + extra - used)
|
||||
})
|
||||
|
||||
/** 临时额度有效期文案 */
|
||||
const tempCreditPeriodText = computed(() => {
|
||||
const a = currentAccount.value
|
||||
const extra = Number(a.extraCredit) || 0
|
||||
if (extra <= 0) return ''
|
||||
if (!a.extraExpireDate) return '长期有效'
|
||||
const today = proxy.parseTime(new Date(), '{y}-{m}-{d}')
|
||||
if (a.extraExpireDate < today) return '已过期'
|
||||
return '有效期至 ' + a.extraExpireDate
|
||||
})
|
||||
|
||||
/* ============ 额度调整弹窗 ============ */
|
||||
const creditFormRef = ref(null)
|
||||
const creditSubmitLoading = ref(false)
|
||||
const creditDialog = reactive({
|
||||
visible: false,
|
||||
form: {}
|
||||
})
|
||||
const creditRules = {
|
||||
creditType: [{ required: true, message: '请选择额度类型', trigger: 'change' }],
|
||||
adjustMode: [{ required: true, message: '请选择调整方式', trigger: 'change' }],
|
||||
changeAmount: [{ required: true, message: '调整金额必须大于0', trigger: 'blur' }],
|
||||
reason: [{ required: true, message: '请填写调整原因', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
/** 禁止选择晚于今天的日期(不允许预约未来生效) */
|
||||
function afterToday(date) {
|
||||
const today = new Date()
|
||||
today.setHours(23, 59, 59, 999)
|
||||
return date.getTime() > today.getTime()
|
||||
}
|
||||
|
||||
function defaultCreditForm(creditType) {
|
||||
return {
|
||||
customerId: route.query.customerId || null,
|
||||
creditType: creditType || 'CREDIT',
|
||||
adjustMode: 'ADD',
|
||||
changeAmount: null,
|
||||
reason: '',
|
||||
effectiveDate: proxy.parseTime(new Date(), '{y}-{m}-{d}'),
|
||||
expireDate: ''
|
||||
}
|
||||
}
|
||||
|
||||
function openAdjustDialog(creditType) {
|
||||
const customerId = Number(route.query.customerId)
|
||||
if (!customerId) {
|
||||
proxy.$modal.msgError('缺少客户ID,请从客户列表进入客户余额页')
|
||||
return
|
||||
}
|
||||
resetCreditForm()
|
||||
creditDialog.form = defaultCreditForm(creditType)
|
||||
creditDialog.visible = true
|
||||
}
|
||||
|
||||
function resetCreditForm() {
|
||||
creditDialog.form = defaultCreditForm()
|
||||
proxy.resetForm('creditFormRef')
|
||||
}
|
||||
|
||||
/** 切换额度类型:清掉校验红框,临时额度默认生效日为今天 */
|
||||
function onCreditTypeChange() {
|
||||
creditDialog.form.changeAmount = null
|
||||
if (creditDialog.form.creditType === 'TEMP' && !creditDialog.form.effectiveDate) {
|
||||
creditDialog.form.effectiveDate = proxy.parseTime(new Date(), '{y}-{m}-{d}')
|
||||
}
|
||||
nextTick(() => proxy.clearValidate('creditFormRef'))
|
||||
}
|
||||
|
||||
/** 当前被调整额度(调整前) */
|
||||
const creditBefore = computed(() => {
|
||||
const a = currentAccount.value
|
||||
return Number(creditDialog.form.creditType === 'TEMP' ? a.extraCredit : a.totalCredit) || 0
|
||||
})
|
||||
|
||||
/** 调整后被调额度 */
|
||||
const creditAfter = computed(() => {
|
||||
const input = Number(creditDialog.form.changeAmount) || 0
|
||||
const mode = creditDialog.form.adjustMode
|
||||
if (mode === 'SET') return input
|
||||
return mode === 'SUB' ? creditBefore.value - input : creditBefore.value + input
|
||||
})
|
||||
|
||||
/** 调整后授信总额(信用+临时) */
|
||||
const totalCreditAfter = computed(() => {
|
||||
const a = currentAccount.value
|
||||
const other = Number(creditDialog.form.creditType === 'TEMP' ? a.totalCredit : a.extraCredit) || 0
|
||||
return other + creditAfter.value
|
||||
})
|
||||
|
||||
function submitAdjustCredit() {
|
||||
creditFormRef.value.validate(valid => {
|
||||
if (!valid) return
|
||||
const f = creditDialog.form
|
||||
const amount = Number(f.changeAmount)
|
||||
if (!(amount > 0)) {
|
||||
proxy.$modal.msgError('调整金额必须大于0')
|
||||
return
|
||||
}
|
||||
if (f.creditType === 'TEMP') {
|
||||
if (f.expireDate && f.effectiveDate && f.expireDate <= f.effectiveDate) {
|
||||
proxy.$modal.msgError('失效日期必须晚于生效日期')
|
||||
return
|
||||
}
|
||||
}
|
||||
if (creditAfter.value < 0) {
|
||||
proxy.$modal.msgError('调整后额度不能为负数')
|
||||
return
|
||||
}
|
||||
const used = Number(currentAccount.value.usedCredit) || 0
|
||||
if (totalCreditAfter.value < used) {
|
||||
proxy.$modal.msgError('调整后授信总额不能小于已用额度 ' + formatMoney(used))
|
||||
return
|
||||
}
|
||||
if (creditAfter.value === creditBefore.value) {
|
||||
proxy.$modal.msgError('额度未发生变化,无需调整')
|
||||
return
|
||||
}
|
||||
const payload = {
|
||||
customerId: Number(f.customerId) || Number(route.query.customerId) || null,
|
||||
creditType: f.creditType,
|
||||
adjustMode: f.adjustMode,
|
||||
changeAmount: amount,
|
||||
reason: f.reason
|
||||
}
|
||||
if (!payload.customerId) {
|
||||
proxy.$modal.msgError('缺少客户ID,请从客户列表进入客户余额页')
|
||||
return
|
||||
}
|
||||
if (f.creditType === 'TEMP') {
|
||||
payload.effectiveDate = f.effectiveDate || null
|
||||
payload.expireDate = f.expireDate || null
|
||||
}
|
||||
creditSubmitLoading.value = true
|
||||
adjustCredit(payload).then(() => {
|
||||
proxy.$modal.msgSuccess('额度调整成功')
|
||||
creditDialog.visible = false
|
||||
loadCustomer()
|
||||
loadAccounts()
|
||||
getCreditFlowList()
|
||||
}).finally(() => {
|
||||
creditSubmitLoading.value = false
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function loadCustomer() {
|
||||
const customerId = route.query.customerId
|
||||
if (!customerId) return
|
||||
@@ -752,6 +1314,208 @@ function resetFlowQuery() {
|
||||
getFlowList()
|
||||
}
|
||||
|
||||
/* ================= 额度流水 ================= */
|
||||
const creditFlowList = ref([])
|
||||
const creditFlowTotal = ref(0)
|
||||
const creditFlowLoading = ref(false)
|
||||
const creditFlowDateRange = ref([])
|
||||
const creditFlowQuery = reactive({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
customerId: route.query.customerId,
|
||||
creditType: '',
|
||||
changeType: ''
|
||||
})
|
||||
|
||||
function getCreditFlowList() {
|
||||
creditFlowLoading.value = true
|
||||
const params = proxy.addDateRange({ ...creditFlowQuery }, creditFlowDateRange.value)
|
||||
listCreditFlow(params).then(res => {
|
||||
creditFlowList.value = res.rows || []
|
||||
creditFlowTotal.value = res.total || 0
|
||||
}).finally(() => {
|
||||
creditFlowLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function resetCreditFlowQuery() {
|
||||
creditFlowQuery.creditType = ''
|
||||
creditFlowQuery.changeType = ''
|
||||
creditFlowDateRange.value = []
|
||||
creditFlowQuery.pageNum = 1
|
||||
getCreditFlowList()
|
||||
}
|
||||
|
||||
/* ================= 账单核销 ================= */
|
||||
const billStatusMap = {
|
||||
DRAFT: { label: '草稿', type: 'info' },
|
||||
CONFIRMED: { label: '已确认', type: 'warning' },
|
||||
PARTIAL_WRITTEN_OFF: { label: '部分核销', type: 'primary' },
|
||||
WRITTEN_OFF: { label: '已核销', type: 'success' },
|
||||
VOID: { label: '已作废', type: 'danger' }
|
||||
}
|
||||
function billStatusLabel(v) {
|
||||
return billStatusMap[v]?.label || v || '-'
|
||||
}
|
||||
function billStatusTag(v) {
|
||||
return billStatusMap[v]?.type || 'info'
|
||||
}
|
||||
function formatDate(val) {
|
||||
if (!val) return '-'
|
||||
return String(val).substring(0, 10)
|
||||
}
|
||||
function goBillView() {
|
||||
router.push({
|
||||
path: '/customersuppliermanagement/customerbill',
|
||||
query: { customerId: route.query.customerId || '' }
|
||||
})
|
||||
}
|
||||
|
||||
const unsettledList = ref([])
|
||||
const unsettledLoading = ref(false)
|
||||
/** 未核销账单合计 */
|
||||
const unsettledTotal = computed(() => {
|
||||
const sum = unsettledList.value.reduce((s, r) => s + (Number(r.unwrittenAmount) || 0), 0)
|
||||
return formatMoney(sum)
|
||||
})
|
||||
|
||||
const writeoffList = ref([])
|
||||
const writeoffTotal = ref(0)
|
||||
const writeoffLoading = ref(false)
|
||||
const woQuery = reactive({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
customerId: route.query.customerId,
|
||||
status: ''
|
||||
})
|
||||
let billWriteoffLoaded = false
|
||||
|
||||
function loadUnsettledBills() {
|
||||
const customerId = route.query.customerId
|
||||
if (!customerId) return
|
||||
unsettledLoading.value = true
|
||||
listUnsettledBill(customerId).then(res => {
|
||||
unsettledList.value = res.data || []
|
||||
}).finally(() => {
|
||||
unsettledLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function getWriteoffList() {
|
||||
const customerId = route.query.customerId
|
||||
if (!customerId) return
|
||||
writeoffLoading.value = true
|
||||
woQuery.customerId = customerId
|
||||
listBalanceWriteoff({ ...woQuery }).then(res => {
|
||||
writeoffList.value = res.rows || []
|
||||
writeoffTotal.value = res.total || 0
|
||||
}).finally(() => {
|
||||
writeoffLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function resetWriteoffQuery() {
|
||||
woQuery.status = ''
|
||||
woQuery.pageNum = 1
|
||||
getWriteoffList()
|
||||
}
|
||||
|
||||
function loadBillWriteoffData() {
|
||||
loadAccounts()
|
||||
loadUnsettledBills()
|
||||
getWriteoffList()
|
||||
}
|
||||
|
||||
/* ---- 核销弹窗 ---- */
|
||||
const woSubmitLoading = ref(false)
|
||||
const woDialog = reactive({ visible: false, bill: null, form: {} })
|
||||
/** 核销后余额预览(不为负则可提交) */
|
||||
const woAfterBalance = computed(() => {
|
||||
const balance = Number(currentAccount.value.balance) || 0
|
||||
const amount = Number(woDialog.form.writeoffAmount) || 0
|
||||
return formatMoney(balance - amount)
|
||||
})
|
||||
|
||||
function openWriteoffDialog(row) {
|
||||
// 先刷新账户余额,避免用过期余额核销
|
||||
listAccount(route.query.customerId).then(res => {
|
||||
accountList.value = res.data || []
|
||||
})
|
||||
woDialog.bill = row
|
||||
const balance = Number(currentAccount.value.balance) || 0
|
||||
const unwritten = Number(row.unwrittenAmount) || 0
|
||||
woDialog.form = {
|
||||
arBillId: row.arBillId,
|
||||
customerId: Number(route.query.customerId),
|
||||
writeoffAmount: Math.min(unwritten, balance > 0 ? balance : unwritten),
|
||||
writeoffDate: proxy.parseTime(new Date(), '{y}-{m}-{d}'),
|
||||
remark: ''
|
||||
}
|
||||
woDialog.visible = true
|
||||
}
|
||||
|
||||
function submitWriteoff() {
|
||||
const bill = woDialog.bill
|
||||
const amount = Number(woDialog.form.writeoffAmount)
|
||||
if (!(amount > 0)) {
|
||||
proxy.$modal.msgError('核销金额必须大于0')
|
||||
return
|
||||
}
|
||||
const unwritten = Number(bill.unwrittenAmount) || 0
|
||||
if (amount > unwritten) {
|
||||
proxy.$modal.msgError('核销金额不能超过账单未核销金额 ' + formatMoney(unwritten))
|
||||
return
|
||||
}
|
||||
const balance = Number(currentAccount.value.balance) || 0
|
||||
if (amount > balance) {
|
||||
proxy.$modal.msgError('账户余额不足,当前余额 CNY ' + formatMoney(balance) + ',请先登记收款并审核')
|
||||
return
|
||||
}
|
||||
woSubmitLoading.value = true
|
||||
balanceWriteoff(woDialog.form).then(() => {
|
||||
proxy.$modal.msgSuccess('核销成功:账单已核销 CNY ' + formatMoney(amount))
|
||||
woDialog.visible = false
|
||||
loadBillWriteoffData()
|
||||
getFlowList()
|
||||
}).finally(() => {
|
||||
woSubmitLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
/* ---- 反核销弹窗 ---- */
|
||||
const cancelWoLoading = ref(false)
|
||||
const cancelWoDialog = reactive({ visible: false, row: null, cancelRemark: '' })
|
||||
|
||||
function openCancelWriteoffDialog(row) {
|
||||
cancelWoDialog.row = row
|
||||
cancelWoDialog.cancelRemark = ''
|
||||
cancelWoDialog.visible = true
|
||||
}
|
||||
|
||||
function submitCancelWriteoff() {
|
||||
const remark = cancelWoDialog.cancelRemark?.trim()
|
||||
if (!remark) {
|
||||
proxy.$modal.msgError('请填写反核销原因')
|
||||
return
|
||||
}
|
||||
cancelWoLoading.value = true
|
||||
cancelBalanceWriteoff(cancelWoDialog.row.writeoffId, remark).then(() => {
|
||||
proxy.$modal.msgSuccess('反核销成功,余额已退回')
|
||||
cancelWoDialog.visible = false
|
||||
loadBillWriteoffData()
|
||||
getFlowList()
|
||||
}).finally(() => {
|
||||
cancelWoLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
watch(activeTab, (tab) => {
|
||||
if (tab === 'billWriteoff' && !billWriteoffLoaded) {
|
||||
billWriteoffLoaded = true
|
||||
loadBillWriteoffData()
|
||||
}
|
||||
})
|
||||
|
||||
/* ================= 附件上传 ================= */
|
||||
const uploadFileUrl = ref(baseUrl + "/common/upload")
|
||||
const uploadHeaders = ref({ Authorization: "Bearer " + getToken() })
|
||||
@@ -848,6 +1612,8 @@ function initPage() {
|
||||
viewDialog.attachments = []
|
||||
receiptDateRange.value = []
|
||||
flowDateRange.value = []
|
||||
creditFlowDateRange.value = []
|
||||
creditDialog.visible = false
|
||||
receiptQuery.customerId = customerId
|
||||
receiptQuery.receiptType = ''
|
||||
receiptQuery.status = ''
|
||||
@@ -856,15 +1622,35 @@ function initPage() {
|
||||
flowQuery.bizType = ''
|
||||
flowQuery.direction = ''
|
||||
flowQuery.pageNum = 1
|
||||
creditFlowQuery.customerId = customerId
|
||||
creditFlowQuery.creditType = ''
|
||||
creditFlowQuery.changeType = ''
|
||||
creditFlowQuery.pageNum = 1
|
||||
receiptList.value = []
|
||||
receiptTotal.value = 0
|
||||
flowList.value = []
|
||||
flowTotal.value = 0
|
||||
creditFlowList.value = []
|
||||
creditFlowTotal.value = 0
|
||||
unsettledList.value = []
|
||||
writeoffList.value = []
|
||||
writeoffTotal.value = 0
|
||||
woQuery.customerId = customerId
|
||||
woQuery.status = ''
|
||||
woQuery.pageNum = 1
|
||||
woDialog.visible = false
|
||||
cancelWoDialog.visible = false
|
||||
billWriteoffLoaded = false
|
||||
if (!customerId) return
|
||||
loadCustomer()
|
||||
loadAccounts()
|
||||
getReceiptList()
|
||||
getFlowList()
|
||||
getCreditFlowList()
|
||||
if (activeTab.value === 'billWriteoff') {
|
||||
billWriteoffLoaded = true
|
||||
loadBillWriteoffData()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(initPage)
|
||||
@@ -916,6 +1702,31 @@ watch(() => route.query.customerId, (newId, oldId) => {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.balance-sub {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
min-height: 16px;
|
||||
}
|
||||
.credit-tip {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.credit-preview {
|
||||
margin-top: 4px;
|
||||
padding: 10px 14px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
.credit-alert {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.balance-value {
|
||||
font-size: 22px;
|
||||
@@ -952,4 +1763,28 @@ watch(() => route.query.customerId, (newId, oldId) => {
|
||||
font-weight: 600;
|
||||
margin: 12px 0 8px;
|
||||
}
|
||||
.writeoff-summary {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.wo-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
flex-wrap: wrap;
|
||||
font-size: 13px;
|
||||
}
|
||||
.wo-section-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
margin: 14px 0 8px;
|
||||
padding-left: 8px;
|
||||
border-left: 3px solid var(--el-color-primary);
|
||||
}
|
||||
.wo-form {
|
||||
margin-top: 14px;
|
||||
}
|
||||
.wo-form-unit {
|
||||
margin-left: 8px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user