✨ 完善付款申请多结算行、结算导入与计费方式回填

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-19 12:06:49 +08:00
parent 74bc8c844d
commit c5d5687999
19 changed files with 449 additions and 70 deletions
@@ -61,4 +61,6 @@ public class PaymentApplicationSaveRequest implements Serializable {
private String remark;
private List<PaymentApplicationInvoiceRequest> invoices;
private List<PaymentApplicationRecordRequest> paymentRecords;
/** 多张结算单时按行提交的付款金额 */
private List<PaymentApplicationSettlementLineRequest> settlementLines;
}
@@ -0,0 +1,43 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.pojo.dto;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
/** 付款申请多结算单付款明细。 @author Chill */
@Data
public class PaymentApplicationSettlementLineRequest implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private Long formalSettlementId;
private Long preSettlementId;
private BigDecimal paymentRatio;
private BigDecimal appliedAmount;
}
@@ -63,4 +63,10 @@ public class ReceivablePayableFeeCalculateRequest implements Serializable {
@Schema(description = "动态费用项目")
private Map<String, BigDecimal> feeItems;
@Schema(description = "运输单价")
private BigDecimal unitPrice;
@Schema(description = "本次调整字段:transportQuantity、mileage、unitPrice 或费用项名称")
private String changedField;
}
@@ -9,6 +9,7 @@
*/
package org.springblade.transport.pojo.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -99,6 +100,10 @@ public class PreSettlementDetail extends TenantEntity {
@Schema(description = "批次号")
private String batchNo;
@TableField(exist = false)
@Schema(description = "计费方式")
private String billingType;
@Schema(description = "运输单价")
private BigDecimal unitPrice;
@@ -73,6 +73,10 @@ public class ReceivablePayableDetailVO extends ReceivablePayableDetail {
@Schema(description = "状态名称")
private String settlementStatusName;
@TableField(exist = false)
@Schema(description = "计费方式")
private String billingType;
@TableField(exist = false)
@Schema(description = "费用项目")
private Map<String, Object> feeItems;
@@ -45,13 +45,13 @@ public interface UserMapper extends BaseMapper<User> {
/**
* 自定义分页
*
* @param page
* @param user
* @param deptIdList
* @param tenantId
* @return
* @param page 分页
* @param user 查询条件
* @param deptId 部门 ID(含自身及子孙,由 SQL FIND_IN_SET 过滤;为空则不过滤部门)
* @param tenantId 租户 ID
* @return 用户列表
*/
List<User> selectUserPage(IPage<User> page, @Param("user") User user, @Param("deptIdList") List<Long> deptIdList, @Param("tenantId") String tenantId);
List<User> selectUserPage(IPage<User> page, @Param("user") User user, @Param("deptId") Long deptId, @Param("tenantId") String tenantId);
/**
* 获取用户
@@ -61,20 +61,21 @@
<if test="user.status!=null and user.status>=0">
and bu.status = #{user.status}
</if>
<if test="deptIdList!=null and deptIdList.size>0">
<if test="deptId != null">
and bu.id in (
SELECT
user_id
FROM
blade_user_dept
WHERE
status = 1 and is_deleted = 0 and dept_id IN
<foreach collection="deptIdList" index="index" item="item" open="(" separator="," close=")">
#{item}
</foreach>
select distinct bud.user_id
from blade_user_dept bud
where bud.status = 1
and bud.is_deleted = 0
and bud.dept_id in (
select bd.id
from blade_dept bd
where bd.is_deleted = 0
and (bd.id = #{deptId} or find_in_set(#{deptId}, bd.ancestors) &gt; 0)
)
)
</if>
ORDER BY bu.id
ORDER BY bu.create_time DESC, bu.id DESC
</select>
<select id="getUser" resultMap="userResultMap">
@@ -367,7 +367,9 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
@Override
public List<Dept> getDeptChild(Long deptId) {
return baseMapper.selectList(Wrappers.<Dept>query().lambda().like(Dept::getAncestors, deptId));
// 必须用 FIND_IN_SET:LIKE '%1%' 会误匹配雪花 ID 中的数字片段,导致子部门集合爆炸
return baseMapper.selectList(Wrappers.<Dept>query().lambda()
.apply("FIND_IN_SET({0}, ancestors)", String.valueOf(deptId)));
}
@Override
@@ -407,8 +407,8 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
@Override
public IPage<User> selectUserPage(IPage<User> page, User user, Long deptId, String tenantId) {
normalizeUserSearch(user);
List<Long> deptIdList = SysCache.getDeptChildIds(deptId);
return page.setRecords(baseMapper.selectUserPage(page, user, deptIdList, tenantId));
// 部门过滤下推到 SQL(FIND_IN_SET),避免先展开整棵子树再拼超大 IN 列表
return page.setRecords(baseMapper.selectUserPage(page, user, deptId, tenantId));
}
@Override
@@ -77,11 +77,14 @@ public final class SettlementImportFeeReader {
private final List<Map<String, Object>> rows;
private final List<String> feeItemNames;
private final List<String> warnings;
private final List<String> headers;
public ParseResult(List<Map<String, Object>> rows, List<String> feeItemNames, List<String> warnings) {
public ParseResult(List<Map<String, Object>> rows, List<String> feeItemNames, List<String> warnings,
List<String> headers) {
this.rows = rows;
this.feeItemNames = feeItemNames;
this.warnings = warnings;
this.headers = headers;
}
public List<Map<String, Object>> getRows() {
@@ -95,6 +98,10 @@ public final class SettlementImportFeeReader {
public List<String> getWarnings() {
return warnings;
}
public List<String> getHeaders() {
return headers;
}
}
public static ParseResult read(MultipartFile file) {
@@ -140,7 +147,7 @@ public final class SettlementImportFeeReader {
rows.add(mapped);
}
}
return new ParseResult(rows, feeItemNames, List.of());
return new ParseResult(rows, feeItemNames, List.of(), new ArrayList<>(headers.values()));
} catch (ServiceException exception) {
throw exception;
} catch (Exception exception) {
@@ -212,15 +219,17 @@ public final class SettlementImportFeeReader {
}
String field = FIXED_HEADER_MAP.get(trimmedHeader);
if (field == null) {
mapped.put(trimmedHeader, blankToNull(cellString(poiCell)));
continue;
}
if (DATE_FIELDS.contains(field)) {
mapped.put(field, dateCellString(poiCell));
} else if (OPTIONAL_DECIMAL_FIELDS.contains(field)) {
mapped.put(field, optionalDecimal(cellString(poiCell), trimmedHeader, excelRowIndex));
} else if ("transportFee".equals(field) || "quantity".equals(field)
|| "settlementAmount".equals(field)) {
} else if ("transportFee".equals(field) || "quantity".equals(field)) {
mapped.put(field, decimal(cellString(poiCell), trimmedHeader, excelRowIndex));
} else if ("settlementAmount".equals(field)) {
mapped.put(field, optionalDecimal(cellString(poiCell), trimmedHeader, excelRowIndex));
} else if ("serialNumber".equals(field)) {
mapped.put(field, intValue(cellString(poiCell)));
} else {
@@ -246,7 +255,9 @@ public final class SettlementImportFeeReader {
BigDecimal feeItemTotal = feeItems.values().stream()
.reduce(BigDecimal.ZERO, BigDecimal::add)
.setScale(2, RoundingMode.HALF_UP);
BigDecimal settlementAmount = transportFee.add(feeItemTotal).setScale(2, RoundingMode.HALF_UP);
BigDecimal settlementAmount = mapped.get("settlementAmount") == null
? transportFee.add(feeItemTotal).setScale(2, RoundingMode.HALF_UP)
: toDecimal(mapped.get("settlementAmount"));
mapped.put("settlementAmount", settlementAmount);
mapped.put("结算金额", settlementAmount);
mapped.put("feeItems", feeItems);
@@ -360,7 +360,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
.in(ReceivablePayableCargoFee::getDetailId, sourceDetailIds)
.orderByAsc(ReceivablePayableCargoFee::getLineNo))
.stream()
.filter(fee -> Func.isNotEmpty(fee.getBillingType()))
.filter(fee -> Func.isNotEmpty(fee.getBillingType()) && !"-".equals(fee.getBillingType()))
.collect(Collectors.groupingBy(ReceivablePayableCargoFee::getDetailId,
Collectors.mapping(ReceivablePayableCargoFee::getBillingType,
Collectors.collectingAndThen(Collectors.toCollection(LinkedHashSet::new),
@@ -72,6 +72,7 @@ import org.springblade.transport.mapper.PreSettlementMapper;
import org.springblade.transport.mapper.ProjectApplyMapper;
import org.springblade.transport.mapper.TemporaryCreditLimitMapper;
import org.springblade.transport.pojo.dto.PaymentApplicationInvoiceRequest;
import org.springblade.transport.pojo.dto.PaymentApplicationSettlementLineRequest;
import org.springblade.transport.pojo.dto.PaymentApplicationRecordRequest;
import org.springblade.transport.pojo.dto.PaymentApplicationSaveRequest;
import org.springblade.transport.pojo.dto.PaymentApplicationStatusRequest;
@@ -369,6 +370,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
matchedInvoiceAmount = matchedInvoiceAmount.add(money(item.getMatchedAmount()));
}
entity.setMatchedInvoiceAmount(matchedInvoiceAmount);
// 进度预付只关联预结算单。编辑回传的 settlementId 可能是预结算单 ID,不能拿去查正式结算单。
normalizeSettlementReference(request);
if (TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT.equals(request.getPaymentType()) && request.getSettlementId() == null
&& request.getSettlementIds() != null && !request.getSettlementIds().isEmpty()) {
request.setSettlementId(request.getSettlementIds().get(0));
@@ -385,6 +388,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
fillBillLedger(entity, request);
validateQuota(entity);
saveOrUpdate(entity);
// not_null 更新策略不会把 settlementId 的 null 写回库,进度预付需显式清掉误存的正式结算单 ID
clearDetachedFormalReference(entity);
// 汇票付款在生成申请时即占用可用余额;编辑时按差额调整,避免重复扣减
syncBillOccupation(entity);
invoiceMapper.delete(Wrappers.<PaymentApplicationInvoice>lambdaQuery()
@@ -417,8 +422,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
entity.setInvoiceStatus(matchedInvoiceAmount.compareTo(BigDecimal.ZERO) > 0 ? "matched" : "unmatched");
entity.setPaidAmount(paidAmount);
updateById(entity);
saveSettlementRelations(entity, formalSettlements);
savePreSettlementRelations(entity, preSettlements);
saveSettlementRelations(entity, formalSettlements, request.getSettlementLines());
savePreSettlementRelations(entity, preSettlements, request.getSettlementLines());
return entity.getId();
}
@@ -1836,6 +1841,46 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
}
}
/**
* 进度预付只认预结算单。尾款付款若同时带回一个并不存在的正式结算单 ID,改为按预结算单保存。
*/
private void normalizeSettlementReference(PaymentApplicationSaveRequest request) {
if ("progress_advance".equals(request.getPaymentType())) {
if (request.getPreSettlementId() == null && request.getPreSettlementIds() != null) {
request.setPreSettlementId(request.getPreSettlementIds().stream().filter(Objects::nonNull).findFirst().orElse(null));
}
if (request.getPreSettlementId() == null) {
request.setPreSettlementId(request.getSettlementId());
}
request.setSettlementId(null);
request.setSettlementIds(null);
return;
}
if (!TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT.equals(request.getPaymentType()) || request.getSettlementId() == null) {
return;
}
boolean hasPreSettlement = request.getPreSettlementId() != null
|| (request.getPreSettlementIds() != null && request.getPreSettlementIds().stream().anyMatch(Objects::nonNull));
if (!hasPreSettlement) {
return;
}
FormalSettlement formalSettlement = formalSettlementMapper.selectById(request.getSettlementId());
if (formalSettlement == null || Objects.equals(formalSettlement.getIsDeleted(), 1)) {
request.setSettlementId(null);
request.setSettlementIds(null);
}
}
/** 预结算付款不挂正式结算单时,清掉历史误写入的正式结算单 ID,避免再次提交按正式结算单刷新。 */
private void clearDetachedFormalReference(PaymentApplication entity) {
if (entity.getId() == null || entity.getPreSettlementId() == null || entity.getSettlementId() != null) {
return;
}
baseMapper.update(null, Wrappers.<PaymentApplication>lambdaUpdate()
.eq(PaymentApplication::getId, entity.getId())
.setSql("settlement_id = null, settlement_no = null"));
}
private void fillReference(PaymentApplication entity, PaymentApplicationSaveRequest request) {
if ("project_advance".equals(request.getPaymentType())) {
ProjectApply project = projectApplyMapper.selectById(request.getProjectId());
@@ -1898,19 +1943,33 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
entity.setPayableAmount(payableAmount);
}
private void saveSettlementRelations(PaymentApplication entity, List<FormalSettlement> settlements) {
private void saveSettlementRelations(PaymentApplication entity, List<FormalSettlement> settlements,
List<PaymentApplicationSettlementLineRequest> lines) {
if (settlements.isEmpty()) return;
settlementRelationMapper.delete(Wrappers.<PaymentApplicationSettlement>lambdaQuery()
.eq(PaymentApplicationSettlement::getPaymentApplicationId, entity.getId()));
boolean customAmounts = settlements.size() > 1 && lines != null && !lines.isEmpty();
BigDecimal totalBase = settlements.stream()
.map(item -> payableOfFormalSettlement(item.getId(), item.getSettlementAmount(), entity.getId()))
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal remainingApplied = money(entity.getAppliedAmount());
BigDecimal remainingPaid = money(entity.getPaidAmount());
if (customAmounts) {
BigDecimal lineTotal = BigDecimal.ZERO;
for (FormalSettlement settlement : settlements) {
BigDecimal payable = payableOfFormalSettlement(settlement.getId(), settlement.getSettlementAmount(), entity.getId());
BigDecimal applied = this.specifiedAppliedAmount(lines, settlement.getId(), null);
this.assertAppliedWithinPayable(settlement.getFormalSettlementNo(), applied, payable);
lineTotal = lineTotal.add(applied);
}
this.assertLineTotalMatches(entity, lineTotal);
}
for (int index = 0; index < settlements.size(); index++) {
FormalSettlement settlement = settlements.get(index);
BigDecimal base = payableOfFormalSettlement(settlement.getId(), settlement.getSettlementAmount(), entity.getId());
BigDecimal applied = index == settlements.size() - 1 ? remainingApplied
BigDecimal applied = customAmounts
? this.specifiedAppliedAmount(lines, settlement.getId(), null)
: index == settlements.size() - 1 || totalBase.compareTo(BigDecimal.ZERO) == 0 ? remainingApplied
: money(entity.getAppliedAmount()).multiply(base).divide(totalBase, 2, RoundingMode.DOWN);
BigDecimal paid = index == settlements.size() - 1 ? remainingPaid
: money(entity.getPaidAmount()).multiply(applied).divide(money(entity.getAppliedAmount()).max(BigDecimal.ONE), 2, RoundingMode.DOWN);
@@ -1985,7 +2044,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
entity.setBillType("预结算单");
}
private void savePreSettlementRelations(PaymentApplication entity, List<PreSettlement> settlements) {
private void savePreSettlementRelations(PaymentApplication entity, List<PreSettlement> settlements,
List<PaymentApplicationSettlementLineRequest> lines) {
settlementRelationMapper.delete(Wrappers.<PaymentApplicationSettlement>lambdaQuery()
.eq(PaymentApplicationSettlement::getPaymentApplicationId, entity.getId())
.isNotNull(PaymentApplicationSettlement::getPreSettlementId));
@@ -2000,14 +2060,27 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
return;
}
// 单选/多选均落关联行,生成即占用可付款额度(删除草稿或审核不通过后释放)
boolean customAmounts = targetSettlements.size() > 1 && lines != null && !lines.isEmpty();
BigDecimal totalBase = targetSettlements.stream()
.map(item -> payableOfPreSettlement(item.getId(), item.getSettlementAmount(), entity.getId()))
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal remainingApplied = money(entity.getAppliedAmount());
if (customAmounts) {
BigDecimal lineTotal = BigDecimal.ZERO;
for (PreSettlement settlement : targetSettlements) {
BigDecimal payable = payableOfPreSettlement(settlement.getId(), settlement.getSettlementAmount(), entity.getId());
BigDecimal applied = this.specifiedAppliedAmount(lines, null, settlement.getId());
this.assertAppliedWithinPayable(settlement.getPreSettlementNo(), applied, payable);
lineTotal = lineTotal.add(applied);
}
this.assertLineTotalMatches(entity, lineTotal);
}
for (int index = 0; index < targetSettlements.size(); index++) {
PreSettlement settlement = targetSettlements.get(index);
BigDecimal base = payableOfPreSettlement(settlement.getId(), settlement.getSettlementAmount(), entity.getId());
BigDecimal applied = index == targetSettlements.size() - 1 || totalBase.compareTo(BigDecimal.ZERO) == 0
BigDecimal applied = customAmounts
? this.specifiedAppliedAmount(lines, null, settlement.getId())
: index == targetSettlements.size() - 1 || totalBase.compareTo(BigDecimal.ZERO) == 0
? remainingApplied
: money(entity.getAppliedAmount()).multiply(base).divide(totalBase, 2, RoundingMode.DOWN);
PaymentApplicationSettlement relation = new PaymentApplicationSettlement();
@@ -2022,6 +2095,42 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
}
}
private BigDecimal specifiedAppliedAmount(List<PaymentApplicationSettlementLineRequest> lines,
Long formalSettlementId, Long preSettlementId) {
if (lines == null) {
throw new ServiceException("请填写每张结算单的付款金额");
}
for (PaymentApplicationSettlementLineRequest line : lines) {
if (line == null) {
continue;
}
boolean matched = formalSettlementId != null
? Objects.equals(formalSettlementId, line.getFormalSettlementId())
: Objects.equals(preSettlementId, line.getPreSettlementId());
if (matched && line.getAppliedAmount() != null) {
return money(line.getAppliedAmount()).setScale(2, RoundingMode.HALF_UP);
}
}
throw new ServiceException("请填写每张结算单的付款金额");
}
private void assertAppliedWithinPayable(String settlementNo, BigDecimal appliedAmount, BigDecimal payableAmount) {
if (money(appliedAmount).compareTo(BigDecimal.ZERO) < 0) {
throw new ServiceException("付款金额不能小于0");
}
if (money(appliedAmount).compareTo(money(payableAmount)) > 0) {
String label = Func.isEmpty(settlementNo) ? "结算单" : settlementNo;
throw new ServiceException(label + "付款金额不能超过可付款金额");
}
}
private void assertLineTotalMatches(PaymentApplication entity, BigDecimal lineTotal) {
BigDecimal appliedAmount = money(entity.getAppliedAmount()).setScale(2, RoundingMode.HALF_UP);
if (lineTotal.setScale(2, RoundingMode.HALF_UP).compareTo(appliedAmount) != 0) {
throw new ServiceException("申请付款金额须等于各结算单付款金额合计");
}
}
private void validateQuota(PaymentApplication entity) {
if (!"project_advance".equals(entity.getPaymentType())) return;
BigDecimal appliedAmount = money(entity.getAppliedAmount());
@@ -2429,8 +2538,14 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
private void change(Long id, String from, String to, String node, String reason) { PaymentApplication entity = existing(id); if (!from.equals(entity.getApprovalStatus())) throw new ServiceException("当前状态不允许此操作"); entity.setApprovalStatus(to); entity.setCurrentNode(node); entity.setCurrentProcessor(AuthUtil.getUserName()); entity.setRemark(reason == null ? entity.getRemark() : limit(reason, 200)); updateById(entity); refreshFormalSettlement(entity); }
private void refreshFormalSettlement(PaymentApplication entity) {
if (entity.getSettlementId() != null) formalSettlementService.refreshPaymentSummary(entity.getSettlementId());
else if (entity.getPreSettlementId() != null) {
if (entity.getSettlementId() != null) {
FormalSettlement formalSettlement = formalSettlementMapper.selectById(entity.getSettlementId());
if (formalSettlement != null && !Objects.equals(formalSettlement.getIsDeleted(), 1)) {
formalSettlementService.refreshPaymentSummary(entity.getSettlementId());
return;
}
}
if (entity.getPreSettlementId() != null) {
formalSettlementService.refreshPaymentSummariesForPreSettlement(entity.getPreSettlementId());
}
}
@@ -186,6 +186,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
PreSettlementVO vo = PreSettlementWrapper.build().entityVO(settlement);
fillPaymentApplicationAmounts(List.of(vo));
List<PreSettlementDetail> details = listDetails(id);
fillDetailBillingTypes(details);
vo.setDetails(details);
List<Long> detailIds = details.stream().map(PreSettlementDetail::getId).toList();
Map<Long, List<PreSettlementDetailFee>> feeMap = Func.isEmpty(detailIds) ? new HashMap<>()
@@ -788,7 +789,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
.in(ReceivablePayableCargoFee::getDetailId, detailIds)
.orderByAsc(ReceivablePayableCargoFee::getLineNo))
.stream()
.filter(fee -> Func.isNotEmpty(fee.getBillingType()))
.filter(fee -> Func.isNotEmpty(fee.getBillingType()) && !"-".equals(fee.getBillingType()))
.collect(Collectors.groupingBy(ReceivablePayableCargoFee::getDetailId,
Collectors.mapping(ReceivablePayableCargoFee::getBillingType,
Collectors.collectingAndThen(Collectors.toCollection(LinkedHashSet::new),
@@ -1492,6 +1493,34 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
.eq(PreSettlementChangeRecord::getPreSettlementId, settlementId));
}
/** 预结算明细展示计费方式:按来源应收应付货物费用的计费类型汇总。 */
private void fillDetailBillingTypes(List<PreSettlementDetail> details) {
if (details == null || details.isEmpty()) {
return;
}
List<Long> sourceDetailIds = details.stream()
.map(PreSettlementDetail::getSourceDetailId)
.filter(Objects::nonNull)
.distinct()
.toList();
if (sourceDetailIds.isEmpty()) {
return;
}
Map<Long, String> billingTypeMap = sourceFeeMapper.selectList(
Wrappers.<ReceivablePayableCargoFee>lambdaQuery()
.in(ReceivablePayableCargoFee::getDetailId, sourceDetailIds)
.orderByAsc(ReceivablePayableCargoFee::getLineNo))
.stream()
.filter(fee -> Func.isNotEmpty(fee.getBillingType()) && !"-".equals(fee.getBillingType()))
.collect(Collectors.groupingBy(ReceivablePayableCargoFee::getDetailId,
Collectors.mapping(ReceivablePayableCargoFee::getBillingType,
Collectors.collectingAndThen(Collectors.toCollection(LinkedHashSet::new),
values -> String.join("、", values)))));
for (PreSettlementDetail detail : details) {
detail.setBillingType(billingTypeMap.getOrDefault(detail.getSourceDetailId(), ""));
}
}
private List<PreSettlementDetail> listDetails(Long settlementId) {
return detailMapper.selectList(Wrappers.<PreSettlementDetail>lambdaQuery()
.eq(PreSettlementDetail::getPreSettlementId, settlementId)
@@ -607,14 +607,22 @@ public class ReceivablePayableDetailServiceImpl
BigDecimal transportQuantity = money(adjusted.getTransportQuantity());
BigDecimal mileage = money(adjusted.getMileage());
BigDecimal freightAmount = money(adjusted.getFreightAmount());
BigDecimal unitPrice = adjusted.getUnitPrice() == null ? null : money(adjusted.getUnitPrice());
Map<String, BigDecimal> effectiveFeeItems = feeItems;
boolean billingBasisChanged = money(existing.getTransportQuantity()).compareTo(transportQuantity) != 0
|| money(existing.getMileage()).compareTo(mileage) != 0;
if (billingBasisChanged) {
boolean quantityChanged = money(existing.getTransportQuantity()).compareTo(transportQuantity) != 0;
boolean mileageChanged = money(existing.getMileage()).compareTo(mileage) != 0;
boolean unitPriceChanged = money(existing.getUnitPrice()).compareTo(money(unitPrice)) != 0;
if (quantityChanged || mileageChanged || unitPriceChanged) {
String changedField = quantityChanged && !mileageChanged ? "transportQuantity"
: mileageChanged && !quantityChanged ? "mileage"
: unitPriceChanged && !quantityChanged && !mileageChanged ? "unitPrice" : null;
AdjustedFeeCalculation calculation = calculateAdjustedFee(detail, existing,
transportQuantity, mileage, freightAmount, feeItems);
transportQuantity, mileage, freightAmount, feeItems, unitPrice, changedField);
freightAmount = calculation.freightAmount();
effectiveFeeItems = calculation.feeItems();
if (calculation.unitPrice() != null) {
adjusted.setUnitPrice(calculation.unitPrice().setScale(2, RoundingMode.HALF_UP));
}
}
appendChange(changes, "规格", existing.getSpecification(), adjusted.getSpecification());
appendChange(changes, "型号", existing.getModel(), adjusted.getModel());
@@ -678,15 +686,21 @@ public class ReceivablePayableDetailServiceImpl
}
validateNonNegative(request.getTransportQuantity(), "运输量");
validateNonNegative(request.getMileage(), "里程");
validateNonNegative(request.getUnitPrice(), "运输单价");
AdjustedFeeCalculation calculation = calculateAdjustedFee(detail, fee,
money(request.getTransportQuantity()), money(request.getMileage()),
money(request.getFreightAmount()), normalizeFeeItems(request.getFeeItems()));
money(request.getFreightAmount()), normalizeFeeItems(request.getFeeItems()),
request.getUnitPrice() == null ? null : money(request.getUnitPrice()),
request.getChangedField());
ReceivablePayableCargoFeeVO result = Objects.requireNonNull(
BeanUtil.copyProperties(fee, ReceivablePayableCargoFeeVO.class));
result.setTransportQuantity(request.getTransportQuantity());
result.setMileage(request.getMileage());
result.setFreightAmount(calculation.freightAmount());
result.setFeeItems(new LinkedHashMap<>(calculation.feeItems()));
if (calculation.unitPrice() != null) {
result.setUnitPrice(calculation.unitPrice().setScale(2, RoundingMode.HALF_UP));
}
BigDecimal afterAmount = adjustedAfterAmount(calculation.freightAmount(), calculation.feeItems());
result.setAdjustAmount(afterAmount.subtract(money(fee.getOriginalAmount())));
result.setAfterAmount(afterAmount);
@@ -2531,10 +2545,16 @@ public class ReceivablePayableDetailServiceImpl
private AdjustedFeeCalculation calculateAdjustedFee(ReceivablePayableDetail detail,
ReceivablePayableCargoFee fee, BigDecimal transportQuantity,
BigDecimal mileage, BigDecimal freightAmount,
Map<String, BigDecimal> feeItems) {
Map<String, BigDecimal> feeItems, BigDecimal unitPrice,
String changedField) {
if (isManualFee(fee)) {
throw new ServiceException("手工费用不支持按合同计费规则试算");
}
boolean transportChanged = Func.isEmpty(changedField)
|| List.of("transportQuantity", "mileage", "unitPrice").contains(changedField);
if (!transportChanged) {
return new AdjustedFeeCalculation(freightAmount, new LinkedHashMap<>(feeItems), null);
}
Waybill adjustedWaybill = adjustedWaybill(detail, fee, transportQuantity, mileage);
List<Map<String, Object>> rules = parseList(fee.getBillingRulesJson());
if (rules.isEmpty()) {
@@ -2550,26 +2570,94 @@ public class ReceivablePayableDetailServiceImpl
if (rules.isEmpty()) {
throw new ServiceException("未找到费用明细对应的合同计费规则,请先更新费用");
}
Map<String, BigDecimal> calculatedAmounts = new LinkedHashMap<>();
BigDecimal calculatedFreight = BigDecimal.ZERO;
boolean freightRuleMatched = false;
Map<String, BigDecimal> calculatedFeeItems = new LinkedHashMap<>(feeItems);
BigDecimal calculatedFreight = null;
BigDecimal resolvedUnitPrice = null;
boolean calculated = false;
for (Map<String, Object> rule : rules) {
BigDecimal amount = calculateRule(rule, adjustedWaybill);
if (amount == null) continue;
if (!shouldRecalculateRule(rule, changedField)) {
continue;
}
String type = stringValue(rule, "billingType", "");
boolean freightRule = isFreightRule(rule);
Map<String, Object> effectiveRule = rule;
if (freightRule && unitPriceAdjustable(type) && unitPrice != null) {
effectiveRule = new LinkedHashMap<>(rule);
effectiveRule.put("unitPrice", unitPrice);
resolvedUnitPrice = unitPrice;
}
BigDecimal amount = calculateRule(effectiveRule, adjustedWaybill);
if (amount == null) {
continue;
}
calculated = true;
String feeItem = stringValue(rule, "feeItem", "费用");
calculatedAmounts.merge(feeItem, amount, BigDecimal::add);
if (isFreightRule(rule)) {
calculatedFreight = calculatedFreight.add(amount);
freightRuleMatched = true;
calculatedFeeItems.put(feeItem, amount);
if (freightRule) {
calculatedFreight = (calculatedFreight == null ? BigDecimal.ZERO : calculatedFreight).add(amount);
if (intervalBilling(type)) {
resolvedUnitPrice = matchedIntervalUnitPrice(rule, adjustedWaybill);
}
}
}
if (calculatedAmounts.isEmpty()) {
if (!calculated && Func.isEmpty(changedField)) {
throw new ServiceException("费用明细对应的合同计费规则无法试算,请先更新费用");
}
Map<String, BigDecimal> calculatedFeeItems = new LinkedHashMap<>(feeItems);
calculatedAmounts.forEach(calculatedFeeItems::put);
return new AdjustedFeeCalculation(freightRuleMatched ? calculatedFreight : freightAmount,
calculatedFeeItems);
return new AdjustedFeeCalculation(calculatedFreight == null ? freightAmount : calculatedFreight,
calculatedFeeItems, resolvedUnitPrice);
}
/**
* 运费在运输总量、里程、可调整单价变化后按方案重算。
* 其他费用仅在计费要素使用了本次变化的运输总量或里程时重算,便于保留手工修改的费用总额。
*/
private boolean shouldRecalculateRule(Map<String, Object> rule, String changedField) {
String element = stringValue(rule, "billingElement", "");
String type = stringValue(rule, "billingType", "");
if (isFreightRule(rule)) {
return !"unitPrice".equals(changedField) || unitPriceAdjustable(type);
}
if ("unitPrice".equals(changedField) || Func.isEmpty(changedField)) {
return Func.isEmpty(changedField) && (usesTransportQuantity(element) || usesMileage(element));
}
if ("transportQuantity".equals(changedField)) {
return usesTransportQuantity(element);
}
if ("mileage".equals(changedField)) {
return usesMileage(element);
}
return false;
}
private boolean usesTransportQuantity(String element) {
return List.of("按重量", "按体积", "按数量", "按吨·公里").contains(element);
}
private boolean usesMileage(String element) {
return List.of("按里程", "按吨·公里").contains(element);
}
private boolean unitPriceAdjustable(String billingType) {
return !tierUnitPrice(billingType) && !intervalBilling(billingType);
}
private boolean tierUnitPrice(String billingType) {
return "阶梯单价".equals(billingType);
}
private boolean intervalBilling(String billingType) {
String type = billingType == null ? "" : billingType;
return "区间单价".equals(type) || (type.contains("区间") && type.contains("一口价"));
}
private BigDecimal matchedIntervalUnitPrice(Map<String, Object> rule, Waybill waybill) {
BigDecimal fallback = decimal(rule.get("unitPrice"));
return range(ranges(rule), measure(rule, waybill))
.map(matched -> {
BigDecimal matchedPrice = decimal(matched.get("unitPrice"));
return matchedPrice.signum() == 0 ? fallback : matchedPrice;
})
.orElse(fallback);
}
private Waybill adjustedWaybill(ReceivablePayableDetail detail, ReceivablePayableCargoFee fee,
@@ -2642,7 +2730,8 @@ public class ReceivablePayableDetailServiceImpl
return name != null && (name.contains("运费") || name.contains("运输费"));
}
private record AdjustedFeeCalculation(BigDecimal freightAmount, Map<String, BigDecimal> feeItems) {
private record AdjustedFeeCalculation(BigDecimal freightAmount, Map<String, BigDecimal> feeItems,
BigDecimal unitPrice) {
}
private void appendChange(List<String> changes, String field, BigDecimal before, BigDecimal after) {
@@ -2981,21 +3070,29 @@ public class ReceivablePayableDetailServiceImpl
if (detailIds.isEmpty()) return;
List<ReceivablePayableCargoFee> fees = cargoFeeMapper.selectList(
Wrappers.<ReceivablePayableCargoFee>lambdaQuery()
.select(ReceivablePayableCargoFee::getDetailId, ReceivablePayableCargoFee::getCargoName)
.select(ReceivablePayableCargoFee::getDetailId, ReceivablePayableCargoFee::getCargoName,
ReceivablePayableCargoFee::getBillingType)
.in(ReceivablePayableCargoFee::getDetailId, detailIds)
.eq(ReceivablePayableCargoFee::getIsDeleted, 0)
.orderByAsc(ReceivablePayableCargoFee::getDetailId)
.orderByAsc(ReceivablePayableCargoFee::getLineNo));
Map<Long, LinkedHashSet<String>> cargoNames = new LinkedHashMap<>();
Map<Long, LinkedHashSet<String>> billingTypes = new LinkedHashMap<>();
for (ReceivablePayableCargoFee fee : fees) {
if (Func.isEmpty(fee.getCargoName())) continue;
cargoNames.computeIfAbsent(fee.getDetailId(), key -> new LinkedHashSet<>()).add(fee.getCargoName());
if (Func.isNotEmpty(fee.getCargoName())) {
cargoNames.computeIfAbsent(fee.getDetailId(), key -> new LinkedHashSet<>()).add(fee.getCargoName());
}
if (Func.isNotEmpty(fee.getBillingType()) && !"-".equals(fee.getBillingType())) {
billingTypes.computeIfAbsent(fee.getDetailId(), key -> new LinkedHashSet<>()).add(fee.getBillingType());
}
}
details.forEach(detail -> {
Set<String> names = cargoNames.get(detail.getId());
if (Func.isNotEmpty(names)) {
detail.setCargoName(String.join(",", names));
}
Set<String> types = billingTypes.get(detail.getId());
detail.setBillingType(Func.isEmpty(types) ? "" : String.join("、", types));
});
}
@@ -115,6 +115,13 @@ public class SettlementImportBatchServiceImpl
private static final String TYPE_PAYABLE = "payable";
private static final String BILL_TYPE_FORMAL = "formal";
private static final String BILL_TYPE_PRE = "pre";
private static final String[] VEHICLE_ALIAS_KEYS = {
"*车牌号/航班号/船号/班列号",
"车牌号/航班号/船号/班列号",
"*车牌号/船班号/船次号/班次号",
"车牌号/船班号/船次号/班次号",
"车号"
};
private final IProjectApplyService projectApplyService;
private final IContractManageService contractManageService;
@@ -231,6 +238,7 @@ public class SettlementImportBatchServiceImpl
Map<String, Object> data = new LinkedHashMap<>();
data.put("rows", result.getRows());
data.put("feeItemNames", result.getFeeItemNames());
data.put("headers", result.getHeaders());
data.put("warnings", result.getWarnings());
return data;
}
@@ -263,6 +271,7 @@ public class SettlementImportBatchServiceImpl
}
List<SettlementImportTemplateExcel> failureList = new ArrayList<>();
for (Map<String, Object> row : request.getRows()) {
keepBlankVehicleNo(row);
SettlementImportTemplateExcel excel = mapToExcel(row);
excel.setErrorMessage(stringValue(row, "errorMessage", "导入失败原因", "失败原因"));
failureList.add(excel);
@@ -737,6 +746,7 @@ public class SettlementImportBatchServiceImpl
if (row == null) {
continue;
}
keepBlankVehicleNo(row);
BigDecimal transportFee = decimalValue(row, "transportFee", "*运输费", "运输费", "freight", "运费");
if (transportFee == null) {
transportFee = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
@@ -744,7 +754,12 @@ public class SettlementImportBatchServiceImpl
Map<String, BigDecimal> feeItems = extractFeeItems(row, feeItemNames);
BigDecimal feeItemTotal = feeItems.values().stream()
.reduce(BigDecimal.ZERO, BigDecimal::add).setScale(2, RoundingMode.HALF_UP);
BigDecimal settlementAmount = transportFee.add(feeItemTotal).setScale(2, RoundingMode.HALF_UP);
BigDecimal settlementAmount = decimalValue(row, "settlementAmount", "结算金额");
if (settlementAmount == null) {
settlementAmount = transportFee.add(feeItemTotal).setScale(2, RoundingMode.HALF_UP);
} else {
settlementAmount = settlementAmount.setScale(2, RoundingMode.HALF_UP);
}
row.put("transportFee", transportFee);
row.put("feeItems", feeItems);
row.put("settlementAmount", settlementAmount);
@@ -1114,7 +1129,7 @@ public class SettlementImportBatchServiceImpl
excel.setSerialNumber(intValue(row.get("serialNumber")));
excel.setOriginalNo(stringValue(row, "originalNo", "原始单号"));
excel.setLoadingIdentifier(stringValue(row, "loadingIdentifier", "配载标识号"));
excel.setVehicleNo(stringValue(row, "vehicleNo", "*车牌号/船班号/船次号/班次号"));
excel.setVehicleNo(vehicleNoValue(row));
excel.setTransportType(stringValue(row, "transportType", "*运输方式", "运输方式"));
excel.setDriverName(stringValue(row, "driverName", "司机/船长姓名"));
excel.setDriverPhone(stringValue(row, "driverPhone", "司机/船长手机号"));
@@ -1147,6 +1162,30 @@ public class SettlementImportBatchServiceImpl
return excel;
}
/**
* 车号以 vehicleNo 为准。单元格没填时不再回退到表头别名,避免失败明细和落库被自动补上车号。
*/
private void keepBlankVehicleNo(Map<String, Object> row) {
if (row == null || !row.containsKey("vehicleNo")) {
return;
}
String vehicleNo = stringValue(row, "vehicleNo");
row.put("vehicleNo", vehicleNo);
if (Func.isNotEmpty(vehicleNo)) {
return;
}
for (String alias : VEHICLE_ALIAS_KEYS) {
row.put(alias, null);
}
}
private String vehicleNoValue(Map<String, Object> row) {
if (row != null && row.containsKey("vehicleNo")) {
return stringValue(row, "vehicleNo");
}
return stringValue(row, "vehicleNo", VEHICLE_ALIAS_KEYS);
}
private Map<String, Object> option(Object id, String nameKey, Object nameValue) {
return option(id, nameKey, nameValue, null, null);
}
@@ -1351,8 +1390,15 @@ public class SettlementImportBatchServiceImpl
row.put("vehicleNo", waybill.getVehicleNo());
row.put("transportType", waybill.getTransportType());
row.put("driverName", waybill.getDriverName());
row.put("driverPhone", waybill.getDriverPhone());
row.put("departureAddress", waybill.getDepartureAddress());
row.put("departureContact", waybill.getDepartureContact());
row.put("departurePhone", waybill.getDeparturePhone());
row.put("arrivalAddress", waybill.getArrivalAddress());
row.put("arrivalContact", waybill.getArrivalContact());
row.put("arrivalPhone", waybill.getArrivalPhone());
row.put("originalNo", waybill.getOriginalNo());
row.put("loadingIdentifier", waybill.getLoadingIdentifier());
row.put("cargoName", waybill.getCargoName());
row.put("cargoType", waybill.getCargoType());
row.put("quantity", waybill.getQuantity());
@@ -465,7 +465,7 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
Waybill waybill = new Waybill();
waybill.setOriginalNo(stringValue(row, "originalNo"));
waybill.setLoadingIdentifier(stringValue(row, "loadingIdentifier", "配载标识号"));
waybill.setVehicleNo(stringValue(row, "vehicleNo", "车牌号/船班号/船次号/班次号", "车牌号/航班号/船号/班列号"));
waybill.setVehicleNo(vehicleNoFromRow(row));
waybill.setDriverId(longValue(row, "driverId", "司机ID"));
waybill.setDriverName(stringValue(row, "driverName", "司机/船长姓名", "司机/船长"));
waybill.setDriverPhone(stringValue(row, "driverPhone", "司机/船长手机号"));
@@ -542,6 +542,14 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
return null;
}
/** 车号列没填时保持为空,不从其它表头别名回填。 */
private String vehicleNoFromRow(Map<String, Object> row) {
if (row != null && row.containsKey("vehicleNo")) {
return stringValue(row, "vehicleNo");
}
return stringValue(row, "vehicleNo", "车牌号/船班号/船次号/班次号", "车牌号/航班号/船号/班列号", "车号");
}
private Long longValue(Map<String, Object> row, String field, String fieldName) {
String value = stringValue(row, field);
if (Func.isEmpty(value)) return null;
@@ -792,12 +800,12 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
}
// 检查同一配载标识号下车牌号是否一致
String currentVehicleNo = stringValue(row, "vehicleNo", "车牌号/船班号/船次号/班次号", "车牌号/航班号/船号/班列号");
String currentVehicleNo = vehicleNoFromRow(row);
for (Integer otherRowIndex : sameIdentifierRows) {
if (otherRowIndex.equals(rowIndex)) {
continue;
}
String otherVehicleNo = stringValue(allRows.get(otherRowIndex), "vehicleNo", "车牌号/船班号/船次号/班次号", "车牌号/航班号/船号/班列号");
String otherVehicleNo = vehicleNoFromRow(allRows.get(otherRowIndex));
if (Func.isNotEmpty(currentVehicleNo) && Func.isNotEmpty(otherVehicleNo)
&& !currentVehicleNo.equals(otherVehicleNo)) {
errors.add("同一配载标识号下,车牌号不一致");
@@ -808,7 +816,7 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
}
private void validateVehicleNo(Map<String, Object> row, List<String> errors) {
String vehicleNo = stringValue(row, "vehicleNo", "车牌号/船班号/船次号/班次号", "车牌号/航班号/船号/班列号");
String vehicleNo = vehicleNoFromRow(row);
String transportType = stringValue(row, "transportType", "运输类型", "*运输类型", "运输方式", "*运输方式");
if (Func.isEmpty(transportType)) {
@@ -1055,12 +1063,12 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
}
// 检查同一运单标识号下车牌号是否一致
String currentVehicleNo = stringValue(row, "vehicleNo", "车牌号/船班号/船次号/班次号", "车牌号/航班号/船号/班列号");
String currentVehicleNo = vehicleNoFromRow(row);
for (Integer otherRowIndex : sameIdentifierRows) {
if (otherRowIndex.equals(rowIndex)) {
continue;
}
String otherVehicleNo = stringValue(allRows.get(otherRowIndex), "vehicleNo", "车牌号/船班号/船次号/班次号", "车牌号/航班号/船号/班列号");
String otherVehicleNo = vehicleNoFromRow(allRows.get(otherRowIndex));
if (Func.isNotEmpty(currentVehicleNo) && Func.isNotEmpty(otherVehicleNo)
&& !currentVehicleNo.equals(otherVehicleNo)) {
errors.add("同一运单标识号下,车牌号必须一致");
@@ -1219,7 +1227,7 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
WaybillImportBatchExcel excel = new WaybillImportBatchExcel();
excel.setOriginalNo(stringValue(row, "originalNo"));
excel.setLoadingIdentifier(stringValue(row, "loadingIdentifier", "配载标识号"));
excel.setVehicleNo(stringValue(row, "vehicleNo", "车牌号/船班号/船次号/班次号", "车牌号/航班号/船号/班列号"));
excel.setVehicleNo(vehicleNoFromRow(row));
excel.setTransportType(stringValue(row, "transportType", "运输类型", "*运输类型", "运输方式", "*运输方式"));
excel.setDriverName(stringValue(row, "driverName", "司机/船长姓名", "司机/船长"));
excel.setDriverPhone(stringValue(row, "driverPhone", "司机/船长手机号"));
+3 -1
View File
@@ -1324,7 +1324,9 @@ CREATE TABLE `blade_user_dept` (
`oa_id` varchar(64) NULL DEFAULT NULL COMMENT 'OA人员ID',
`oa_login_id` varchar(128) NULL DEFAULT NULL COMMENT 'OA登录账号',
`sync_time` datetime NULL DEFAULT NULL COMMENT '同步时间',
PRIMARY KEY (`id`) USING BTREE
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_blade_user_dept_user` (`user_id`, `is_deleted`, `status`) USING BTREE,
INDEX `idx_blade_user_dept_dept` (`dept_id`, `is_deleted`, `status`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户部门表';
-- ----------------------------
@@ -0,0 +1,6 @@
-- 用户部门关联表索引:加速人员列表按组织树过滤
-- 部署时在业务库执行本脚本
ALTER TABLE `blade_user_dept`
ADD INDEX `idx_blade_user_dept_user` (`user_id`, `is_deleted`, `status`) USING BTREE,
ADD INDEX `idx_blade_user_dept_dept` (`dept_id`, `is_deleted`, `status`) USING BTREE;
+3 -1
View File
@@ -5487,7 +5487,9 @@ CREATE TABLE `blade_user_dept` (
`oa_id` varchar(64) DEFAULT NULL COMMENT 'OA人员ID',
`oa_login_id` varchar(128) DEFAULT NULL COMMENT 'OA登录账号',
`sync_time` datetime DEFAULT NULL COMMENT '同步时间',
PRIMARY KEY (`id`) USING BTREE
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_blade_user_dept_user` (`user_id`, `is_deleted`, `status`) USING BTREE,
INDEX `idx_blade_user_dept_dept` (`dept_id`, `is_deleted`, `status`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户部门表';
-- ----------------------------