Merge remote-tracking branch 'origin/master' into feature/hcs-api-p0

# Conflicts:
#	blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java
#	blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
This commit is contained in:
刘泉佳
2026-10-23 19:18:33 +08:00
85 changed files with 2415 additions and 354 deletions
@@ -207,6 +207,12 @@ public class Dept extends TenantEntity {
@Schema(description = "是否平台公司:0否,1是") @Schema(description = "是否平台公司:0否,1是")
private Integer isPlatformCompany; private Integer isPlatformCompany;
/**
* 是否业务部门:0否,1是
*/
@Schema(description = "是否业务部门:0否,1是")
private Integer isBusinessDept;
/** /**
* OA主键,仅同步记录,不对外展示 * OA主键,仅同步记录,不对外展示
*/ */
@@ -26,6 +26,10 @@ public class FormalSettlementSaveRequest implements Serializable {
private List<Long> sourcePreSettlementIds; private List<Long> sourcePreSettlementIds;
private List<Long> sourceDetailIds; private List<Long> sourceDetailIds;
private List<DetailAdjustment> detailAdjustments; private List<DetailAdjustment> detailAdjustments;
/** 业务模式(业务字典 business_model) */
private String businessModel;
/** 运输方式(业务字典 transport_type) */
private String transportType;
private LocalDate exchangeRateDate; private LocalDate exchangeRateDate;
private BigDecimal exchangeRate; private BigDecimal exchangeRate;
private String attachmentsJson; private String attachmentsJson;
@@ -55,6 +55,12 @@ public class PreSettlementSaveRequest implements Serializable {
@Schema(description = "结算类型:receivable/payable") @Schema(description = "结算类型:receivable/payable")
private String settlementType; private String settlementType;
@Schema(description = "业务模式(业务字典 business_model)")
private String businessModel;
@Schema(description = "运输方式(业务字典 transport_type)")
private String transportType;
@Schema(description = "汇率日期") @Schema(description = "汇率日期")
private LocalDate exchangeRateDate; private LocalDate exchangeRateDate;
@@ -199,6 +199,10 @@ public class ContractManage extends TenantEntity {
@TableField(updateStrategy = FieldStrategy.ALWAYS) @TableField(updateStrategy = FieldStrategy.ALWAYS)
private String changeContent; private String changeContent;
@TableField(exist = false)
@Schema(description = "变更类型")
private String changeType;
@Schema(description = "变更原因") @Schema(description = "变更原因")
@TableField(updateStrategy = FieldStrategy.ALWAYS) @TableField(updateStrategy = FieldStrategy.ALWAYS)
private String changeReason; private String changeReason;
@@ -32,6 +32,7 @@ import lombok.EqualsAndHashCode;
import org.springblade.core.tenant.mp.TenantEntity; import org.springblade.core.tenant.mp.TenantEntity;
import java.io.Serial; import java.io.Serial;
import java.math.BigDecimal;
/** /**
* 评分量化表实体类 * 评分量化表实体类
@@ -62,5 +63,10 @@ public class CreditScoreQuantification extends TenantEntity {
*/ */
@Schema(description = "标准说明") @Schema(description = "标准说明")
private String standardDescription; private String standardDescription;
/**
* 减分项目最高扣分限制:客商评分时所有减分项合计不得超过该值,超出按该上限计分
*/
@Schema(description = "减分项目最高扣分限制")
private BigDecimal maxMinusScore;
} }
@@ -115,6 +115,10 @@ public class CustomerArchive extends TenantEntity {
@Schema(description = "营业期限类型") @Schema(description = "营业期限类型")
private String businessTermType; private String businessTermType;
@Schema(description = "营业期限开始日")
@TableField(updateStrategy = FieldStrategy.ALWAYS)
private LocalDate businessStartDate;
@Schema(description = "营业期限截止日") @Schema(description = "营业期限截止日")
@TableField(updateStrategy = FieldStrategy.ALWAYS) @TableField(updateStrategy = FieldStrategy.ALWAYS)
private LocalDate businessEndDate; private LocalDate businessEndDate;
@@ -123,6 +127,10 @@ public class CustomerArchive extends TenantEntity {
@TableField(updateStrategy = FieldStrategy.ALWAYS) @TableField(updateStrategy = FieldStrategy.ALWAYS)
private BigDecimal registeredCapital; private BigDecimal registeredCapital;
@Schema(description = "实缴资本(万元)")
@TableField(updateStrategy = FieldStrategy.ALWAYS)
private BigDecimal paidInCapital;
@Schema(description = "客商负责人") @Schema(description = "客商负责人")
private String principal; private String principal;
@@ -140,6 +148,10 @@ public class CustomerArchive extends TenantEntity {
@TableField(updateStrategy = FieldStrategy.ALWAYS) @TableField(updateStrategy = FieldStrategy.ALWAYS)
private BigDecimal applyCreditLimit; private BigDecimal applyCreditLimit;
@Schema(description = "已使用额度(万元),新建可录历史值,后续由收付款业务累加/减更新")
@TableField(updateStrategy = FieldStrategy.ALWAYS)
private BigDecimal usedCreditLimit;
@Schema(description = "备注") @Schema(description = "备注")
private String remark; private String remark;
@@ -88,6 +88,14 @@ public class FormalSettlement extends TenantEntity {
* 结算金额(结算币种,含全部合计费用) * 结算金额(结算币种,含全部合计费用)
*/ */
private BigDecimal settlementAmount; private BigDecimal settlementAmount;
/**
* 业务模式(业务字典 business_model)
*/
private String businessModel;
/**
* 运输方式(业务字典 transport_type)
*/
private String transportType;
/** /**
* 本位币,默认 RMB * 本位币,默认 RMB
*/ */
@@ -92,6 +92,12 @@ public class PreSettlement extends TenantEntity {
@Schema(description = "结算金额") @Schema(description = "结算金额")
private BigDecimal settlementAmount; private BigDecimal settlementAmount;
@Schema(description = "业务模式(业务字典 business_model)")
private String businessModel;
@Schema(description = "运输方式(业务字典 transport_type)")
private String transportType;
@Schema(description = "本位币") @Schema(description = "本位币")
private String localCurrency; private String localCurrency;
@@ -29,6 +29,7 @@ import lombok.EqualsAndHashCode;
import org.springblade.transport.pojo.entity.CustomerCreditScore; import org.springblade.transport.pojo.entity.CustomerCreditScore;
import java.io.Serial; import java.io.Serial;
import java.math.BigDecimal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -57,4 +58,11 @@ public class CustomerCreditScoreVO extends CustomerCreditScore {
@Schema(description = "评估标准") @Schema(description = "评估标准")
private List<CreditRatingStandardVO> standards = new ArrayList<>(); private List<CreditRatingStandardVO> standards = new ArrayList<>();
/**
* 来自评分量化表的减分项目最高扣分限制(非落库字段,供前端计分使用)
*/
@TableField(exist = false)
@Schema(description = "减分项目最高扣分限制")
private BigDecimal maxMinusScore;
} }
@@ -43,6 +43,8 @@ public class FormalSettlementVO extends FormalSettlement implements KingdeeUnloc
@TableField(exist = false) private String ids; @TableField(exist = false) private String ids;
@TableField(exist = false) private String approvalStatusName; @TableField(exist = false) private String approvalStatusName;
@TableField(exist = false) private String settlementTypeName; @TableField(exist = false) private String settlementTypeName;
@TableField(exist = false) @Schema(description = "业务模式名称") private String businessModelName;
@TableField(exist = false) @Schema(description = "运输方式名称") private String transportTypeName;
@TableField(exist = false) private Integer attachmentCount; @TableField(exist = false) private Integer attachmentCount;
@TableField(exist = false) private String attachmentSyncStatus; @TableField(exist = false) private String attachmentSyncStatus;
@TableField(exist = false) private String attachmentSyncStatusName; @TableField(exist = false) private String attachmentSyncStatusName;
@@ -85,6 +85,14 @@ public class PreSettlementVO extends PreSettlement {
@Schema(description = "结算类型名称") @Schema(description = "结算类型名称")
private String settlementTypeName; private String settlementTypeName;
@TableField(exist = false)
@Schema(description = "业务模式名称")
private String businessModelName;
@TableField(exist = false)
@Schema(description = "运输方式名称")
private String transportTypeName;
@TableField(exist = false) @TableField(exist = false)
@Schema(description = "结算明细") @Schema(description = "结算明细")
private List<PreSettlementDetail> details; private List<PreSettlementDetail> details;
@@ -56,6 +56,9 @@ public class TripQueryVO implements Serializable {
@Schema(description = "可见组织 ID 集合:非空时只出这些组织的运单行,空集合表示不按组织过滤") @Schema(description = "可见组织 ID 集合:非空时只出这些组织的运单行,空集合表示不按组织过滤")
private List<Long> deptIds; private List<Long> deptIds;
@Schema(description = "可见项目 ID 集合:非空时只出这些项目的运单行,空集合表示不按项目过滤(小程序调度端优先用此口径)")
private List<Long> projectIds;
@Schema(description = "状态:0待接单/1进行中/2已完成/3已取消,不传为全部(不含已取消)") @Schema(description = "状态:0待接单/1进行中/2已完成/3已取消,不传为全部(不含已取消)")
private Integer status; private Integer status;
@@ -26,6 +26,7 @@ import org.springblade.core.launch.constant.AppConstant;
import org.springblade.core.tool.api.R; import org.springblade.core.tool.api.R;
import org.springblade.system.pojo.entity.MpUser; import org.springblade.system.pojo.entity.MpUser;
import org.springblade.system.pojo.entity.MpUserInfo; import org.springblade.system.pojo.entity.MpUserInfo;
import org.springblade.system.pojo.vo.MpUserDataScopeVO;
import org.springframework.cloud.openfeign.FeignClient; import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@@ -53,6 +54,7 @@ public interface IMpUserClient {
String UPDATE_USER = API_PREFIX + "/update-user"; String UPDATE_USER = API_PREFIX + "/update-user";
String BIND_OPENID = API_PREFIX + "/bind-openid"; String BIND_OPENID = API_PREFIX + "/bind-openid";
String SET_PASSWORD = API_PREFIX + "/set-password"; String SET_PASSWORD = API_PREFIX + "/set-password";
String RESOLVE_DATA_SCOPE = API_PREFIX + "/resolve-data-scope";
/** /**
* 按主键查询 * 按主键查询
@@ -60,6 +62,12 @@ public interface IMpUserClient {
@GetMapping(USER_BY_ID) @GetMapping(USER_BY_ID)
R<MpUser> userById(@RequestParam("userId") Long userId); R<MpUser> userById(@RequestParam("userId") Long userId);
/**
* 解析小程序用户可见组织(所属组织 + 数据权限范围 + 数据层级范围)
*/
@GetMapping(RESOLVE_DATA_SCOPE)
R<MpUserDataScopeVO> resolveDataScope(@RequestParam("userId") Long userId);
/** /**
* 按租户 + 手机号查询 * 按租户 + 手机号查询
*/ */
@@ -25,6 +25,7 @@
*/ */
package org.springblade.system.pojo.entity; package org.springblade.system.pojo.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@@ -35,6 +36,7 @@ import org.springblade.core.tool.sensitive.SensitiveType;
import java.io.Serial; import java.io.Serial;
import java.util.Date; import java.util.Date;
import java.util.List;
/** /**
* 小程序用户实体(仅用于小程序登录,与 blade_user 分离) * 小程序用户实体(仅用于小程序登录,与 blade_user 分离)
@@ -113,10 +115,26 @@ public class MpUser extends TenantEntity {
@Schema(description = "小程序身份") @Schema(description = "小程序身份")
private String identities; private String identities;
/** /**
* 部门id * 部门id(多选,逗号分隔)
*/ */
@Schema(description = "部门id") @Schema(description = "部门id")
private String deptId; private String deptId;
/**
* 数据权限范围:1-仅所属组织,2-全部组织,3-自定义
*/
@Schema(description = "数据权限范围")
private Integer dataScopeRange;
/**
* 数据层级范围:1-包含下级,2-仅本级
*/
@Schema(description = "数据层级范围")
private Integer dataLevelRange;
/**
* 自定义数据权限组织ID列表(dataScopeRange=3 时生效)
*/
@TableField(exist = false)
@Schema(description = "自定义数据权限组织ID")
private List<Long> dataScopeDeptIds;
/** /**
* 岗位id * 岗位id
*/ */
@@ -0,0 +1,44 @@
/**
* 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>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.system.pojo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 小程序用户组织数据权限
*
* @author Chill
*/
@Data
@TableName("blade_mp_user_data_scope")
@Schema(description = "小程序用户组织数据权限")
public class MpUserDataScope implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "主键")
private Long id;
@Schema(description = "小程序用户主键")
private Long userId;
@Schema(description = "组织主键")
private Long deptId;
}
@@ -0,0 +1,91 @@
/**
* 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.system.pojo.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* 小程序用户数据权限解析结果
* <p>
* allDept=true 表示全部组织(查询不过滤);否则按 deptIds 收口,空列表表示无可见组织。
*
* @author Chill
*/
@Data
@Schema(description = "小程序用户数据权限")
public class MpUserDataScopeVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 是否全部组织(不过滤)
*/
@Schema(description = "是否全部组织")
private boolean allDept;
/**
* 可见组织 ID;allDept=false 时生效,空表示无权限
*/
@Schema(description = "可见组织ID")
private List<Long> deptIds = new ArrayList<>();
/**
* 全部组织
*/
public static MpUserDataScopeVO all() {
MpUserDataScopeVO scope = new MpUserDataScopeVO();
scope.setAllDept(true);
return scope;
}
/**
* 无可见组织
*/
public static MpUserDataScopeVO empty() {
MpUserDataScopeVO scope = new MpUserDataScopeVO();
scope.setAllDept(false);
scope.setDeptIds(new ArrayList<>());
return scope;
}
/**
* 指定可见组织
*/
public static MpUserDataScopeVO of(List<Long> deptIds) {
MpUserDataScopeVO scope = new MpUserDataScopeVO();
scope.setAllDept(false);
scope.setDeptIds(deptIds == null ? new ArrayList<>() : new ArrayList<>(deptIds));
return scope;
}
}
@@ -42,6 +42,13 @@ public class MpUserVO extends MpUser {
@Serial @Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/**
* 所属组织名称(逗号分隔)
*/
@TableField(exist = false)
@Schema(description = "所属组织名称")
private String deptName;
/** /**
* 身份名称(逗号分隔中文) * 身份名称(逗号分隔中文)
*/ */
@@ -260,6 +260,17 @@ public class DeptController extends BladeController {
return R.data(deptService.listPlatformCompany()); return R.data(deptService.listPlatformCompany());
} }
/**
* 业务部门下拉(是否业务部门=是)
*/
@PreAuth(AuthConstant.PERMIT_ALL)
@GetMapping("/business-dept-select")
@ApiOperationSupport(order = 12)
@Operation(summary = "业务部门下拉", description = "返回是否业务部门=是的部门列表")
public R<List<Dept>> businessDeptSelect() {
return R.data(deptService.listBusinessDept());
}
/** /**
* 获取部门的主管信息 * 获取部门的主管信息
*/ */
@@ -71,7 +71,8 @@ public class MpUserController extends BladeController {
@ApiOperationSupport(order = 1) @ApiOperationSupport(order = 1)
@Operation(summary = "详情", description = "传入id") @Operation(summary = "详情", description = "传入id")
public R<MpUserVO> detail(MpUser mpUser) { public R<MpUserVO> detail(MpUser mpUser) {
MpUser detail = mpUserService.getOne(Condition.getQueryWrapper(mpUser)); Long id = mpUser == null ? null : mpUser.getId();
MpUser detail = id == null ? null : mpUserService.detail(id);
if (detail == null) { if (detail == null) {
return R.fail("小程序用户不存在"); return R.fail("小程序用户不存在");
} }
@@ -28,10 +28,12 @@ import org.springblade.core.tenant.annotation.NonDS;
import org.springblade.core.tool.api.R; import org.springblade.core.tool.api.R;
import org.springblade.system.pojo.entity.MpUser; import org.springblade.system.pojo.entity.MpUser;
import org.springblade.system.pojo.entity.MpUserInfo; import org.springblade.system.pojo.entity.MpUserInfo;
import org.springblade.system.pojo.vo.MpUserDataScopeVO;
import org.springblade.system.service.IMpUserService; import org.springblade.system.service.IMpUserService;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
/** /**
@@ -53,6 +55,12 @@ public class MpUserClient implements IMpUserClient {
return R.data(mpUserService.getById(userId)); return R.data(mpUserService.getById(userId));
} }
@Override
@GetMapping(RESOLVE_DATA_SCOPE)
public R<MpUserDataScopeVO> resolveDataScope(@RequestParam("userId") Long userId) {
return R.data(mpUserService.resolveDataScope(userId));
}
@Override @Override
@GetMapping(USER_BY_PHONE) @GetMapping(USER_BY_PHONE)
public R<MpUser> userByPhone(String tenantId, String phone) { public R<MpUser> userByPhone(String tenantId, String phone) {
@@ -20,6 +20,7 @@
<result column="mnemonic_code" property="mnemonicCode"/> <result column="mnemonic_code" property="mnemonicCode"/>
<result column="carrier_customer_id" property="carrierCustomerId"/> <result column="carrier_customer_id" property="carrierCustomerId"/>
<result column="is_platform_company" property="isPlatformCompany"/> <result column="is_platform_company" property="isPlatformCompany"/>
<result column="is_business_dept" property="isBusinessDept"/>
<result column="sort" property="sort"/> <result column="sort" property="sort"/>
<result column="remark" property="remark"/> <result column="remark" property="remark"/>
<result column="status" property="status"/> <result column="status" property="status"/>
@@ -43,6 +44,7 @@
<result column="mnemonic_code" property="mnemonicCode"/> <result column="mnemonic_code" property="mnemonicCode"/>
<result column="carrier_customer_id" property="carrierCustomerId"/> <result column="carrier_customer_id" property="carrierCustomerId"/>
<result column="is_platform_company" property="isPlatformCompany"/> <result column="is_platform_company" property="isPlatformCompany"/>
<result column="is_business_dept" property="isBusinessDept"/>
<result column="sort" property="sort"/> <result column="sort" property="sort"/>
<result column="remark" property="remark"/> <result column="remark" property="remark"/>
<result column="status" property="status"/> <result column="status" property="status"/>
@@ -0,0 +1,21 @@
/**
* 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>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springblade.system.pojo.entity.MpUserDataScope;
/**
* 小程序用户组织数据权限 Mapper
*
* @author Chill
*/
public interface MpUserDataScopeMapper extends BaseMapper<MpUserDataScope> {
}
@@ -106,6 +106,13 @@ public interface IDeptService extends IService<Dept> {
*/ */
List<Dept> listPlatformCompany(); List<Dept> listPlatformCompany();
/**
* 业务部门下拉(是否业务部门=是)
*
* @return 业务部门列表
*/
List<Dept> listBusinessDept();
/** /**
* 根据部门名称精确匹配获取部门ID集合 * 根据部门名称精确匹配获取部门ID集合
* *
@@ -26,6 +26,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import org.springblade.core.mp.base.BaseService; import org.springblade.core.mp.base.BaseService;
import org.springblade.system.pojo.entity.MpUser; import org.springblade.system.pojo.entity.MpUser;
import org.springblade.system.pojo.entity.MpUserInfo; import org.springblade.system.pojo.entity.MpUserInfo;
import org.springblade.system.pojo.vo.MpUserDataScopeVO;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -67,6 +68,18 @@ public interface IMpUserService extends BaseService<MpUser> {
*/ */
IPage<MpUser> selectMpUserPage(IPage<MpUser> page, MpUser query); IPage<MpUser> selectMpUserPage(IPage<MpUser> page, MpUser query);
/**
* 详情(含自定义数据权限组织)
*/
MpUser detail(Long id);
/**
* 解析小程序用户可见组织(所属组织 + 数据权限范围 + 数据层级范围)
* <p>
* allDept=true 表示全部组织;否则 deptIds 为可见组织,空列表表示无权限。
*/
MpUserDataScopeVO resolveDataScope(Long userId);
/** /**
* 保存小程序用户 * 保存小程序用户
*/ */
@@ -337,6 +337,18 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
return list(queryWrapper); return list(queryWrapper);
} }
@Override
public List<Dept> listBusinessDept() {
LambdaQueryWrapper<Dept> queryWrapper = Wrappers.<Dept>lambdaQuery()
.eq(Dept::getIsBusinessDept, 1)
.orderByAsc(Dept::getSort)
.orderByAsc(Dept::getId);
if (!AuthUtil.isAdministrator()) {
queryWrapper.eq(Dept::getTenantId, AuthUtil.getTenantId());
}
return list(queryWrapper);
}
@Override @Override
public String getDeptIds(String tenantId, String deptNames) { public String getDeptIds(String tenantId, String deptNames) {
List<Dept> deptList = baseMapper.selectList(Wrappers.<Dept>query().lambda().eq(Dept::getTenantId, tenantId).in(Dept::getDeptName, Func.toStrList(deptNames))); List<Dept> deptList = baseMapper.selectList(Wrappers.<Dept>query().lambda().eq(Dept::getTenantId, tenantId).in(Dept::getDeptName, Func.toStrList(deptNames)));
@@ -443,6 +455,7 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
rootDept.setIsDeleted(BladeConstant.DB_NOT_DELETED); rootDept.setIsDeleted(BladeConstant.DB_NOT_DELETED);
rootDept.setIsOa(0); rootDept.setIsOa(0);
rootDept.setIsPlatformCompany(0); rootDept.setIsPlatformCompany(0);
rootDept.setIsBusinessDept(0);
rootDept.setSyncTime(new Date()); rootDept.setSyncTime(new Date());
this.save(rootDept); this.save(rootDept);
log.info("已创建 OA 同步集团顶级 id={} name={} code={}", log.info("已创建 OA 同步集团顶级 id={} name={} code={}",
@@ -536,6 +549,7 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
newRoot.setIsDeleted(BladeConstant.DB_NOT_DELETED); newRoot.setIsDeleted(BladeConstant.DB_NOT_DELETED);
newRoot.setIsOa(0); newRoot.setIsOa(0);
newRoot.setIsPlatformCompany(0); newRoot.setIsPlatformCompany(0);
newRoot.setIsBusinessDept(0);
newRoot.setSyncTime(new Date()); newRoot.setSyncTime(new Date());
this.save(newRoot); this.save(newRoot);
log.info("迁移创建新集团顶级 id={}(来自历史 id={})", newRootId, legacyRootId); log.info("迁移创建新集团顶级 id={}(来自历史 id={})", newRootId, legacyRootId);
@@ -598,6 +612,7 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
externalDept.setIsDeleted(BladeConstant.DB_NOT_DELETED); externalDept.setIsDeleted(BladeConstant.DB_NOT_DELETED);
externalDept.setIsOa(0); externalDept.setIsOa(0);
externalDept.setIsPlatformCompany(0); externalDept.setIsPlatformCompany(0);
externalDept.setIsBusinessDept(0);
externalDept.setSyncTime(new Date()); externalDept.setSyncTime(new Date());
this.save(externalDept); this.save(externalDept);
log.info("已创建外部组织顶级 id={}", externalDept.getId()); log.info("已创建外部组织顶级 id={}", externalDept.getId());
@@ -678,6 +693,9 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
if (dept.getIsPlatformCompany() == null) { if (dept.getIsPlatformCompany() == null) {
dept.setIsPlatformCompany(0); dept.setIsPlatformCompany(0);
} }
if (dept.getIsBusinessDept() == null) {
dept.setIsBusinessDept(0);
}
// 排序未填时默认 1 // 排序未填时默认 1
if (dept.getSort() == null) { if (dept.getSort() == null) {
dept.setSort(1); dept.setSort(1);
@@ -804,6 +822,7 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
dept.setIsDeleted(BladeConstant.DB_NOT_DELETED); dept.setIsDeleted(BladeConstant.DB_NOT_DELETED);
dept.setIsOa(1); dept.setIsOa(1);
dept.setIsPlatformCompany(0); dept.setIsPlatformCompany(0);
dept.setIsBusinessDept(0);
dept.setSyncTime(new Date()); dept.setSyncTime(new Date());
boolean saved = save(dept); boolean saved = save(dept);
if (saved) { if (saved) {
@@ -877,7 +896,9 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
private void validateContractInitialSerial(Dept dept) { private void validateContractInitialSerial(Dept dept) {
Integer serial = dept.getContractInitialSerial(); Integer serial = dept.getContractInitialSerial();
if (serial == null) { // -1 为空占位,未填写时按 null 落库,不自动保留 -1
if (serial == null || serial == -1) {
dept.setContractInitialSerial(null);
return; return;
} }
if (serial < 1 || serial > 999) { if (serial < 1 || serial > 999) {
@@ -35,15 +35,20 @@ import org.springblade.core.tool.utils.DigestUtil;
import org.springblade.core.tool.utils.Func; import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.StringUtil; import org.springblade.core.tool.utils.StringUtil;
import org.springblade.system.cache.MpUserCache; import org.springblade.system.cache.MpUserCache;
import org.springblade.system.cache.SysCache;
import org.springblade.system.mapper.MpUserDataScopeMapper;
import org.springblade.system.mapper.MpUserMapper; import org.springblade.system.mapper.MpUserMapper;
import org.springblade.system.pojo.entity.MpUser; import org.springblade.system.pojo.entity.MpUser;
import org.springblade.system.pojo.entity.MpUserDataScope;
import org.springblade.system.pojo.entity.MpUserInfo; import org.springblade.system.pojo.entity.MpUserInfo;
import org.springblade.system.pojo.enums.MpUserIdentity; import org.springblade.system.pojo.enums.MpUserIdentity;
import org.springblade.system.pojo.vo.MpUserDataScopeVO;
import org.springblade.system.service.IMpUserService; import org.springblade.system.service.IMpUserService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
@@ -57,6 +62,19 @@ import java.util.Objects;
@RequiredArgsConstructor @RequiredArgsConstructor
public class MpUserServiceImpl extends BaseServiceImpl<MpUserMapper, MpUser> implements IMpUserService { public class MpUserServiceImpl extends BaseServiceImpl<MpUserMapper, MpUser> implements IMpUserService {
/** 数据权限范围:仅所属组织 */
private static final int DATA_SCOPE_OWN = 1;
/** 数据权限范围:全部组织 */
private static final int DATA_SCOPE_ALL = 2;
/** 数据权限范围:自定义 */
private static final int DATA_SCOPE_CUSTOM = 3;
/** 数据层级范围:包含下级 */
private static final int DATA_LEVEL_INCLUDE_CHILDREN = 1;
/** 数据层级范围:仅本级 */
private static final int DATA_LEVEL_CURRENT_ONLY = 2;
private final MpUserDataScopeMapper mpUserDataScopeMapper;
@Override @Override
public MpUser getByPhone(String tenantId, String phone) { public MpUser getByPhone(String tenantId, String phone) {
if (Func.isBlank(tenantId) || Func.isBlank(phone)) { if (Func.isBlank(tenantId) || Func.isBlank(phone)) {
@@ -112,6 +130,64 @@ public class MpUserServiceImpl extends BaseServiceImpl<MpUserMapper, MpUser> imp
.orderByDesc(MpUser::getCreateTime)); .orderByDesc(MpUser::getCreateTime));
} }
@Override
public MpUser detail(Long id) {
if (Func.isEmpty(id)) {
return null;
}
return fillScopes(getById(id));
}
@Override
public MpUserDataScopeVO resolveDataScope(Long userId) {
if (Func.isEmpty(userId) || userId <= 0) {
return MpUserDataScopeVO.empty();
}
MpUser user = getById(userId);
if (user == null || Func.isEmpty(user.getId())) {
return MpUserDataScopeVO.empty();
}
Integer dataScopeRange = user.getDataScopeRange() == null ? DATA_SCOPE_OWN : user.getDataScopeRange();
Integer dataLevelRange = user.getDataLevelRange() == null ? DATA_LEVEL_INCLUDE_CHILDREN : user.getDataLevelRange();
if (Objects.equals(dataScopeRange, DATA_SCOPE_ALL)) {
return MpUserDataScopeVO.all();
}
List<Long> baseDeptIds;
if (Objects.equals(dataScopeRange, DATA_SCOPE_CUSTOM)) {
baseDeptIds = mpUserDataScopeMapper.selectList(Wrappers.<MpUserDataScope>lambdaQuery()
.eq(MpUserDataScope::getUserId, userId))
.stream()
.map(MpUserDataScope::getDeptId)
.filter(Objects::nonNull)
.distinct()
.toList();
} else {
// 仅所属组织
baseDeptIds = Func.toLongList(user.getDeptId());
}
if (Func.isEmpty(baseDeptIds)) {
return MpUserDataScopeVO.empty();
}
boolean includeChildren = !Objects.equals(dataLevelRange, DATA_LEVEL_CURRENT_ONLY);
LinkedHashSet<Long> allowedDeptIds = new LinkedHashSet<>();
for (Long deptId : baseDeptIds) {
if (Func.isEmpty(deptId) || deptId <= 0) {
continue;
}
if (includeChildren) {
List<Long> childIds = SysCache.getDeptChildIds(deptId);
if (Func.isNotEmpty(childIds)) {
allowedDeptIds.addAll(childIds);
} else {
allowedDeptIds.add(deptId);
}
} else {
allowedDeptIds.add(deptId);
}
}
return MpUserDataScopeVO.of(List.copyOf(allowedDeptIds));
}
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public boolean submit(MpUser user) { public boolean submit(MpUser user) {
@@ -122,7 +198,7 @@ public class MpUserServiceImpl extends BaseServiceImpl<MpUserMapper, MpUser> imp
throw new ServiceException("该手机号已存在小程序用户"); throw new ServiceException("该手机号已存在小程序用户");
} }
encryptPasswordIfPresent(user); encryptPasswordIfPresent(user);
boolean saved = save(user); boolean saved = save(user) && submitScopes(user);
if (saved) { if (saved) {
MpUserCache.clear(); MpUserCache.clear();
} }
@@ -135,7 +211,18 @@ public class MpUserServiceImpl extends BaseServiceImpl<MpUserMapper, MpUser> imp
if (user == null || Func.isEmpty(user.getId())) { if (user == null || Func.isEmpty(user.getId())) {
throw new ServiceException("小程序用户主键不能为空"); throw new ServiceException("小程序用户主键不能为空");
} }
MpUser dbUser = getById(user.getId());
if (dbUser == null || Func.isEmpty(dbUser.getId())) {
throw new ServiceException("小程序用户不存在");
}
prepare(user); prepare(user);
// 未改手机号(前端脱敏未提交 / 仍为脱敏值)时沿用库中原值,避免把 138****8000 写回
if (Func.isBlank(user.getPhone()) || isMaskedPhone(user.getPhone())) {
user.setPhone(dbUser.getPhone());
}
if (Func.isBlank(user.getEmail()) || isMaskedEmail(user.getEmail())) {
user.setEmail(dbUser.getEmail());
}
validate(user); validate(user);
MpUser existed = getByPhone(user.getTenantId(), user.getPhone()); MpUser existed = getByPhone(user.getTenantId(), user.getPhone());
if (existed != null && Func.isNotEmpty(existed.getId()) && !existed.getId().equals(user.getId())) { if (existed != null && Func.isNotEmpty(existed.getId()) && !existed.getId().equals(user.getId())) {
@@ -143,7 +230,7 @@ public class MpUserServiceImpl extends BaseServiceImpl<MpUserMapper, MpUser> imp
} }
// 后台编辑不改密码,避免空串覆盖 // 后台编辑不改密码,避免空串覆盖
user.setPassword(null); user.setPassword(null);
boolean updated = updateById(user); boolean updated = updateById(user) && submitScopes(user);
if (updated) { if (updated) {
MpUserCache.clear(); MpUserCache.clear();
} }
@@ -167,6 +254,7 @@ public class MpUserServiceImpl extends BaseServiceImpl<MpUserMapper, MpUser> imp
} }
boolean removed = deleteLogic(ids); boolean removed = deleteLogic(ids);
if (removed) { if (removed) {
mpUserDataScopeMapper.delete(Wrappers.<MpUserDataScope>lambdaQuery().in(MpUserDataScope::getUserId, ids));
MpUserCache.clear(); MpUserCache.clear();
} }
return removed; return removed;
@@ -314,6 +402,15 @@ public class MpUserServiceImpl extends BaseServiceImpl<MpUserMapper, MpUser> imp
user.setPostId(trimToNull(user.getPostId())); user.setPostId(trimToNull(user.getPostId()));
user.setRemark(trimToNull(user.getRemark())); user.setRemark(trimToNull(user.getRemark()));
user.setIdentities(normalizeIdentities(user.getIdentities())); user.setIdentities(normalizeIdentities(user.getIdentities()));
if (user.getDataScopeRange() == null) {
user.setDataScopeRange(1);
}
if (user.getDataLevelRange() == null) {
user.setDataLevelRange(1);
}
if (!Objects.equals(user.getDataScopeRange(), 3)) {
user.setDataScopeDeptIds(List.of());
}
if (Func.isBlank(user.getAccount()) && Func.isNotBlank(user.getPhone())) { if (Func.isBlank(user.getAccount()) && Func.isNotBlank(user.getPhone())) {
user.setAccount(user.getPhone()); user.setAccount(user.getPhone());
} }
@@ -347,6 +444,9 @@ public class MpUserServiceImpl extends BaseServiceImpl<MpUserMapper, MpUser> imp
if (Func.isBlank(user.getPhone())) { if (Func.isBlank(user.getPhone())) {
throw new ServiceException("手机号不能为空"); throw new ServiceException("手机号不能为空");
} }
if (Func.isBlank(user.getDeptId())) {
throw new ServiceException("请选择所属组织");
}
if (Func.isBlank(user.getIdentities())) { if (Func.isBlank(user.getIdentities())) {
throw new ServiceException("小程序身份不能为空"); throw new ServiceException("小程序身份不能为空");
} }
@@ -359,6 +459,42 @@ public class MpUserServiceImpl extends BaseServiceImpl<MpUserMapper, MpUser> imp
throw new ServiceException("不支持的小程序身份:" + code); throw new ServiceException("不支持的小程序身份:" + code);
} }
} }
Integer dataScopeRange = user.getDataScopeRange();
if (dataScopeRange == null || (dataScopeRange != 1 && dataScopeRange != 2 && dataScopeRange != 3)) {
throw new ServiceException("数据权限范围不正确");
}
Integer dataLevelRange = user.getDataLevelRange();
if (dataLevelRange == null || (dataLevelRange != 1 && dataLevelRange != 2)) {
throw new ServiceException("数据层级范围不正确");
}
if (Objects.equals(dataScopeRange, 3) && Func.isEmpty(user.getDataScopeDeptIds())) {
throw new ServiceException("请选择自定义数据权限组织");
}
}
private boolean submitScopes(MpUser user) {
if (user == null || Func.isEmpty(user.getId())) {
return false;
}
mpUserDataScopeMapper.delete(Wrappers.<MpUserDataScope>lambdaQuery().eq(MpUserDataScope::getUserId, user.getId()));
if (Objects.equals(user.getDataScopeRange(), 3) && Func.isNotEmpty(user.getDataScopeDeptIds())) {
user.getDataScopeDeptIds().stream().filter(Objects::nonNull).distinct().forEach(deptId -> {
MpUserDataScope scope = new MpUserDataScope();
scope.setUserId(user.getId());
scope.setDeptId(deptId);
mpUserDataScopeMapper.insert(scope);
});
}
return true;
}
private MpUser fillScopes(MpUser user) {
if (user == null || Func.isEmpty(user.getId())) {
return user;
}
user.setDataScopeDeptIds(mpUserDataScopeMapper.selectList(Wrappers.<MpUserDataScope>lambdaQuery()
.eq(MpUserDataScope::getUserId, user.getId())).stream().map(MpUserDataScope::getDeptId).toList());
return user;
} }
private String normalizeIdentities(String identities) { private String normalizeIdentities(String identities) {
@@ -377,4 +513,18 @@ public class MpUserServiceImpl extends BaseServiceImpl<MpUserMapper, MpUser> imp
return trimValue.isEmpty() ? null : trimValue; return trimValue.isEmpty() ? null : trimValue;
} }
/**
* 判断是否为前端脱敏手机号(如 138****8000),不可直接落库。
*/
private boolean isMaskedPhone(String phone) {
return Func.isNotBlank(phone) && phone.contains("*");
}
/**
* 判断是否为前端脱敏邮箱。
*/
private boolean isMaskedEmail(String email) {
return Func.isNotBlank(email) && email.contains("*");
}
} }
@@ -923,7 +923,9 @@ public class OASyncServiceImpl implements IOASyncService {
.ne(Dept::getOaId, "") .ne(Dept::getOaId, "")
.select(Dept::getId, Dept::getDeptCode, Dept::getDeptName, Dept::getFullName, .select(Dept::getId, Dept::getDeptCode, Dept::getDeptName, Dept::getFullName,
Dept::getOaModified, Dept::getParentId, Dept::getOaId, Dept::getBelongCompanyCode, Dept::getOaModified, Dept::getParentId, Dept::getOaId, Dept::getBelongCompanyCode,
Dept::getParentCode, Dept::getOaSupSubComId)); Dept::getParentCode, Dept::getOaSupSubComId,
Dept::getContractCodePrefix, Dept::getContractInitialSerial,
Dept::getIsPlatformCompany, Dept::getIsBusinessDept));
Map<String, Dept> byOaId = new HashMap<>(); Map<String, Dept> byOaId = new HashMap<>();
for (Dept dept : deptList) { for (Dept dept : deptList) {
if (StringUtils.isNotBlank(dept.getOaId())) { if (StringUtils.isNotBlank(dept.getOaId())) {
@@ -986,6 +988,11 @@ public class OASyncServiceImpl implements IOASyncService {
} else { } else {
dept.setId(DefaultIdentifierGenerator.getInstance().nextId(null)); dept.setId(DefaultIdentifierGenerator.getInstance().nextId(null));
} }
// OA 不同步合同本地配置:保留已有值,并把 -1 空占位规范为 null,避免 ALWAYS 策略写回 -1/清空
preserveLocalContractFields(dept, existing);
// OA 不同步「是否平台公司 / 是否业务部门」:更新时强制保留本地已配置值,新增默认否
preserveLocalBizFlags(dept, existing);
normalizeContractInitialSerial(dept);
if (StringUtils.isNotBlank(dept.getDeptCode())) { if (StringUtils.isNotBlank(dept.getDeptCode())) {
deptIdByCode.put(dept.getDeptCode(), dept.getId()); deptIdByCode.put(dept.getDeptCode(), dept.getId());
} }
@@ -1046,6 +1053,55 @@ public class OASyncServiceImpl implements IOASyncService {
return new OrgSyncCount(syncedCount, skippedCount, notHandledList); return new OrgSyncCount(syncedCount, skippedCount, notHandledList);
} }
/**
* OA 同步对象不含合同本地配置,更新时保留库中已有合同编码前缀与初始流水号。
*/
private void preserveLocalContractFields(Dept incoming, Dept existing) {
if (incoming == null || existing == null) {
return;
}
if (StringUtils.isBlank(incoming.getContractCodePrefix())) {
incoming.setContractCodePrefix(existing.getContractCodePrefix());
}
Integer incomingSerial = incoming.getContractInitialSerial();
if (incomingSerial == null || incomingSerial == -1) {
incoming.setContractInitialSerial(existing.getContractInitialSerial());
}
}
/**
* OA 同步不携带本地业务标志:更新时保留已配置的「是否平台公司 / 是否业务部门」,新增默认否。
*/
private void preserveLocalBizFlags(Dept incoming, Dept existing) {
if (incoming == null) {
return;
}
if (existing != null) {
incoming.setIsPlatformCompany(existing.getIsPlatformCompany() != null ? existing.getIsPlatformCompany() : 0);
incoming.setIsBusinessDept(existing.getIsBusinessDept() != null ? existing.getIsBusinessDept() : 0);
return;
}
if (incoming.getIsPlatformCompany() == null) {
incoming.setIsPlatformCompany(0);
}
if (incoming.getIsBusinessDept() == null) {
incoming.setIsBusinessDept(0);
}
}
/**
* 合同初始流水号 -1 为空占位,同步落库前规范为 null。
*/
private void normalizeContractInitialSerial(Dept dept) {
if (dept == null) {
return;
}
Integer serial = dept.getContractInitialSerial();
if (serial != null && serial == -1) {
dept.setContractInitialSerial(null);
}
}
/** /**
* oaModified 一致则跳过更新;任一侧为空无法确认一致,执行更新以补齐字段。 * oaModified 一致则跳过更新;任一侧为空无法确认一致,执行更新以补齐字段。
* 组织编码已按新规则重算、父子挂载不一致、或名称字段与当前映射不一致时,即使 modified 相同也要更新。 * 组织编码已按新规则重算、父子挂载不一致、或名称字段与当前映射不一致时,即使 modified 相同也要更新。
@@ -26,6 +26,7 @@ import org.springblade.core.mp.support.BaseEntityWrapper;
import org.springblade.core.tool.utils.BeanUtil; import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.Func; import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.DictCache; import org.springblade.system.cache.DictCache;
import org.springblade.system.cache.SysCache;
import org.springblade.system.cache.UserCache; import org.springblade.system.cache.UserCache;
import org.springblade.system.pojo.entity.MpUser; import org.springblade.system.pojo.entity.MpUser;
import org.springblade.system.pojo.enums.DictEnum; import org.springblade.system.pojo.enums.DictEnum;
@@ -53,6 +54,9 @@ public class MpUserWrapper extends BaseEntityWrapper<MpUser, MpUserVO> {
// 密码哈希不下发前端 // 密码哈希不下发前端
mpUserVO.setPassword(null); mpUserVO.setPassword(null);
mpUserVO.setIdentitiesName(resolveIdentitiesName(mpUser.getIdentities())); mpUserVO.setIdentitiesName(resolveIdentitiesName(mpUser.getIdentities()));
if (Func.isNotBlank(mpUser.getDeptId())) {
mpUserVO.setDeptName(Func.join(SysCache.getDeptNames(mpUser.getDeptId())));
}
if (mpUser.getSex() != null) { if (mpUser.getSex() != null) {
mpUserVO.setSexName(DictCache.getValue(DictEnum.SEX, mpUser.getSex())); mpUserVO.setSexName(DictCache.getValue(DictEnum.SEX, mpUser.getSex()));
} }
@@ -99,6 +99,8 @@ public class FormalSettlementController extends BladeController {
excel.setContractNo(vo.getContractNo()); excel.setContractNo(vo.getContractNo());
excel.setContractName(vo.getContractName()); excel.setContractName(vo.getContractName());
excel.setSettlementAmount(formatMoney(vo.getSettlementAmount(), vo.getCurrency())); excel.setSettlementAmount(formatMoney(vo.getSettlementAmount(), vo.getCurrency()));
excel.setBusinessModelName(vo.getBusinessModelName());
excel.setTransportTypeName(vo.getTransportTypeName());
excel.setLocalSettlementAmount(formatMoney(vo.getLocalSettlementAmount(), vo.getLocalCurrency())); excel.setLocalSettlementAmount(formatMoney(vo.getLocalSettlementAmount(), vo.getLocalCurrency()));
excel.setExchangeRate(vo.getExchangeRate() == null ? "" : vo.getExchangeRate().stripTrailingZeros().toPlainString()); excel.setExchangeRate(vo.getExchangeRate() == null ? "" : vo.getExchangeRate().stripTrailingZeros().toPlainString());
excel.setInvoiceStatusName(invoiceStatusName(vo.getInvoiceStatus(), vo.getSettlementType())); excel.setInvoiceStatusName(invoiceStatusName(vo.getInvoiceStatus(), vo.getSettlementType()));
@@ -250,6 +250,8 @@ public class PreSettlementController extends BladeController {
excel.setContractNo(vo.getContractNo()); excel.setContractNo(vo.getContractNo());
excel.setContractName(vo.getContractName()); excel.setContractName(vo.getContractName());
excel.setSettlementAmount(formatMoney(vo.getSettlementAmount(), vo.getCurrency())); excel.setSettlementAmount(formatMoney(vo.getSettlementAmount(), vo.getCurrency()));
excel.setBusinessModelName(vo.getBusinessModelName());
excel.setTransportTypeName(vo.getTransportTypeName());
excel.setLocalSettlementAmount(formatMoney(vo.getLocalSettlementAmount(), vo.getLocalCurrency())); excel.setLocalSettlementAmount(formatMoney(vo.getLocalSettlementAmount(), vo.getLocalCurrency()));
excel.setExchangeRate(vo.getExchangeRate() == null ? "" : vo.getExchangeRate().stripTrailingZeros().toPlainString()); excel.setExchangeRate(vo.getExchangeRate() == null ? "" : vo.getExchangeRate().stripTrailingZeros().toPlainString());
excel.setAdvanceAppliedAmount(formatMoney(vo.getAdvanceAppliedAmount(), vo.getCurrency())); excel.setAdvanceAppliedAmount(formatMoney(vo.getAdvanceAppliedAmount(), vo.getCurrency()));
@@ -39,6 +39,10 @@ public class FormalSettlementExcel implements Serializable {
private String contractName; private String contractName;
@ExcelProperty("原币结算金额") @ExcelProperty("原币结算金额")
private String settlementAmount; private String settlementAmount;
@ExcelProperty("业务模式")
private String businessModelName;
@ExcelProperty("运输方式")
private String transportTypeName;
@ExcelProperty("本位币结算金额") @ExcelProperty("本位币结算金额")
private String localSettlementAmount; private String localSettlementAmount;
@ExcelProperty("结算汇率") @ExcelProperty("结算汇率")
@@ -50,6 +50,10 @@ public class PreSettlementExcel implements Serializable {
private String contractName; private String contractName;
@ExcelProperty("原币结算金额") @ExcelProperty("原币结算金额")
private String settlementAmount; private String settlementAmount;
@ExcelProperty("业务模式")
private String businessModelName;
@ExcelProperty("运输方式")
private String transportTypeName;
@ExcelProperty("本位币结算金额") @ExcelProperty("本位币结算金额")
private String localSettlementAmount; private String localSettlementAmount;
@ExcelProperty("结算汇率") @ExcelProperty("结算汇率")
@@ -96,8 +96,7 @@ public final class SettlementImportTemplateExporter {
private SettlementImportTemplateExporter() { private SettlementImportTemplateExporter() {
} }
public static void export(HttpServletResponse response, List<String> transportTypeOptions, public static void export(HttpServletResponse response, List<String> transportTypeOptions) {
List<String> feeItemNames) {
List<String> headers = buildHeaders(); List<String> headers = buildHeaders();
// 运输方式由调用方传入业务字典 transport_type 的字典名称,禁止写死 // 运输方式由调用方传入业务字典 transport_type 的字典名称,禁止写死
List<String> transportOptions = transportTypeOptions == null ? List.of() : transportTypeOptions.stream() List<String> transportOptions = transportTypeOptions == null ? List.of() : transportTypeOptions.stream()
@@ -106,7 +105,7 @@ public final class SettlementImportTemplateExporter {
.distinct() .distinct()
.toList(); .toList();
try (Workbook workbook = new XSSFWorkbook()) { try (Workbook workbook = new XSSFWorkbook()) {
writeInstructionSheet(workbook, transportOptions, feeItemNames); writeInstructionSheet(workbook);
Sheet dataSheet = workbook.createSheet("导入结算单模板"); Sheet dataSheet = workbook.createSheet("导入结算单模板");
CellStyle headerStyle = createHeaderStyle(workbook); CellStyle headerStyle = createHeaderStyle(workbook);
Row headerRow = dataSheet.createRow(0); Row headerRow = dataSheet.createRow(0);
@@ -137,8 +136,7 @@ public final class SettlementImportTemplateExporter {
return headers; return headers;
} }
private static void writeInstructionSheet(Workbook workbook, List<String> transportOptions, private static void writeInstructionSheet(Workbook workbook) {
List<String> feeItemNames) {
Sheet sheet = workbook.createSheet("填写说明"); Sheet sheet = workbook.createSheet("填写说明");
CellStyle titleStyle = workbook.createCellStyle(); CellStyle titleStyle = workbook.createCellStyle();
Font titleFont = workbook.createFont(); Font titleFont = workbook.createFont();
@@ -147,24 +145,11 @@ public final class SettlementImportTemplateExporter {
List<String> lines = new ArrayList<>(); List<String> lines = new ArrayList<>();
lines.add("导入结算单填写说明"); lines.add("导入结算单填写说明");
lines.add("1. 请不要修改「导入结算单模板」工作表的固定表头;带 * 号为必填项。"); lines.add("1. 请不要修改「导入结算单模板」工作表的固定表头;带 * 号为必填项。");
lines.add("2. 运输方式请从下拉项中选择,须与业务字典 transport_type 的字典名称一致。"); lines.add("2. 运输方式请从下拉项中选择。");
lines.add("3. 日期请填写 yyyy-MM-dd(如 2026-01-01);Excel 日期单元格也可直接选择。"); lines.add("3. 日期请填写 yyyy-MM-dd(如 2026-01-01);Excel 日期单元格也可直接选择。");
lines.add("4. 里程(km) 可为空;填写时须为正数。"); lines.add("4. 里程(km) 可为空;填写时须为正数。");
lines.add("5. 「*运输费」之后、「*实际发货时间」之前可按需自行插入费用项列,列名须与系统已启用费用项名称完全一致;金额不为 0 时若不存在或已停用则无法导入。"); lines.add("5. 「*运输费」之后、「*实际发货时间」之前可按需自行插入费用项列,列名须与系统已启用费用项名称完全一致;金额不为 0 时若不存在或已停用则无法导入。");
lines.add("6. 模板不含结算金额列;导入后系统按「运输费 + 各费用项」自动合计结算金额。"); lines.add("6. 模板不含结算金额列;导入后系统按「运输费 + 各费用项」自动合计结算金额。");
if (transportOptions != null && !transportOptions.isEmpty()) {
lines.add("7. 当前可选运输方式(业务字典 transport_type 名称):"
+ String.join("、", transportOptions) + "。");
} else {
lines.add("7. 运输方式请从业务字典 transport_type 维护的名称中选择。");
}
if (feeItemNames != null && !feeItemNames.isEmpty()) {
lines.add("8. 费用项请按实际需要在模板中自行增列,列名须与系统已启用费用项一致;当前系统已启用费用项:"
+ String.join("、", feeItemNames.stream().limit(30).toList())
+ (feeItemNames.size() > 30 ? " 等" : "") + "。");
} else {
lines.add("8. 费用项请按实际需要在模板中自行增列,列名须与系统已启用的费用项主数据名称完全一致。");
}
for (int index = 0; index < lines.size(); index++) { for (int index = 0; index < lines.size(); index++) {
Row row = sheet.createRow(index); Row row = sheet.createRow(index);
Cell cell = row.createCell(0); Cell cell = row.createCell(0);
@@ -33,13 +33,16 @@
<result column="business_scope" property="businessScope"/> <result column="business_scope" property="businessScope"/>
<result column="network_freight_platform" property="networkFreightPlatform"/> <result column="network_freight_platform" property="networkFreightPlatform"/>
<result column="business_term_type" property="businessTermType"/> <result column="business_term_type" property="businessTermType"/>
<result column="business_start_date" property="businessStartDate"/>
<result column="business_end_date" property="businessEndDate"/> <result column="business_end_date" property="businessEndDate"/>
<result column="registered_capital" property="registeredCapital"/> <result column="registered_capital" property="registeredCapital"/>
<result column="paid_in_capital" property="paidInCapital"/>
<result column="principal" property="principal"/> <result column="principal" property="principal"/>
<result column="mnemonic_code" property="mnemonicCode"/> <result column="mnemonic_code" property="mnemonicCode"/>
<result column="customer_level" property="customerLevel"/> <result column="customer_level" property="customerLevel"/>
<result column="max_credit_limit" property="maxCreditLimit"/> <result column="max_credit_limit" property="maxCreditLimit"/>
<result column="apply_credit_limit" property="applyCreditLimit"/> <result column="apply_credit_limit" property="applyCreditLimit"/>
<result column="used_credit_limit" property="usedCreditLimit"/>
<result column="remark" property="remark"/> <result column="remark" property="remark"/>
<result column="qualification_attachments" property="qualificationAttachments"/> <result column="qualification_attachments" property="qualificationAttachments"/>
<result column="access_type" property="accessType"/> <result column="access_type" property="accessType"/>
@@ -85,13 +88,16 @@
business_scope, business_scope,
network_freight_platform, network_freight_platform,
business_term_type, business_term_type,
business_start_date,
business_end_date, business_end_date,
registered_capital, registered_capital,
paid_in_capital,
principal, principal,
mnemonic_code, mnemonic_code,
customer_level, customer_level,
max_credit_limit, max_credit_limit,
apply_credit_limit, apply_credit_limit,
used_credit_limit,
remark, remark,
qualification_attachments, qualification_attachments,
access_type, access_type,
@@ -107,7 +107,7 @@ public class BillLedgerServiceImpl extends BaseServiceImpl<BillLedgerMapper, Bil
private List<BillLedgerVO> availableList(String keyword, Long deptId, Long selectedId, String payerName, private List<BillLedgerVO> availableList(String keyword, Long deptId, Long selectedId, String payerName,
String payeeName) { String payeeName) {
LocalDate today = LocalDate.now(); LocalDate today = LocalDate.now();
// 未到期且有余额;收票且部门可用,或出票/收票单位与结算收付款方一致(开票、收票都可) // 可选汇票:未到期且可用余额 > 0(已选中的可回显);不再按部门、开票/收票类型、结算收付款方过滤
return list(Wrappers.<BillLedger>lambdaQuery() return list(Wrappers.<BillLedger>lambdaQuery()
.and(Func.isNotEmpty(keyword), wrapper -> wrapper.like(BillLedger::getBillNo, keyword) .and(Func.isNotEmpty(keyword), wrapper -> wrapper.like(BillLedger::getBillNo, keyword)
.or().like(BillLedger::getIssuerName, keyword) .or().like(BillLedger::getIssuerName, keyword)
@@ -119,35 +119,10 @@ public class BillLedgerServiceImpl extends BaseServiceImpl<BillLedgerMapper, Bil
.or(selectedId != null, child -> child.eq(BillLedger::getId, selectedId))) .or(selectedId != null, child -> child.eq(BillLedger::getId, selectedId)))
.orderByAsc(BillLedger::getMaturityDate) .orderByAsc(BillLedger::getMaturityDate)
.orderByDesc(BillLedger::getCreateTime)).stream() .orderByDesc(BillLedger::getCreateTime)).stream()
.filter(item -> selectedId != null && Objects.equals(item.getId(), selectedId)
|| selectableBill(item, deptId, payerName, payeeName))
.map(item -> BillLedgerWrapper.build().entityVO(item)) .map(item -> BillLedgerWrapper.build().entityVO(item))
.toList(); .toList();
} }
/**
* 收票且当前部门在可用范围内,或出票单位、收票单位与结算付款方、收款方一致(顺序不限)。
*/
private boolean selectableBill(BillLedger ledger, Long deptId, String payerName, String payeeName) {
boolean receivedAndDept = "received".equals(ledger.getBillType()) && departmentAvailable(ledger, deptId);
return receivedAndDept || partiesMatch(ledger.getIssuerName(), ledger.getReceiverName(), payerName, payeeName);
}
private boolean partiesMatch(String issuerName, String receiverName, String payerName, String payeeName) {
String issuer = trimName(issuerName);
String receiver = trimName(receiverName);
String payer = trimName(payerName);
String payee = trimName(payeeName);
if (issuer.isEmpty() || receiver.isEmpty() || payer.isEmpty() || payee.isEmpty()) {
return false;
}
return (issuer.equals(payer) && receiver.equals(payee)) || (issuer.equals(payee) && receiver.equals(payer));
}
private String trimName(String value) {
return value == null ? "" : value.trim();
}
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public Long submit(BillLedgerSaveRequest request) { public Long submit(BillLedgerSaveRequest request) {
@@ -292,12 +267,6 @@ public class BillLedgerServiceImpl extends BaseServiceImpl<BillLedgerMapper, Bil
.reduce(BigDecimal.ZERO, BigDecimal::add); .reduce(BigDecimal.ZERO, BigDecimal::add);
} }
private boolean departmentAvailable(BillLedger ledger, Long deptId) {
List<?> values = parseArray(ledger.getAvailableDeptIdsJson(), "可用部门");
if (values.stream().anyMatch(value -> "all".equals(String.valueOf(value)))) return true;
return deptId == null || values.stream().anyMatch(value -> String.valueOf(deptId).equals(String.valueOf(value)));
}
private List<?> parseArray(String value, String name) { private List<?> parseArray(String value, String name) {
if (Func.isEmpty(value)) return List.of(); if (Func.isEmpty(value)) return List.of();
try { try {
@@ -32,7 +32,6 @@ import lombok.RequiredArgsConstructor;
import org.springblade.core.log.exception.ServiceException; import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl; import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.secure.utils.AuthUtil; import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.Func; import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.SysCache; import org.springblade.system.cache.SysCache;
import org.springblade.transport.constant.TransportBusiConstant; import org.springblade.transport.constant.TransportBusiConstant;
@@ -112,7 +111,7 @@ public class BillPaymentServiceImpl extends BaseServiceImpl<BillPaymentMapper, B
String deptName = required(SysCache.getDeptName(deptId), "使用部门", 100); String deptName = required(SysCache.getDeptName(deptId), "使用部门", 100);
BillLedger ledger = lockedBill(request.getBillLedgerId()); BillLedger ledger = lockedBill(request.getBillLedgerId());
BigDecimal usedAmount = positive(request.getUsedAmount(), "本次使用"); BigDecimal usedAmount = positive(request.getUsedAmount(), "本次使用");
validateLedgerAmount(ledger, usedAmount, deptId); validateLedgerAmount(ledger, usedAmount);
if (entity.getId() == null) { if (entity.getId() == null) {
entity.setPaymentNo(nextNo()); entity.setPaymentNo(nextNo());
entity.setApprovalStatus(TransportBusiConstant.DRAFT); entity.setApprovalStatus(TransportBusiConstant.DRAFT);
@@ -165,7 +164,7 @@ public class BillPaymentServiceImpl extends BaseServiceImpl<BillPaymentMapper, B
throw new ServiceException("仅审批中的汇票付款允许审核"); throw new ServiceException("仅审批中的汇票付款允许审核");
} }
BillLedger ledger = lockedBill(entity.getBillLedgerId()); BillLedger ledger = lockedBill(entity.getBillLedgerId());
validateLedgerAmount(ledger, entity.getUsedAmount(), entity.getDeptId()); validateLedgerAmount(ledger, entity.getUsedAmount());
BillLedgerUsage exists = usageMapper.selectOne(Wrappers.<BillLedgerUsage>lambdaQuery() BillLedgerUsage exists = usageMapper.selectOne(Wrappers.<BillLedgerUsage>lambdaQuery()
.eq(BillLedgerUsage::getBillPaymentId, entity.getId()) .eq(BillLedgerUsage::getBillPaymentId, entity.getId())
.eq(BillLedgerUsage::getUsageStatus, TransportBusiConstant.APPROVED) .eq(BillLedgerUsage::getUsageStatus, TransportBusiConstant.APPROVED)
@@ -245,13 +244,13 @@ public class BillPaymentServiceImpl extends BaseServiceImpl<BillPaymentMapper, B
private void validateStoredAmount(BillPayment entity) { private void validateStoredAmount(BillPayment entity) {
BillLedger ledger = lockedBill(entity.getBillLedgerId()); BillLedger ledger = lockedBill(entity.getBillLedgerId());
validateLedgerAmount(ledger, entity.getUsedAmount(), entity.getDeptId()); validateLedgerAmount(ledger, entity.getUsedAmount());
entity.setBillNo(ledger.getBillNo()); entity.setBillNo(ledger.getBillNo());
entity.setFaceAmount(ledger.getFaceAmount()); entity.setFaceAmount(ledger.getFaceAmount());
entity.setAvailableBalance(ledger.getAvailableBalance()); entity.setAvailableBalance(ledger.getAvailableBalance());
} }
private void validateLedgerAmount(BillLedger ledger, BigDecimal usedAmount, Long deptId) { private void validateLedgerAmount(BillLedger ledger, BigDecimal usedAmount) {
if (usedAmount == null || usedAmount.compareTo(BigDecimal.ZERO) <= 0) { if (usedAmount == null || usedAmount.compareTo(BigDecimal.ZERO) <= 0) {
throw new ServiceException("本次使用必须大于0"); throw new ServiceException("本次使用必须大于0");
} }
@@ -261,21 +260,6 @@ public class BillPaymentServiceImpl extends BaseServiceImpl<BillPaymentMapper, B
if (usedAmount.compareTo(money(ledger.getAvailableBalance())) > 0) { if (usedAmount.compareTo(money(ledger.getAvailableBalance())) > 0) {
throw new ServiceException("本次使用不能超过汇票可用余额"); throw new ServiceException("本次使用不能超过汇票可用余额");
} }
if (!departmentAvailable(ledger, deptId)) {
throw new ServiceException("当前使用部门不在汇票可用部门范围内");
}
}
private boolean departmentAvailable(BillLedger ledger, Long deptId) {
if (Func.isEmpty(ledger.getAvailableDeptIdsJson())) return false;
try {
Object parsed = JsonUtil.parse(ledger.getAvailableDeptIdsJson(), List.class);
if (!(parsed instanceof List<?> values)) return false;
if (values.stream().anyMatch(item -> "all".equals(String.valueOf(item)))) return true;
return deptId != null && values.stream().anyMatch(item -> String.valueOf(deptId).equals(String.valueOf(item)));
} catch (Exception exception) {
throw new ServiceException("汇票可用部门配置不正确");
}
} }
private BillLedger lockedBill(Long id) { private BillLedger lockedBill(Long id) {
@@ -40,7 +40,6 @@ import org.springblade.system.pojo.entity.Dept;
import org.springblade.transport.constant.HcsSyncConstant; import org.springblade.transport.constant.HcsSyncConstant;
import org.springblade.transport.excel.ContractManageExcel; import org.springblade.transport.excel.ContractManageExcel;
import org.springblade.transport.mapper.ContractManageMapper; import org.springblade.transport.mapper.ContractManageMapper;
import org.springblade.transport.mapper.HcsContractExeStageMapper;
import org.springblade.transport.pojo.entity.ContractManage; import org.springblade.transport.pojo.entity.ContractManage;
import org.springblade.transport.pojo.entity.HcsContractExeStage; import org.springblade.transport.pojo.entity.HcsContractExeStage;
import org.springblade.transport.pojo.vo.ContractManageVO; import org.springblade.transport.pojo.vo.ContractManageVO;
@@ -96,6 +95,8 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
private static final String ARCHIVE_STATUS_PENDING = "未归档"; private static final String ARCHIVE_STATUS_PENDING = "未归档";
private static final String ARCHIVE_STATUS_ARCHIVED = "已归档"; private static final String ARCHIVE_STATUS_ARCHIVED = "已归档";
private static final String FILE_TYPE_SEAL_ARCHIVE = "双章归档文件"; private static final String FILE_TYPE_SEAL_ARCHIVE = "双章归档文件";
private static final String CHANGE_TYPE_INFO = "合同信息变更";
private static final String CHANGE_TYPE_SETTLEMENT = "结算规则变更";
private static final long TEMPORARY_VALIDITY_DAYS = 30L; private static final long TEMPORARY_VALIDITY_DAYS = 30L;
private static final BigDecimal TAX_RATE_MIN = BigDecimal.ZERO; private static final BigDecimal TAX_RATE_MIN = BigDecimal.ZERO;
private static final BigDecimal TAX_RATE_MAX = BigDecimal.valueOf(100); private static final BigDecimal TAX_RATE_MAX = BigDecimal.valueOf(100);
@@ -313,26 +314,85 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|| !List.of(STATUS_APPROVED, STATUS_CHANGE_APPROVED, STATUS_CHANGE_REJECTED).contains(source.getApprovalStatus())) { || !List.of(STATUS_APPROVED, STATUS_CHANGE_APPROVED, STATUS_CHANGE_REJECTED).contains(source.getApprovalStatus())) {
throw new ServiceException("当前合同状态不允许发起变更"); throw new ServiceException("当前合同状态不允许发起变更");
} }
TransportBusinessSupport.validateRequired(request.getSettlementCurrency(), "请选择结算币种"); String changeType = resolveChangeType(request.getChangeType());
boolean settlementRuleOnly = Objects.equals(changeType, CHANGE_TYPE_SETTLEMENT);
String changeReason = TransportBusinessSupport.trimToNull(request.getChangeReason()); String changeReason = TransportBusinessSupport.trimToNull(request.getChangeReason());
if (Func.isEmpty(changeReason) && Objects.equals(source.getApprovalStatus(), STATUS_CHANGE_REJECTED)) { if (Func.isEmpty(changeReason) && Objects.equals(source.getApprovalStatus(), STATUS_CHANGE_REJECTED)) {
changeReason = resolveResubmitChangeReason(source); changeReason = resolveResubmitChangeReason(source);
} }
TransportBusinessSupport.validateLength(changeReason, 500, "变更原因不能超过500字"); TransportBusinessSupport.validateLength(changeReason, 500, "变更原因不能超过500字");
validateBillingPlans(request.getBillingPlanJson());
Map<String, Object> beforeData = buildContractChangeSnapshot(source);
if (settlementRuleOnly) {
applySettlementRuleChange(source, request);
} else {
applyContractInfoChange(source, request);
}
Map<String, Object> afterData = buildContractChangeSnapshot(source);
retainChangedSnapshotFields(beforeData, afterData);
source.setApprovalStatus(STATUS_CHANGE_REVIEWING);
source.setChangeContent(TransportBusinessSupport.trimToNull(request.getChangeContent()));
source.setChangeReason(changeReason);
source.setChangeAttachmentsJson(TransportBusinessSupport.trimToNull(request.getChangeAttachmentsJson()));
source.setCurrentNode("合同变更审批");
source.setCurrentProcessor("待处理");
appendChangeRecord(source, changeType, changeReason, STATUS_CHANGE_REVIEWING, "审核中", beforeData, afterData);
return updateById(source);
}
/**
* 解析并校验合同变更类型,兼容历史未传类型的请求。
*/
private String resolveChangeType(String changeType) {
String normalized = TransportBusinessSupport.trimToNull(changeType);
if (Func.isEmpty(normalized)) {
return CHANGE_TYPE_INFO;
}
if (!List.of(CHANGE_TYPE_INFO, CHANGE_TYPE_SETTLEMENT).contains(normalized)) {
throw new ServiceException("请选择变更类型");
}
return normalized;
}
/**
* 结算规则变更:仅允许更新计费信息与结算单规则。
*/
private void applySettlementRuleChange(ContractManage source, ContractManage request) {
source.setBillingEnabled(request.getBillingEnabled());
source.setFeeGenerationMode(request.getFeeGenerationMode());
source.setBillingPlanJson(request.getBillingPlanJson());
source.setSettlementRuleJson(request.getSettlementRuleJson());
source.setPreSettlementConfigJson(request.getPreSettlementConfigJson());
source.setFormalSettlementConfigJson(request.getFormalSettlementConfigJson());
}
/**
* 合同信息变更:更新合同基本信息、计费/结算规则、付款比例及附件。
*/
private void applyContractInfoChange(ContractManage source, ContractManage request) {
TransportBusinessSupport.validateRequired(request.getSettlementCurrency(), "请选择结算币种");
ContractManage candidate = new ContractManage(); ContractManage candidate = new ContractManage();
BeanUtil.copyProperties(source, candidate); BeanUtil.copyProperties(source, candidate);
candidate.setPartyB(TransportBusinessSupport.trimToNull(request.getPartyB())); candidate.setPartyB(TransportBusinessSupport.trimToNull(request.getPartyB()));
validateContractPartyCombinationUnique(candidate); validateContractPartyCombinationUnique(candidate);
validateBillingPlans(request.getBillingPlanJson());
validatePaymentRatios(request.getPaymentRatioJson()); validatePaymentRatios(request.getPaymentRatioJson());
Map<String, Object> beforeData = buildContractChangeSnapshot(source);
applyContractNo(source, request.getContractNo(), false); applyContractNo(source, request.getContractNo(), false);
source.setContractName(request.getContractName()); source.setPartyB(request.getPartyB()); source.setContractName(request.getContractName());
source.setStartDate(request.getStartDate()); source.setEndDate(request.getEndDate()); source.setContractFormat(request.getContractFormat()); source.setPartyB(request.getPartyB());
source.setSettlementMode(request.getSettlementMode()); source.setLegalSealFlag(request.getLegalSealFlag()); source.setCopyCount(request.getCopyCount()); source.setStartDate(request.getStartDate());
source.setSettlementCurrency(TransportBusinessSupport.trimToNull(request.getSettlementCurrency())); source.setInvoiceCycle(request.getInvoiceCycle()); source.setEndDate(request.getEndDate());
source.setPaymentDays(request.getPaymentDays()); source.setRemark(request.getRemark()); source.setBillingEnabled(request.getBillingEnabled()); source.setFeeGenerationMode(request.getFeeGenerationMode()); source.setContractFormat(request.getContractFormat());
source.setContractAmount(request.getContractAmount()); source.setTemplateFlag(request.getTemplateFlag()); source.setSettlementMode(request.getSettlementMode());
source.setLegalSealFlag(request.getLegalSealFlag());
source.setCopyCount(request.getCopyCount());
source.setSettlementCurrency(TransportBusinessSupport.trimToNull(request.getSettlementCurrency()));
source.setInvoiceCycle(request.getInvoiceCycle());
source.setPaymentDays(request.getPaymentDays());
source.setRemark(request.getRemark());
source.setBillingEnabled(request.getBillingEnabled());
source.setFeeGenerationMode(request.getFeeGenerationMode());
source.setContractAmount(request.getContractAmount());
source.setTemplateFlag(request.getTemplateFlag());
source.setOriginalContractNo(TransportBusinessSupport.trimToNull(request.getOriginalContractNo())); source.setOriginalContractNo(TransportBusinessSupport.trimToNull(request.getOriginalContractNo()));
source.setElectronicSealFlag(request.getElectronicSealFlag()); source.setElectronicSealFlag(request.getElectronicSealFlag());
normalizeOptionalIntegerFields(source); normalizeOptionalIntegerFields(source);
@@ -458,6 +458,10 @@ public class CreditScoreQuantificationServiceImpl extends BaseServiceImpl<Credit
quantification.setName(trimToEmpty(quantification.getName())); quantification.setName(trimToEmpty(quantification.getName()));
quantification.setRemark(trimToNull(quantification.getRemark())); quantification.setRemark(trimToNull(quantification.getRemark()));
quantification.setStandardDescription(trimToNull(quantification.getStandardDescription())); quantification.setStandardDescription(trimToNull(quantification.getStandardDescription()));
if (quantification.getMaxMinusScore() != null
&& quantification.getMaxMinusScore().compareTo(BigDecimal.ZERO) < 0) {
throw new ServiceException("减分项目最高扣分限制不能小于0");
}
if (Func.isEmpty(quantification.getStatus())) { if (Func.isEmpty(quantification.getStatus())) {
quantification.setStatus(STATUS_DRAFT); quantification.setStatus(STATUS_DRAFT);
} }
@@ -472,6 +476,10 @@ public class CreditScoreQuantificationServiceImpl extends BaseServiceImpl<Credit
validateLength(quantification.getName(), NAME_MAX_LENGTH, "评定表名称最多30字符"); validateLength(quantification.getName(), NAME_MAX_LENGTH, "评定表名称最多30字符");
validateLength(quantification.getRemark(), REMARK_MAX_LENGTH, "备注最多500字符"); validateLength(quantification.getRemark(), REMARK_MAX_LENGTH, "备注最多500字符");
validateLength(quantification.getStandardDescription(), REMARK_MAX_LENGTH, "标准说明最多500字符"); validateLength(quantification.getStandardDescription(), REMARK_MAX_LENGTH, "标准说明最多500字符");
if (quantification.getMaxMinusScore() != null
&& quantification.getMaxMinusScore().compareTo(BigDecimal.ZERO) < 0) {
throw new ServiceException("减分项目最高扣分限制不能小于0");
}
LambdaQueryWrapper<CreditScoreQuantification> queryWrapper = Wrappers.<CreditScoreQuantification>lambdaQuery() LambdaQueryWrapper<CreditScoreQuantification> queryWrapper = Wrappers.<CreditScoreQuantification>lambdaQuery()
.eq(CreditScoreQuantification::getName, quantification.getName()) .eq(CreditScoreQuantification::getName, quantification.getName())
.eq(CreditScoreQuantification::getIsDeleted, 0); .eq(CreditScoreQuantification::getIsDeleted, 0);
@@ -281,6 +281,8 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
customer.setCustomerCode(Func.isEmpty(customer.getCustomerCode()) ? this.nextCustomerCode() : customer.getCustomerCode()); customer.setCustomerCode(Func.isEmpty(customer.getCustomerCode()) ? this.nextCustomerCode() : customer.getCustomerCode());
} }
CustomerArchive entity = Objects.requireNonNull(BeanUtil.copyProperties(customer, CustomerArchive.class)); CustomerArchive entity = Objects.requireNonNull(BeanUtil.copyProperties(customer, CustomerArchive.class));
// 曾审核通过后的评分替换:保存/再提交时仍沿用原生效等级与额度,待再次审核通过后再切换
this.keepEffectiveCreditUntilReapproved(before, entity);
boolean result = saveOrUpdate(entity); boolean result = saveOrUpdate(entity);
customer.setId(entity.getId()); customer.setId(entity.getId());
replaceDetail(entity.getId(), customer); replaceDetail(entity.getId(), customer);
@@ -421,6 +423,8 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
after.setCurrentNode("审核通过"); after.setCurrentNode("审核通过");
after.setCurrentProcessor(Func.isEmpty(processorName) ? "系统" : processorName.trim()); after.setCurrentProcessor(Func.isEmpty(processorName) ? "系统" : processorName.trim());
after.setApprovedTime(LocalDateTime.now()); after.setApprovedTime(LocalDateTime.now());
// 审核通过后按当前未删除评分刷新生效等级/额度(替换旧评分场景在此切换生效)
this.applyCreditFromActiveScore(after);
addChangeRecord(id, "客商准入审核通过", before, after); addChangeRecord(id, "客商准入审核通过", before, after);
boolean updated = updateById(after); boolean updated = updateById(after);
log.info("流程结束审核通过 id={} processor={} result={}", id, after.getCurrentProcessor(), updated); log.info("流程结束审核通过 id={} processor={} result={}", id, after.getCurrentProcessor(), updated);
@@ -485,6 +489,8 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
after.setCurrentNode("审核通过"); after.setCurrentNode("审核通过");
after.setCurrentProcessor(AuthUtil.getUserName()); after.setCurrentProcessor(AuthUtil.getUserName());
after.setApprovedTime(LocalDateTime.now()); after.setApprovedTime(LocalDateTime.now());
// 审核通过后按当前未删除评分刷新生效等级/额度(替换旧评分场景在此切换生效)
this.applyCreditFromActiveScore(after);
addChangeRecord(id, "客商准入审核通过", before, after); addChangeRecord(id, "客商准入审核通过", before, after);
return updateById(after); return updateById(after);
} }
@@ -970,6 +976,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
score.setScoreDate(LocalDate.now()); score.setScoreDate(LocalDate.now());
score.setSelfStatus("未完成"); score.setSelfStatus("未完成");
score.setReviewStatus("未完成"); score.setReviewStatus("未完成");
score.setMaxMinusScore(quantification.getMaxMinusScore());
score.setDetails(loadScoreTemplateDetails(quantification.getId())); score.setDetails(loadScoreTemplateDetails(quantification.getId()));
return score; return score;
} }
@@ -991,13 +998,16 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
customer.setBusinessScope(trimToNull(customer.getBusinessScope())); customer.setBusinessScope(trimToNull(customer.getBusinessScope()));
customer.setInvoiceTaxRate(normalizeSentinelAmount(customer.getInvoiceTaxRate())); customer.setInvoiceTaxRate(normalizeSentinelAmount(customer.getInvoiceTaxRate()));
customer.setRegisteredCapital(normalizeSentinelAmount(customer.getRegisteredCapital())); customer.setRegisteredCapital(normalizeSentinelAmount(customer.getRegisteredCapital()));
customer.setPaidInCapital(normalizeSentinelAmount(customer.getPaidInCapital()));
customer.setMaxCreditLimit(normalizeSentinelAmount(customer.getMaxCreditLimit())); customer.setMaxCreditLimit(normalizeSentinelAmount(customer.getMaxCreditLimit()));
customer.setApplyCreditLimit(normalizeSentinelAmount(customer.getApplyCreditLimit())); customer.setApplyCreditLimit(normalizeSentinelAmount(customer.getApplyCreditLimit()));
customer.setUsedCreditLimit(normalizeSentinelAmount(customer.getUsedCreditLimit()));
customer.setRegisteredRegionName(trimToNull(customer.getRegisteredRegionName())); customer.setRegisteredRegionName(trimToNull(customer.getRegisteredRegionName()));
customer.setRegisteredDetailAddress(trimToNull(customer.getRegisteredDetailAddress())); customer.setRegisteredDetailAddress(trimToNull(customer.getRegisteredDetailAddress()));
customer.setRegisteredAddress(buildRegisteredAddress(customer)); customer.setRegisteredAddress(buildRegisteredAddress(customer));
customer.setLegalPerson(trimToEmpty(customer.getLegalPerson())); customer.setLegalPerson(trimToEmpty(customer.getLegalPerson()));
customer.setContactPhone(trimToEmpty(customer.getContactPhone())); customer.setContacts(customer.getContacts() == null ? new ArrayList<>() : customer.getContacts());
syncContactPhoneFromContacts(customer);
applyFixedOwnerDept(customer); applyFixedOwnerDept(customer);
customer.setAccessType(Func.isEmpty(customer.getAccessType()) ? ACCESS_TEMPORARY : customer.getAccessType()); customer.setAccessType(Func.isEmpty(customer.getAccessType()) ? ACCESS_TEMPORARY : customer.getAccessType());
customer.setApprovalStatus(Func.isEmpty(customer.getApprovalStatus()) ? APPROVAL_DRAFT : customer.getApprovalStatus()); customer.setApprovalStatus(Func.isEmpty(customer.getApprovalStatus()) ? APPROVAL_DRAFT : customer.getApprovalStatus());
@@ -1005,12 +1015,33 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
if (Func.isEmpty(customer.getStatus())) { if (Func.isEmpty(customer.getStatus())) {
customer.setStatus(STATUS_ENABLED); customer.setStatus(STATUS_ENABLED);
} }
customer.setContacts(customer.getContacts() == null ? new ArrayList<>() : customer.getContacts());
customer.setReceiptAccounts(customer.getReceiptAccounts() == null ? new ArrayList<>() : customer.getReceiptAccounts()); customer.setReceiptAccounts(customer.getReceiptAccounts() == null ? new ArrayList<>() : customer.getReceiptAccounts());
customer.setInvoices(customer.getInvoices() == null ? new ArrayList<>() : customer.getInvoices()); customer.setInvoices(customer.getInvoices() == null ? new ArrayList<>() : customer.getInvoices());
customer.setScores(customer.getScores() == null ? new ArrayList<>() : customer.getScores()); customer.setScores(customer.getScores() == null ? new ArrayList<>() : customer.getScores());
} }
/**
* 主表联系电话改由联系人维护:取第一条有效联系人手机号回填,兼容历史 NOT NULL 及金蝶同步字段。
*/
private void syncContactPhoneFromContacts(CustomerArchiveVO customer) {
if (Func.isNotEmpty(customer.getContactPhone())) {
customer.setContactPhone(trimToEmpty(customer.getContactPhone()));
return;
}
List<CustomerContactVO> contacts = customer.getContacts();
if (Func.isEmpty(contacts)) {
customer.setContactPhone(null);
return;
}
String phone = contacts.stream()
.map(CustomerContactVO::getContactPhone)
.filter(Func::isNotEmpty)
.map(String::trim)
.findFirst()
.orElse(null);
customer.setContactPhone(phone);
}
private void validateBase(CustomerArchiveVO customer) { private void validateBase(CustomerArchiveVO customer) {
if (Func.isEmpty(customer.getFullName())) { if (Func.isEmpty(customer.getFullName())) {
throw new ServiceException("客商全称不能为空"); throw new ServiceException("客商全称不能为空");
@@ -1037,10 +1068,8 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
if (Func.isEmpty(customer.getLegalPerson())) { if (Func.isEmpty(customer.getLegalPerson())) {
throw new ServiceException("法人/负责人不能为空"); throw new ServiceException("法人/负责人不能为空");
} }
if (Func.isEmpty(customer.getContactPhone())) { validateContactsRequired(customer);
throw new ServiceException("联系电话不能为空"); if (Func.isNotEmpty(customer.getContactPhone()) && !customer.getContactPhone().matches("^1\\d{10}$")) {
}
if (!customer.getContactPhone().matches("^1\\d{10}$")) {
throw new ServiceException("联系电话必须为11位手机号"); throw new ServiceException("联系电话必须为11位手机号");
} }
if (Func.isEmpty(customer.getDeptName())) { if (Func.isEmpty(customer.getDeptName())) {
@@ -1057,16 +1086,30 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
validateLength(customer.getRegisteredRegionName(), 100, "省市区最多100个字符"); validateLength(customer.getRegisteredRegionName(), 100, "省市区最多100个字符");
validateLength(customer.getRegisteredDetailAddress(), 255, "详细地址最多255个字符"); validateLength(customer.getRegisteredDetailAddress(), 255, "详细地址最多255个字符");
validateLength(customer.getRegisteredAddress(), 400, "地址最多400个字符"); validateLength(customer.getRegisteredAddress(), 400, "地址最多400个字符");
validateLength(customer.getBusinessScope(), 500, "经营范围最多500个字符"); validateLength(customer.getBusinessScope(), 2000, "经营范围最多2000个字符");
validateLength(customer.getRemark(), 500, "备注最多500个字符"); validateLength(customer.getRemark(), 500, "备注最多500个字符");
validateNonNegative(customer.getInvoiceTaxRate(), "开票税点不能小于0"); validateNonNegative(customer.getInvoiceTaxRate(), "开票税点不能小于0");
validateMax(customer.getInvoiceTaxRate(), new BigDecimal("13"), "开票税点必须在0到13之间"); validateMax(customer.getInvoiceTaxRate(), new BigDecimal("13"), "开票税点必须在0到13之间");
validateNonNegative(customer.getRegisteredCapital(), "注册资金不能小于0"); validateNonNegative(customer.getRegisteredCapital(), "注册资金不能小于0");
validateNonNegative(customer.getPaidInCapital(), "实缴资本不能小于0");
validateNonNegative(customer.getMaxCreditLimit(), "最大资金使用额度不能小于0"); validateNonNegative(customer.getMaxCreditLimit(), "最大资金使用额度不能小于0");
validateNonNegative(customer.getApplyCreditLimit(), "申请总资金使用额度不能小于0"); validateNonNegative(customer.getApplyCreditLimit(), "申请总资金使用额度不能小于0");
validateNonNegative(customer.getUsedCreditLimit(), "已使用额度不能小于0");
validateUnique(customer); validateUnique(customer);
} }
/**
* 提交客商档案时必须至少维护一条联系人。
*/
private void validateContactsRequired(CustomerArchiveVO customer) {
List<CustomerContactVO> contacts = customer.getContacts() == null ? List.of() : customer.getContacts();
boolean hasContact = contacts.stream()
.anyMatch(item -> Func.isNotEmpty(item.getContactName()) || Func.isNotEmpty(item.getContactPhone()));
if (!hasContact) {
throw new ServiceException("请至少维护一条联系人");
}
}
private void validateCustomerType(String customerType) { private void validateCustomerType(String customerType) {
List<String> typeList = Func.toStrList(customerType.replace(",", ",")) List<String> typeList = Func.toStrList(customerType.replace(",", ","))
.stream() .stream()
@@ -1393,6 +1436,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
} }
private void deleteScores(List<Long> customerIds) { private void deleteScores(List<Long> customerIds) {
// 逻辑删除保留历史评分明细;曾审核通过的旧评分在再次审核通过前仍通过主表等级/额度暂时生效
List<CustomerCreditScore> scores = creditScoreMapper.selectList(Wrappers.<CustomerCreditScore>lambdaQuery() List<CustomerCreditScore> scores = creditScoreMapper.selectList(Wrappers.<CustomerCreditScore>lambdaQuery()
.in(CustomerCreditScore::getCustomerId, customerIds) .in(CustomerCreditScore::getCustomerId, customerIds)
.and(wrapper -> wrapper.ne(CustomerCreditScore::getIsDeleted, 1).or().isNull(CustomerCreditScore::getIsDeleted))); .and(wrapper -> wrapper.ne(CustomerCreditScore::getIsDeleted, 1).or().isNull(CustomerCreditScore::getIsDeleted)));
@@ -1403,6 +1447,42 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
} }
} }
/**
* 客商曾审核通过后,保存或再次提审时不覆盖主表已生效的等级/额度。
*/
private void keepEffectiveCreditUntilReapproved(CustomerArchive before, CustomerArchive entity) {
if (before == null || before.getApprovedTime() == null || entity == null) {
return;
}
entity.setCustomerLevel(before.getCustomerLevel());
entity.setMaxCreditLimit(before.getMaxCreditLimit());
entity.setApplyCreditLimit(before.getApplyCreditLimit());
}
/**
* 按当前未删除评分刷新客商生效等级与额度;优先取复评已完成的评分。
*/
private void applyCreditFromActiveScore(CustomerArchive customer) {
if (customer == null || customer.getId() == null) {
return;
}
List<CustomerCreditScoreVO> scores = loadScores(customer.getId());
if (Func.isEmpty(scores)) {
return;
}
CustomerCreditScoreVO activeScore = scores.stream()
.filter(score -> "已完成".equals(score.getReviewStatus()))
.findFirst()
.orElse(scores.get(0));
if (Func.isNotEmpty(activeScore.getCreditLevel())) {
customer.setCustomerLevel(activeScore.getCreditLevel());
}
if (activeScore.getMaxCreditLimit() != null) {
customer.setMaxCreditLimit(activeScore.getMaxCreditLimit());
customer.setApplyCreditLimit(activeScore.getMaxCreditLimit());
}
}
private List<CustomerContactVO> loadContacts(Long customerId) { private List<CustomerContactVO> loadContacts(Long customerId) {
return contactMapper.selectList(Wrappers.<CustomerContact>lambdaQuery() return contactMapper.selectList(Wrappers.<CustomerContact>lambdaQuery()
.eq(CustomerContact::getCustomerId, customerId) .eq(CustomerContact::getCustomerId, customerId)
@@ -2069,13 +2149,16 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
snapshot.put("开票税点", customer.getInvoiceTaxRate()); snapshot.put("开票税点", customer.getInvoiceTaxRate());
snapshot.put("经营范围", customer.getBusinessScope()); snapshot.put("经营范围", customer.getBusinessScope());
snapshot.put("营业期限类型", customer.getBusinessTermType()); snapshot.put("营业期限类型", customer.getBusinessTermType());
snapshot.put("营业期限开始日", customer.getBusinessStartDate());
snapshot.put("营业期限截止日", customer.getBusinessEndDate()); snapshot.put("营业期限截止日", customer.getBusinessEndDate());
snapshot.put("注册资金(万元)", customer.getRegisteredCapital()); snapshot.put("注册资金(万元)", customer.getRegisteredCapital());
snapshot.put("实缴资本(万元)", customer.getPaidInCapital());
snapshot.put("客商负责人", customer.getPrincipal()); snapshot.put("客商负责人", customer.getPrincipal());
snapshot.put("助记码", customer.getMnemonicCode()); snapshot.put("助记码", customer.getMnemonicCode());
snapshot.put("客户等级", customer.getCustomerLevel()); snapshot.put("客户等级", customer.getCustomerLevel());
snapshot.put("最大资金使用额度(万元)", customer.getMaxCreditLimit()); snapshot.put("最大资金使用额度(万元)", customer.getMaxCreditLimit());
snapshot.put("申请总资金使用额度(万元)", customer.getApplyCreditLimit()); snapshot.put("申请总资金使用额度(万元)", customer.getApplyCreditLimit());
snapshot.put("已使用额度(万元)", customer.getUsedCreditLimit());
snapshot.put("备注", customer.getRemark()); snapshot.put("备注", customer.getRemark());
snapshot.put("客商材料", customer.getQualificationAttachments()); snapshot.put("客商材料", customer.getQualificationAttachments());
snapshot.put("准入类型", customer.getAccessType()); snapshot.put("准入类型", customer.getAccessType());
@@ -40,11 +40,14 @@ import org.springblade.thirdparty.bfi.pojo.dto.BfiCustomerSaveDTO;
import org.springblade.transport.config.BfiProperties; import org.springblade.transport.config.BfiProperties;
import org.springblade.transport.constant.CustomerBfiConstants; import org.springblade.transport.constant.CustomerBfiConstants;
import org.springblade.transport.pojo.vo.CustomerArchiveVO; import org.springblade.transport.pojo.vo.CustomerArchiveVO;
import org.springblade.transport.pojo.vo.CustomerContactVO;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@@ -86,12 +89,13 @@ public class CustomerBfiConverter {
param.setType(this.resolvePartnerType(customer.getCustomerNature())); param.setType(this.resolvePartnerType(customer.getCustomerNature()));
// 详细地址:注册地址(省市区 + 详细地址拼接文本) // 详细地址:注册地址(省市区 + 详细地址拼接文本)
param.setBizpartnerAddress(customer.getRegisteredAddress()); param.setBizpartnerAddress(customer.getRegisteredAddress());
// 经营范围:多选编码 → 字典文本「、」拼接 // 经营范围:长文本直传;兼容历史多选编码→字典文本
param.setBusinessscope(this.buildBusinessScopeText(customer.getBusinessScope())); param.setBusinessscope(this.buildBusinessScopeText(customer.getBusinessScope()));
// 营业期限:长期传「长期」,固定期限传「至 yyyy-MM-dd」(TMS 未采集起始日) // 营业期限:长期传「长期」,固定期限传「yyyy-MM-dd 至 yyyy-MM-dd」
param.setBusinessterm(this.buildBusinessTerm(customer.getBusinessTermType(), customer.getBusinessEndDate())); param.setBusinessterm(this.buildBusinessTerm(
// 联系电话:客商联系电话 customer.getBusinessTermType(), customer.getBusinessStartDate(), customer.getBusinessEndDate()));
param.setBizpartnerPhone(customer.getContactPhone()); // 联系电话:优先主表,空时回退首条联系人
param.setBizpartnerPhone(this.resolveContactPhone(customer));
// 备注:客商备注,空时不传 // 备注:客商备注,空时不传
param.setFcusbeizhu(customer.getRemark()); param.setFcusbeizhu(customer.getRemark());
// 业务职能:固定全选(1销售,2结算,3付款,4收货) // 业务职能:固定全选(1销售,2结算,3付款,4收货)
@@ -107,7 +111,7 @@ public class CustomerBfiConverter {
param.setEntryGroupstandard(List.of(groupEntry)); param.setEntryGroupstandard(List.of(groupEntry));
// 银行信息分录:收款账户明细(账户名称传开户行,币种默认 RMB) // 银行信息分录:收款账户明细(账户名称传开户行,币种默认 RMB)
param.setEntryBank(this.buildBankEntries(customer)); param.setEntryBank(this.buildBankEntries(customer));
// 联系人信息分录:页面填写的客商负责人与联系电话 // 联系人信息分录:客商联系人明细(兼容历史负责人字段)
param.setEntryLinkman(this.buildLinkmanEntries(customer)); param.setEntryLinkman(this.buildLinkmanEntries(customer));
return param; return param;
} }
@@ -175,25 +179,58 @@ public class CustomerBfiConverter {
} }
/** /**
* 组装 A902-2 联系人信息分录:页面填写的客商负责人与联系电话,未填负责人时返回空分录 * 组装 A902-2 联系人信息分录:优先推送客商联系人明细;无明细时兼容历史「负责人+联系电话」。
* *
* @param customer 客商档案 * @param customer 客商档案
* @return 联系人信息分录 * @return 联系人信息分录
*/ */
private List<BfiCustomerLinkmanEntryDTO> buildLinkmanEntries(CustomerArchiveVO customer) { private List<BfiCustomerLinkmanEntryDTO> buildLinkmanEntries(CustomerArchiveVO customer) {
if (Func.isNotEmpty(customer.getContacts())) {
List<BfiCustomerLinkmanEntryDTO> entries = new ArrayList<>();
boolean defaultAssigned = false;
for (CustomerContactVO contact : customer.getContacts()) {
if (Func.isBlank(contact.getContactName()) && Func.isBlank(contact.getContactPhone())) {
continue;
}
BfiCustomerLinkmanEntryDTO entry = new BfiCustomerLinkmanEntryDTO();
entry.setContactperson(contact.getContactName());
entry.setPhone(contact.getContactPhone());
boolean isDefault = !defaultAssigned && (Objects.equals(contact.getIsDefault(), 1)
|| Func.isNotBlank(contact.getContactName()));
entry.setIsdefaultLinkman(isDefault);
if (isDefault) {
defaultAssigned = true;
}
entries.add(entry);
}
if (!entries.isEmpty()) {
return entries;
}
}
if (Func.isBlank(customer.getPrincipal())) { if (Func.isBlank(customer.getPrincipal())) {
return List.of(); return List.of();
} }
BfiCustomerLinkmanEntryDTO entry = new BfiCustomerLinkmanEntryDTO(); BfiCustomerLinkmanEntryDTO entry = new BfiCustomerLinkmanEntryDTO();
// 联系人:客商负责人
entry.setContactperson(customer.getPrincipal()); entry.setContactperson(customer.getPrincipal());
// 电话:联系电话 entry.setPhone(this.resolveContactPhone(customer));
entry.setPhone(customer.getContactPhone());
// 默认联系人
entry.setIsdefaultLinkman(Boolean.TRUE); entry.setIsdefaultLinkman(Boolean.TRUE);
return List.of(entry); return List.of(entry);
} }
private String resolveContactPhone(CustomerArchiveVO customer) {
if (Func.isNotBlank(customer.getContactPhone())) {
return customer.getContactPhone();
}
if (Func.isEmpty(customer.getContacts())) {
return null;
}
return customer.getContacts().stream()
.map(CustomerContactVO::getContactPhone)
.filter(Func::isNotBlank)
.findFirst()
.orElse(null);
}
/** /**
* 金蝶伙伴类型编码:客商性质(nature_of_client 字典)—— * 金蝶伙伴类型编码:客商性质(nature_of_client 字典)——
* 值为 dictKey(即伙伴类型编码,1~17)时直接使用; * 值为 dictKey(即伙伴类型编码,1~17)时直接使用;
@@ -248,35 +285,50 @@ public class CustomerBfiConverter {
} }
/** /**
* 经营范围多选编码 → 字典文本「、」拼接,字典缺失时回退原编码 * 经营范围:长文本直传;兼容历史多选编码 → 字典文本「、」拼接。
* *
* @param businessScope 逗号拼接的经营范围编码 * @param businessScope 经营范围长文本或历史逗号拼接编码
* @return 金蝶经营范围文本 * @return 金蝶经营范围文本
*/ */
public String buildBusinessScopeText(String businessScope) { public String buildBusinessScopeText(String businessScope) {
if (Func.isEmpty(businessScope)) { if (Func.isEmpty(businessScope)) {
return ""; return "";
} }
String text = businessScope.trim();
// 长文本(含中文描述)直接推送,不再按字典拆分
if (text.length() > 40 || text.contains(";") || text.contains("。") || text.contains("\n")) {
return text;
}
List<DictBiz> dictList = DictBizCache.getList("scope_of_business"); List<DictBiz> dictList = DictBizCache.getList("scope_of_business");
Map<String, String> scopeNames = dictList == null ? Map.of() : dictList.stream() Map<String, String> scopeNames = dictList == null ? Map.of() : dictList.stream()
.collect(Collectors.toMap(DictBiz::getDictKey, DictBiz::getDictValue, (first, second) -> first)); .collect(Collectors.toMap(DictBiz::getDictKey, DictBiz::getDictValue, (first, second) -> first));
return Func.toStrList(businessScope.replace(",", ",")).stream() return Func.toStrList(text.replace(",", ",")).stream()
.map(code -> scopeNames.getOrDefault(code, code)) .map(code -> scopeNames.getOrDefault(code, code))
.collect(Collectors.joining("、")); .collect(Collectors.joining("、"));
} }
/** /**
* 营业期限:长期传「长期」;固定期限传「至 yyyy-MM-dd」(TMS 未采集起始日) * 营业期限:长期传「长期」;固定期限传「yyyy-MM-dd 至 yyyy-MM-dd」或「至 yyyy-MM-dd」。
* *
* @param businessTermType 营业期限类型 * @param businessTermType 营业期限类型
* @param businessStartDate 营业期限开始日
* @param businessEndDate 营业期限截止日 * @param businessEndDate 营业期限截止日
* @return 金蝶营业期限文本 * @return 金蝶营业期限文本
*/ */
public String buildBusinessTerm(String businessTermType, LocalDate businessEndDate) { public String buildBusinessTerm(String businessTermType, LocalDate businessStartDate, LocalDate businessEndDate) {
if (CustomerBfiConstants.BUSINESS_TERM_LONG.equals(businessTermType)) { if (CustomerBfiConstants.BUSINESS_TERM_LONG.equals(businessTermType)) {
if (businessStartDate == null) {
return CustomerBfiConstants.BUSINESS_TERM_LONG; return CustomerBfiConstants.BUSINESS_TERM_LONG;
} }
return businessEndDate == null ? "" : "至 " + businessEndDate; return businessStartDate + " 至 " + CustomerBfiConstants.BUSINESS_TERM_LONG;
}
if (businessStartDate != null && businessEndDate != null) {
return businessStartDate + " 至 " + businessEndDate;
}
if (businessEndDate != null) {
return "至 " + businessEndDate;
}
return businessStartDate == null ? "" : String.valueOf(businessStartDate);
} }
} }
@@ -107,6 +107,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
private static final String STATUS_WAITING_DISPATCH = "waiting_dispatch"; private static final String STATUS_WAITING_DISPATCH = "waiting_dispatch";
private static final String STATUS_DISPATCHING = "dispatching"; private static final String STATUS_DISPATCHING = "dispatching";
private static final String STATUS_RUNNING = "running"; private static final String STATUS_RUNNING = "running";
private static final String STATUS_PENDING_RETURN = WaybillProcessSupport.STATUS_PENDING_RETURN;
private static final String STATUS_COMPLETED = "completed"; private static final String STATUS_COMPLETED = "completed";
private static final String STATUS_CANCELLED = "cancelled"; private static final String STATUS_CANCELLED = "cancelled";
@@ -118,9 +119,10 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
private static final List<String> PENDING_STATUSES = Arrays.asList( private static final List<String> PENDING_STATUSES = Arrays.asList(
STATUS_PENDING, STATUS_WAITING_DISPATCH, STATUS_DISPATCHING STATUS_PENDING, STATUS_WAITING_DISPATCH, STATUS_DISPATCHING
); );
/** Tab「全部」统计口径:待接 + 运输中 + 已完成(不含已取消) */ /** Tab「全部」统计口径:待接 + 运输中 + 待确认回单 + 已完成(不含已取消) */
private static final List<String> TAB_ALL_STATUSES = Arrays.asList( private static final List<String> TAB_ALL_STATUSES = Arrays.asList(
STATUS_PENDING, STATUS_WAITING_DISPATCH, STATUS_DISPATCHING, STATUS_RUNNING, STATUS_COMPLETED STATUS_PENDING, STATUS_WAITING_DISPATCH, STATUS_DISPATCHING, STATUS_RUNNING,
STATUS_PENDING_RETURN, STATUS_COMPLETED
); );
/** 允许完成运单的业务状态 */ /** 允许完成运单的业务状态 */
private static final List<String> COMPLETABLE_STATUSES = Arrays.asList( private static final List<String> COMPLETABLE_STATUSES = Arrays.asList(
@@ -520,22 +522,35 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
nodePunchMapper.insert(punch); nodePunchMapper.insert(punch);
advanceCurrentProcessNode(waybill, nodeCfg); advanceCurrentProcessNode(waybill, nodeCfg);
// 末位打卡节点是到货/卸货/签收、且回单无需确认完成时,本次打卡一并把运单置为完成; // 末位打卡节点是到货/卸货/签收时:
// - 回单无需确认 → 直接完成;
// - 回单需确认 → 进入「待确认回单」,等运管员批量/单条完成后才变已完成。
// isLastDriverPunchNode 与 returnNeedsNoConfirmation 传的就是上面那份 processJson,未再调 resolveProcessJson 取过程配置。 // isLastDriverPunchNode 与 returnNeedsNoConfirmation 传的就是上面那份 processJson,未再调 resolveProcessJson 取过程配置。
String nodeKey = WaybillProcessSupport.nodeKey(nodeCfg); String nodeKey = WaybillProcessSupport.nodeKey(nodeCfg);
String nodeName = WaybillProcessSupport.nodeName(nodeCfg); String nodeName = WaybillProcessSupport.nodeName(nodeCfg);
boolean completed = false; boolean completed = false;
if (WaybillProcessSupport.isLastDriverPunchNode(processJson, nodeKey) if (WaybillProcessSupport.isLastDriverPunchNode(processJson, nodeKey)
&& WaybillProcessSupport.isDeliveryNode(nodeKey, nodeName) && WaybillProcessSupport.isDeliveryNode(nodeKey, nodeName)
&& WaybillProcessSupport.returnNeedsNoConfirmation(processJson)
&& canAutoCompleteWaybill(waybill)) { && canAutoCompleteWaybill(waybill)) {
if (WaybillProcessSupport.returnNeedsNoConfirmation(processJson)) {
completed = waybillService.completeWithoutDeptCheck(waybill.getId()); completed = waybillService.completeWithoutDeptCheck(waybill.getId());
} else {
markPendingReturn(waybill);
}
} }
DriverNodePunchVO vo = toNodePunchVO(punch); DriverNodePunchVO vo = toNodePunchVO(punch);
vo.setWaybillCompleted(completed); vo.setWaybillCompleted(completed);
return vo; return vo;
} }
/**
* 司机末位打卡完成且回单需内部确认时,将运单置为待确认回单。
*/
private void markPendingReturn(Waybill waybill) {
waybill.setBusinessStatus(STATUS_PENDING_RETURN);
waybillService.updateById(waybill);
}
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public boolean accept(Long id) { public boolean accept(Long id) {
@@ -844,7 +859,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
switch (status) { switch (status) {
case 0 -> wrapper.in(Waybill::getBusinessStatus, PENDING_STATUSES); case 0 -> wrapper.in(Waybill::getBusinessStatus, PENDING_STATUSES);
case 1 -> wrapper.in(Waybill::getBusinessStatus, COMPLETABLE_STATUSES); case 1 -> wrapper.in(Waybill::getBusinessStatus, COMPLETABLE_STATUSES);
case 2 -> wrapper.eq(Waybill::getBusinessStatus, STATUS_COMPLETED); case 2 -> wrapper.in(Waybill::getBusinessStatus, STATUS_COMPLETED, STATUS_PENDING_RETURN);
case 3 -> wrapper.eq(Waybill::getBusinessStatus, STATUS_CANCELLED); case 3 -> wrapper.eq(Waybill::getBusinessStatus, STATUS_CANCELLED);
default -> wrapper.in(Waybill::getBusinessStatus, TAB_ALL_STATUSES); default -> wrapper.in(Waybill::getBusinessStatus, TAB_ALL_STATUSES);
} }
@@ -2004,7 +2019,8 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
return switch (businessStatus) { return switch (businessStatus) {
case STATUS_PENDING, STATUS_WAITING_DISPATCH, STATUS_DISPATCHING -> 0; case STATUS_PENDING, STATUS_WAITING_DISPATCH, STATUS_DISPATCHING -> 0;
case STATUS_RUNNING -> 1; case STATUS_RUNNING -> 1;
case STATUS_COMPLETED -> 2; // 待确认回单:司机侧节点已打完,对司机按已完成展示
case STATUS_PENDING_RETURN, STATUS_COMPLETED -> 2;
case STATUS_CANCELLED -> 3; case STATUS_CANCELLED -> 3;
default -> null; default -> null;
}; };
@@ -44,6 +44,7 @@ import org.springblade.transport.pojo.vo.ExceptionDisposalVO;
import org.springblade.transport.service.IDriverWaybillService; import org.springblade.transport.service.IDriverWaybillService;
import org.springblade.transport.service.IExceptionDisposalService; import org.springblade.transport.service.IExceptionDisposalService;
import org.springblade.transport.service.IWaybillService; import org.springblade.transport.service.IWaybillService;
import org.springblade.transport.support.MpUserDataScopeSupport;
import org.springblade.transport.support.WaybillProcessSupport; import org.springblade.transport.support.WaybillProcessSupport;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -79,13 +80,16 @@ public class ExceptionDisposalServiceImpl
private final ExceptionDisposalFollowRecordMapper followRecordMapper; private final ExceptionDisposalFollowRecordMapper followRecordMapper;
private final IWaybillService waybillService; private final IWaybillService waybillService;
private final IDriverWaybillService driverWaybillService; private final IDriverWaybillService driverWaybillService;
private final MpUserDataScopeSupport mpUserDataScopeSupport;
public ExceptionDisposalServiceImpl(ExceptionDisposalFollowRecordMapper followRecordMapper, public ExceptionDisposalServiceImpl(ExceptionDisposalFollowRecordMapper followRecordMapper,
IWaybillService waybillService, IWaybillService waybillService,
IDriverWaybillService driverWaybillService) { IDriverWaybillService driverWaybillService,
MpUserDataScopeSupport mpUserDataScopeSupport) {
this.followRecordMapper = followRecordMapper; this.followRecordMapper = followRecordMapper;
this.waybillService = waybillService; this.waybillService = waybillService;
this.driverWaybillService = driverWaybillService; this.driverWaybillService = driverWaybillService;
this.mpUserDataScopeSupport = mpUserDataScopeSupport;
} }
@Override @Override
@@ -409,6 +413,8 @@ public class ExceptionDisposalServiceImpl
.like(Func.isNotEmpty(query.getCarrierName()), ExceptionDisposal::getCarrierName, query.getCarrierName()) .like(Func.isNotEmpty(query.getCarrierName()), ExceptionDisposal::getCarrierName, query.getCarrierName())
.eq(Func.isNotEmpty(query.getExceptionType()), ExceptionDisposal::getExceptionType, query.getExceptionType()) .eq(Func.isNotEmpty(query.getExceptionType()), ExceptionDisposal::getExceptionType, query.getExceptionType())
.eq(Func.isNotEmpty(query.getDisposalStatus()), ExceptionDisposal::getDisposalStatus, query.getDisposalStatus()); .eq(Func.isNotEmpty(query.getDisposalStatus()), ExceptionDisposal::getDisposalStatus, query.getDisposalStatus());
// 调度端列表按「权限组织 → 项目业务部门 → 项目」收口;司机端按运单查自己的异常时不套此过滤
mpUserDataScopeSupport.applyProjectScopeIfMpDispatcher(wrapper, ExceptionDisposal::getProjectId);
return wrapper.orderByDesc(ExceptionDisposal::getCreateTime); return wrapper.orderByDesc(ExceptionDisposal::getCreateTime);
} }
@@ -361,9 +361,14 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
.eq(SettlementAdjustment::getFormalSettlementId, id) .eq(SettlementAdjustment::getFormalSettlementId, id)
.orderByDesc(SettlementAdjustment::getCreateTime)).stream() .orderByDesc(SettlementAdjustment::getCreateTime)).stream()
.map(this::toAdjustmentVO).toList()); .map(this::toAdjustmentVO).toList());
vo.setChangeRecords(changeRecordMapper.selectList(Wrappers.<FormalSettlementChangeRecord>lambdaQuery() List<FormalSettlementChangeRecord> changeRecords = changeRecordMapper.selectList(
Wrappers.<FormalSettlementChangeRecord>lambdaQuery()
.eq(FormalSettlementChangeRecord::getFormalSettlementId, id) .eq(FormalSettlementChangeRecord::getFormalSettlementId, id)
.orderByDesc(FormalSettlementChangeRecord::getChangeTime))); .orderByDesc(FormalSettlementChangeRecord::getChangeTime));
// 审批通过/驳回类不展示在变更记录中
vo.setChangeRecords(changeRecords.stream()
.filter(record -> !isApprovalChangeContent(record.getChangeContent()))
.toList());
return vo; return vo;
} }
@@ -569,6 +574,8 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
settlement.setKingdeeSyncStatus("unsynced"); settlement.setKingdeeSyncStatus("unsynced");
} }
if (first == null) copyHeader(contract, settlementType, directDetails.get(0), settlement); else copyHeader(first, settlement); if (first == null) copyHeader(contract, settlementType, directDetails.get(0), settlement); else copyHeader(first, settlement);
settlement.setBusinessModel(limit(request.getBusinessModel(), 64));
settlement.setTransportType(limit(request.getTransportType(), 64));
settlement.setExchangeRateDate(request.getExchangeRateDate()); settlement.setExchangeRateDate(request.getExchangeRateDate());
settlement.setExchangeRate(request.getExchangeRate() == null ? BigDecimal.ONE : positive(request.getExchangeRate(), "结算汇率")); settlement.setExchangeRate(request.getExchangeRate() == null ? BigDecimal.ONE : positive(request.getExchangeRate(), "结算汇率"));
BigDecimal sourceAmount = sources.stream().map(PreSettlement::getSettlementAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal sourceAmount = sources.stream().map(PreSettlement::getSettlementAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add);
@@ -642,7 +649,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
settlement.setCurrentProcessor(AuthUtil.getUserName()); settlement.setCurrentProcessor(AuthUtil.getUserName());
settlement.setApprovedTime(LocalDateTime.now()); settlement.setApprovedTime(LocalDateTime.now());
updateById(settlement); updateById(settlement);
saveChange(settlement.getId(), "结算单基本信息", null, "调整", "正式结算审批通过", request.getReason()); // 审批通过不落变更记录
// 审批事务提交后自动推送金蝶(AFTER_COMMIT 监听器消费,推送失败不影响审批结果) // 审批事务提交后自动推送金蝶(AFTER_COMMIT 监听器消费,推送失败不影响审批结果)
eventPublisher.publishEvent(new KingdeeBillApprovedEvent(KingdeeBillApprovedEvent.BIZ_FORMAL_SETTLEMENT, settlement.getId())); eventPublisher.publishEvent(new KingdeeBillApprovedEvent(KingdeeBillApprovedEvent.BIZ_FORMAL_SETTLEMENT, settlement.getId()));
} }
@@ -1279,8 +1286,30 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
} }
Map<Long, ReceivablePayableCargoFee> sourceFeeMap = receivablePayableCargoFeeMapper.selectBatchIds(sourceFeeIds).stream() Map<Long, ReceivablePayableCargoFee> sourceFeeMap = receivablePayableCargoFeeMapper.selectBatchIds(sourceFeeIds).stream()
.collect(Collectors.toMap(ReceivablePayableCargoFee::getId, Function.identity(), (left, right) -> left)); .collect(Collectors.toMap(ReceivablePayableCargoFee::getId, Function.identity(), (left, right) -> left));
// 历史数据可能把预结算费用行ID误写入 sourceFeeId,需再解析到应收应付费用行
List<Long> unresolvedIds = sourceFeeIds.stream().filter(id -> !sourceFeeMap.containsKey(id)).toList();
Map<Long, PreSettlementDetailFee> preFeeMap = unresolvedIds.isEmpty() ? Map.of()
: preDetailFeeMapper.selectBatchIds(unresolvedIds).stream()
.collect(Collectors.toMap(PreSettlementDetailFee::getId, Function.identity(), (left, right) -> left));
List<Long> receivableFeeIds = preFeeMap.values().stream().map(PreSettlementDetailFee::getSourceFeeId)
.filter(Objects::nonNull).distinct().toList();
if (!receivableFeeIds.isEmpty()) {
receivablePayableCargoFeeMapper.selectBatchIds(receivableFeeIds).forEach(item ->
sourceFeeMap.putIfAbsent(item.getId(), item));
}
fees.forEach(fee -> { fees.forEach(fee -> {
ReceivablePayableCargoFee sourceFee = sourceFeeMap.get(fee.getSourceFeeId()); Long sourceFeeId = fee.getSourceFeeId();
ReceivablePayableCargoFee sourceFee = sourceFeeMap.get(sourceFeeId);
if (sourceFee == null && sourceFeeId != null) {
PreSettlementDetailFee preFee = preFeeMap.get(sourceFeeId);
if (preFee != null && preFee.getSourceFeeId() != null) {
sourceFee = sourceFeeMap.get(preFee.getSourceFeeId());
if (sourceFee != null) {
// 仅回写响应,便于前端试算;不落库覆盖历史 sourceFeeId
fee.setSourceFeeId(preFee.getSourceFeeId());
}
}
}
if (sourceFee != null) { if (sourceFee != null) {
fee.setBillingRulesJson(sourceFee.getBillingRulesJson()); fee.setBillingRulesJson(sourceFee.getBillingRulesJson());
} }
@@ -2072,7 +2101,11 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
for (PreSettlementDetailFee item : fees) { for (PreSettlementDetailFee item : fees) {
FormalSettlementDetailFee fee = Objects.requireNonNull(BeanUtil.copyProperties(item, FormalSettlementDetailFee.class)); FormalSettlementDetailFee fee = Objects.requireNonNull(BeanUtil.copyProperties(item, FormalSettlementDetailFee.class));
fee.setTransportQuantity(money(fee.getTransportQuantity())); fee.setTransportQuantity(money(fee.getTransportQuantity()));
fee.setId(null); fee.setFormalSettlementDetailId(target.getId()); fee.setSourceFeeId(item.getId()); detailFeeMapper.insert(fee); fee.setId(null);
fee.setFormalSettlementDetailId(target.getId());
// 优先保留应收应付费用行ID,便于正式结算调整时按合同计费方案试算
fee.setSourceFeeId(item.getSourceFeeId() != null ? item.getSourceFeeId() : item.getId());
detailFeeMapper.insert(fee);
} }
if (fees.isEmpty()) createSingleFee(target); if (fees.isEmpty()) createSingleFee(target);
} }
@@ -2377,8 +2410,11 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
settlement.setApprovalStatus(target); settlement.setCurrentNode(node); settlement.setCurrentProcessor(AuthUtil.getUserName()); settlement.setApprovalStatus(target); settlement.setCurrentNode(node); settlement.setCurrentProcessor(AuthUtil.getUserName());
if (TransportBusiConstant.RETURNED.equals(target)) settlement.setVoidReason(limit(reason, 200)); if (TransportBusiConstant.RETURNED.equals(target)) settlement.setVoidReason(limit(reason, 200));
updateById(settlement); updateById(settlement);
// 审批驳回不落变更记录;提交等其它状态流转仍记录
if (!TransportBusiConstant.RETURNED.equals(target)) {
saveChange(settlement.getId(), "结算单基本信息", null, "调整", node, reason); saveChange(settlement.getId(), "结算单基本信息", null, "调整", node, reason);
} }
}
private void saveManualSummaryFeeChange(Long settlementId, boolean creating, String beforeFeeType, private void saveManualSummaryFeeChange(Long settlementId, boolean creating, String beforeFeeType,
String beforeFeeItem, BigDecimal beforeAdjustAmount, BigDecimal beforeSettlementAmount, String beforeRemark, String beforeFeeItem, BigDecimal beforeAdjustAmount, BigDecimal beforeSettlementAmount, String beforeRemark,
@@ -2436,6 +2472,14 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
} }
} }
/** 审批通过、驳回等审批流转不落变更记录。 */
private boolean isApprovalChangeContent(String content) {
if (Func.isEmpty(content)) {
return false;
}
return content.contains("审批通过") || content.contains("审批驳回") || "已驳回".equals(content);
}
private void saveChange(Long settlementId, String changeType, Integer lineNo, String operationType, private void saveChange(Long settlementId, String changeType, Integer lineNo, String operationType,
String content, String reason) { String content, String reason) {
saveChange(settlementId, changeType, lineNo, operationType, content, reason, null, null); saveChange(settlementId, changeType, lineNo, operationType, content, reason, null, null);
@@ -2443,6 +2487,9 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
private void saveChange(Long settlementId, String changeType, Integer lineNo, String operationType, private void saveChange(Long settlementId, String changeType, Integer lineNo, String operationType,
String content, String reason, Map<String, Object> beforeData, Map<String, Object> afterData) { String content, String reason, Map<String, Object> beforeData, Map<String, Object> afterData) {
if (isApprovalChangeContent(content)) {
return;
}
FormalSettlementChangeRecord record = new FormalSettlementChangeRecord(); FormalSettlementChangeRecord record = new FormalSettlementChangeRecord();
record.setFormalSettlementId(settlementId); record.setFormalSettlementId(settlementId);
record.setChangeType(changeType); record.setChangeType(changeType);
@@ -20,9 +20,11 @@ import org.springblade.system.pojo.entity.Dept;
import org.springblade.transport.excel.LoadingManageExcel; import org.springblade.transport.excel.LoadingManageExcel;
import org.springblade.transport.mapper.LoadingManageMapper; import org.springblade.transport.mapper.LoadingManageMapper;
import org.springblade.transport.mapper.WaybillMapper; import org.springblade.transport.mapper.WaybillMapper;
import org.springblade.transport.mapper.WaybillNodePunchMapper;
import org.springblade.transport.pojo.entity.ContractManage; import org.springblade.transport.pojo.entity.ContractManage;
import org.springblade.transport.pojo.entity.LoadingManage; import org.springblade.transport.pojo.entity.LoadingManage;
import org.springblade.transport.pojo.entity.Waybill; import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.pojo.entity.WaybillNodePunch;
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO; import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
import org.springblade.transport.pojo.vo.LoadingCarrierContractVO; import org.springblade.transport.pojo.vo.LoadingCarrierContractVO;
import org.springblade.transport.pojo.vo.LoadingManageVO; import org.springblade.transport.pojo.vo.LoadingManageVO;
@@ -74,6 +76,9 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
@Resource @Resource
private WaybillMapper waybillMapper; private WaybillMapper waybillMapper;
@Resource
private WaybillNodePunchMapper waybillNodePunchMapper;
@Resource @Resource
private IContractManageService contractManageService; private IContractManageService contractManageService;
@@ -139,10 +144,26 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
loadingManage.setLoadingNo(nextCode()); loadingManage.setLoadingNo(nextCode());
} }
validateCarrierContract(loadingManage, false); validateCarrierContract(loadingManage, false);
if (STATUS_RUNNING.equals(businessStatus) && Func.isNotEmpty(loadingManage.getId())) {
LoadingManage oldRecord = getById(loadingManage.getId());
if (oldRecord != null) {
validateRunningImmutableTaskFields(oldRecord, loadingManage);
Set<Long> nextIds = new HashSet<>(waybillIds(loadingManage.getWaybillIdsJson()));
for (Long oldId : oldWaybillIds) {
if (oldId != null && !nextIds.contains(oldId) && hasLoadOrUnloadPunch(oldId)) {
throw new ServiceException("已打卡的子运单不可从进行中配载单移除");
}
}
}
}
boolean result = saveOrUpdate(loadingManage); boolean result = saveOrUpdate(loadingManage);
// 草稿仅预占运单号,不把运单业务状态改成 draft;正式确认时再完整同步为待执行 // 草稿仅预占运单号,不把运单业务状态改成 draft;正式确认时再完整同步为待执行
if (STATUS_DRAFT.equals(businessStatus)) { if (STATUS_DRAFT.equals(businessStatus)) {
syncDraftWaybillReservation(loadingManage, oldWaybillIds); syncDraftWaybillReservation(loadingManage, oldWaybillIds);
} else if (STATUS_RUNNING.equals(businessStatus)) {
// 进行中暂存:不重置原有子单状态/接单,仅同步任务信息与新增子单
List<Waybill> waybills = loadWaybills(waybillIds(loadingManage.getWaybillIdsJson()));
writeWaybillsForRunningEdit(loadingManage, oldWaybillIds, waybills);
} else { } else {
syncAssociatedWaybills(loadingManage, businessStatus, oldWaybillIds); syncAssociatedWaybills(loadingManage, businessStatus, oldWaybillIds);
} }
@@ -156,28 +177,103 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
validate(loadingManage, false); validate(loadingManage, false);
List<Long> oldWaybillIds = new ArrayList<>(); List<Long> oldWaybillIds = new ArrayList<>();
boolean ownChildrenAdmitted = false; boolean ownChildrenAdmitted = false;
boolean runningEdit = false;
LoadingManage oldRecord = null;
if (Func.isNotEmpty(loadingManage.getId())) { if (Func.isNotEmpty(loadingManage.getId())) {
LoadingManage oldRecord = loadEditable(loadingManage.getId(), true); oldRecord = loadEditable(loadingManage.getId(), true);
oldWaybillIds = waybillIds(oldRecord.getWaybillIdsJson()); oldWaybillIds = waybillIds(oldRecord.getWaybillIdsJson());
// 草稿只预占运单号、未过判据,草稿转正式不得沿用本单豁免 // 草稿只预占运单号、未过判据,草稿转正式不得沿用本单豁免
ownChildrenAdmitted = !STATUS_DRAFT.equals(oldRecord.getBusinessStatus()); ownChildrenAdmitted = !STATUS_DRAFT.equals(oldRecord.getBusinessStatus());
runningEdit = STATUS_RUNNING.equals(oldRecord.getBusinessStatus());
loadingManage.setLoadingNo(oldRecord.getLoadingNo()); loadingManage.setLoadingNo(oldRecord.getLoadingNo());
loadingManage.setDeptId(oldRecord.getDeptId()); loadingManage.setDeptId(oldRecord.getDeptId());
loadingManage.setDeptName(oldRecord.getDeptName()); loadingManage.setDeptName(oldRecord.getDeptName());
} }
prepareCreateOrUpdate(loadingManage); prepareCreateOrUpdate(loadingManage);
// 确认提交:草稿/新建统一生成正式配载单(待执行)
loadingManage.setBusinessStatus(STATUS_PENDING);
if (Func.isEmpty(loadingManage.getLoadingNo())) { if (Func.isEmpty(loadingManage.getLoadingNo())) {
loadingManage.setLoadingNo(nextCode()); loadingManage.setLoadingNo(nextCode());
} }
List<Waybill> waybills = validateWaybillsAvailable(loadingManage, waybillIds(loadingManage.getWaybillIdsJson()), ownChildrenAdmitted); List<Waybill> waybills = validateWaybillsAvailable(loadingManage, waybillIds(loadingManage.getWaybillIdsJson()), ownChildrenAdmitted);
validateCarrierContract(loadingManage, true, waybills); validateCarrierContract(loadingManage, true, waybills);
if (runningEdit && oldRecord != null) {
validateRunningEdit(oldRecord, loadingManage, waybills, oldWaybillIds);
}
// 进行中编辑保持 running;新建/草稿确认按项目过程配置:需接单→待执行,无需接单→进行中
loadingManage.setBusinessStatus(runningEdit ? STATUS_RUNNING : deriveLoadingBusinessStatus(waybills));
boolean result = saveOrUpdate(loadingManage); boolean result = saveOrUpdate(loadingManage);
// 进行中编辑:保留原有子单状态与接单事实,仅对新加入子单做派车同步
if (runningEdit) {
writeWaybillsForRunningEdit(loadingManage, oldWaybillIds, waybills);
} else {
writeWaybillsForDriver(loadingManage, oldWaybillIds, waybills); writeWaybillsForDriver(loadingManage, oldWaybillIds, waybills);
}
return result; return result;
} }
/**
* 进行中配载编辑校验:必填任务字段不可改;已装/卸打卡的子运单不可移除。
*/
private void validateRunningEdit(LoadingManage oldRecord, LoadingManage incoming,
List<Waybill> waybills, List<Long> oldWaybillIds) {
validateRunningImmutableTaskFields(oldRecord, incoming);
Set<Long> nextIds = waybills.stream().map(Waybill::getId).filter(Objects::nonNull).collect(Collectors.toSet());
for (Long oldId : oldWaybillIds) {
if (oldId == null || nextIds.contains(oldId)) {
continue;
}
if (hasLoadOrUnloadPunch(oldId)) {
throw new ServiceException("已打卡的子运单不可从进行中配载单移除");
}
}
}
private void validateRunningImmutableTaskFields(LoadingManage oldRecord, LoadingManage incoming) {
if (!Objects.equals(trimText(oldRecord.getCarrierType()), trimText(incoming.getCarrierType()))) {
throw new ServiceException("进行中配载单不允许修改承运类型");
}
if (Objects.equals(oldRecord.getCarrierType(), "承运商")
&& !Objects.equals(oldRecord.getCarrierContractId(), incoming.getCarrierContractId())) {
throw new ServiceException("进行中配载单不允许修改承运商");
}
if (!Objects.equals(trimText(oldRecord.getVehicleNo()), trimText(incoming.getVehicleNo()))) {
throw new ServiceException("进行中配载单不允许修改车牌号");
}
if (Objects.equals(oldRecord.getCarrierType(), CARRIER_SELF)) {
if (!Objects.equals(trimText(oldRecord.getDriverName()), trimText(incoming.getDriverName()))) {
throw new ServiceException("进行中配载单不允许修改司机");
}
if (!Objects.equals(trimText(oldRecord.getDriverPhone()), trimText(incoming.getDriverPhone()))) {
throw new ServiceException("进行中配载单不允许修改手机号");
}
}
}
private boolean hasLoadOrUnloadPunch(Long waybillId) {
if (waybillId == null) {
return false;
}
List<WaybillNodePunch> punches = waybillNodePunchMapper.selectList(Wrappers.<WaybillNodePunch>lambdaQuery()
.eq(WaybillNodePunch::getWaybillId, waybillId)
.eq(WaybillNodePunch::getIsDeleted, 0));
return punches.stream().anyMatch(this::isLoadOrUnloadPunch);
}
private boolean isLoadOrUnloadPunch(WaybillNodePunch punch) {
if (punch == null) {
return false;
}
String code = Func.toStr(punch.getNodeCode(), "").trim().toLowerCase();
String name = Func.toStr(punch.getNodeName(), "").trim();
return "load".equals(code) || name.contains("装货")
|| "unload".equals(code) || name.contains("卸货")
|| "arrival".equals(code) || name.contains("到货")
|| "sign".equals(code) || name.contains("签收");
}
private String trimText(String value) {
return Func.toStr(value, "").trim();
}
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void createFromImportedWaybills(String loadingNo, List<Waybill> waybills) { public void createFromImportedWaybills(String loadingNo, List<Waybill> waybills) {
@@ -776,8 +872,8 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
String prefix = "运单" + waybill.getWaybillNo(); String prefix = "运单" + waybill.getWaybillNo();
return prefix + switch (reason) { return prefix + switch (reason) {
case NOT_ROAD -> "运输方式不是公路,不可配载"; case NOT_ROAD -> "运输方式不是公路,不可配载";
case NOT_PENDING -> "当前状态不是待执行,不可配载"; case NOT_LOADABLE_STATUS -> "当前状态不是待执行或进行中,不可配载";
case ON_OTHER_MANIFEST -> "已关联其他配载单"; case ON_OTHER_MANIFEST -> "已关联其他有效配载单";
}; };
} }
@@ -910,6 +1006,108 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
clearAssociatedWaybillAcceptRecords(loadingManage); clearAssociatedWaybillAcceptRecords(loadingManage);
} }
/**
* 进行中配载编辑同步:原有子单只更新任务可选信息,保留业务状态与接单事实;
* 新加入子单按正常派车逻辑推导状态并清接单;被移除且未打卡的子单释放回待执行。
*/
private void writeWaybillsForRunningEdit(LoadingManage loadingManage, List<Long> oldWaybillIds, List<Waybill> waybills) {
List<Long> newWaybillIds = waybillIds(loadingManage.getWaybillIdsJson());
Set<Long> oldIdSet = new HashSet<>(oldWaybillIds);
List<Long> removedIds = oldWaybillIds.stream()
.filter(id -> id != null && !newWaybillIds.contains(id))
.toList();
releaseAssociatedWaybills(removedIds, STATUS_PENDING);
List<Long> keptIds = newWaybillIds.stream()
.filter(oldIdSet::contains)
.toList();
if (Func.isNotEmpty(keptIds)) {
updateAssociatedWaybillTaskFields(loadingManage, keptIds, null);
}
List<Long> addedIds = newWaybillIds.stream()
.filter(id -> id != null && !oldIdSet.contains(id))
.toList();
if (Func.isEmpty(addedIds)) {
return;
}
List<Waybill> addedWaybills = waybills.stream()
.filter(waybill -> waybill.getId() != null && addedIds.contains(waybill.getId()))
.toList();
Map<Long, String> statusByWaybillId = deriveWaybillBusinessStatuses(addedWaybills);
Map<String, List<Long>> waybillIdsByStatus = new LinkedHashMap<>();
for (Long waybillId : addedIds) {
waybillIdsByStatus
.computeIfAbsent(statusByWaybillId.getOrDefault(waybillId, STATUS_PENDING), key -> new ArrayList<>())
.add(waybillId);
}
waybillIdsByStatus.forEach((status, ids) -> updateAssociatedWaybillFields(loadingManage, ids, status));
clearWaybillAcceptRecords(addedIds);
}
private void updateAssociatedWaybillTaskFields(LoadingManage loadingManage, List<Long> waybillIds, String status) {
if (Func.isEmpty(waybillIds)) {
return;
}
var update = Wrappers.<Waybill>lambdaUpdate()
.in(Waybill::getId, waybillIds)
.set(Waybill::getLoadingNo, loadingManage.getLoadingNo())
.set(Waybill::getCarrierType, loadingManage.getCarrierType())
.set(Waybill::getCarrierContractId, loadingManage.getCarrierContractId())
.set(Waybill::getCarrierName, loadingManage.getCarrierName())
.set(Waybill::getDriverName, loadingManage.getDriverName())
.set(Waybill::getDriverPhone, loadingManage.getDriverPhone())
.set(Waybill::getVehicleNo, loadingManage.getVehicleNo())
.set(Waybill::getTrailerVehicleNo, loadingManage.getTrailerVehicleNo())
.set(Waybill::getEscortName, loadingManage.getEscortName())
.set(Waybill::getEscortPhone, loadingManage.getEscortPhone())
.set(Waybill::getMileage, loadingManage.getMileage())
.set(Waybill::getEstimatedStartTime, toDateTime(loadingManage.getEstimatedStartDate()))
.set(Waybill::getEstimatedEndTime, toDateTime(loadingManage.getEstimatedEndDate()))
.set(Waybill::getTaskRemark, loadingManage.getTaskRemark())
.set(Waybill::getTaskInfoJson, loadingManage.getTaskInfoJson())
.set(Waybill::getCurrentProcessNode, loadingManage.getCurrentProcessNode());
if (Func.isNotEmpty(status)) {
update.set(Waybill::getBusinessStatus, status);
if (STATUS_COMPLETED.equals(status)) {
update.set(Waybill::getEndDate, LocalDate.now());
}
}
waybillMapper.update(null, update);
}
private void clearWaybillAcceptRecords(List<Long> waybillIds) {
if (Func.isEmpty(waybillIds)) {
return;
}
waybillMapper.update(null, Wrappers.<Waybill>lambdaUpdate()
.in(Waybill::getId, waybillIds)
.set(Waybill::getDriverAcceptStatus, WaybillProcessSupport.ACCEPT_PENDING)
.set(Waybill::getDriverAcceptTime, null)
.set(Waybill::getDriverAcceptDriverId, null)
.set(Waybill::getDriverRejectTime, null)
.set(Waybill::getDriverRejectReason, null));
}
/**
* 配载单业务状态按子运单所属项目的实时过程配置推导:需要接单 → 待执行,不需要 → 进行中。
* <p>
* 一张配载单的子运单同属一个项目,任一张需接单则整单待执行。
*/
private String deriveLoadingBusinessStatus(List<Waybill> waybills) {
if (Func.isEmpty(waybills)) {
return STATUS_RUNNING;
}
Map<Long, String> liveConfigByProject = LiveProcessConfigSupport.byProjects(processConfigService,
waybills.stream().map(Waybill::getProjectId).toList());
for (Waybill waybill : waybills) {
String processJson = LiveProcessConfigSupport.resolve(
liveConfigByProject.get(waybill.getProjectId()), waybill.getProcessJson());
if (WaybillProcessSupport.requiresDriverAcceptConfirmation(processJson)) {
return STATUS_PENDING;
}
}
return STATUS_RUNNING;
}
/** /**
* 子运单业务状态逐张按各自项目的实时过程配置推导:需要接单 → 待执行,不需要 → 进行中。 * 子运单业务状态逐张按各自项目的实时过程配置推导:需要接单 → 待执行,不需要 → 进行中。
* <p> * <p>
@@ -33,6 +33,7 @@ import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.utils.DateUtil; import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.Func; import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.UserDisplayCache; import org.springblade.system.cache.UserDisplayCache;
import org.springblade.system.utils.MpUserAuth;
import org.springblade.transport.pojo.entity.Driver; import org.springblade.transport.pojo.entity.Driver;
import org.springblade.transport.pojo.entity.ExceptionDisposal; import org.springblade.transport.pojo.entity.ExceptionDisposal;
import org.springblade.transport.pojo.entity.LoadingManage; import org.springblade.transport.pojo.entity.LoadingManage;
@@ -59,6 +60,7 @@ import org.springblade.transport.service.IManageWaybillService;
import org.springblade.transport.service.IRiskDisposalService; import org.springblade.transport.service.IRiskDisposalService;
import org.springblade.transport.service.ITripQueryService; import org.springblade.transport.service.ITripQueryService;
import org.springblade.transport.service.IWaybillService; import org.springblade.transport.service.IWaybillService;
import org.springblade.transport.support.MpUserDataScopeSupport;
import org.springblade.transport.support.TransportBusinessSupport; import org.springblade.transport.support.TransportBusinessSupport;
import org.springblade.transport.support.WaybillCargoSupport; import org.springblade.transport.support.WaybillCargoSupport;
import org.springblade.transport.support.WaybillProcessSupport; import org.springblade.transport.support.WaybillProcessSupport;
@@ -89,8 +91,8 @@ import java.util.stream.Collectors;
* 列表与详情与司机端同粒度:取数、状态聚合、接单需求合并都调共享运次读服务, * 列表与详情与司机端同粒度:取数、状态聚合、接单需求合并都调共享运次读服务,
* 本类只做字段裁剪与调度端独有的筛选(组织类型 / 异常 / 创建时间 / 待重新派单)。 * 本类只做字段裁剪与调度端独有的筛选(组织类型 / 异常 / 创建时间 / 待重新派单)。
* <p> * <p>
* 小程序调度账号(如「小程序管理」)组织常与运单业务组织不一致,故不做 dept 过滤, * 小程序调度端:所属组织 + 数据权限范围 + 数据层级范围 → 业务部门在范围内的项目 → 再取这些项目的运单;
* 仅依赖租户隔离,口径接近后台 {@code /waybill-manage/list?allDept=1}。 * 异常/风险与运单同一项目口径。系统用户仍按「所属组织 / 超管不过滤」。
*/ */
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -137,13 +139,14 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
private final IDriverWaybillService driverWaybillService; private final IDriverWaybillService driverWaybillService;
private final ITripQueryService tripQueryService; private final ITripQueryService tripQueryService;
private final ILoadingManageService loadingManageService; private final ILoadingManageService loadingManageService;
private final MpUserDataScopeSupport mpUserDataScopeSupport;
@Override @Override
public AdminHomeStatsVO stats() { public AdminHomeStatsVO stats() {
AdminHomeStatsVO vo = new AdminHomeStatsVO(); AdminHomeStatsVO vo = new AdminHomeStatsVO();
// 三档状态按运次计:有配载单的一趟车算一个,与列表和司机端同一份聚合 // 三档状态按运次计:有配载单的一趟车算一个,与列表和司机端同一份聚合
TripQueryVO query = TripQueryVO.forAdmin(); TripQueryVO query = TripQueryVO.forAdmin();
query.setDeptIds(manageableDeptIds()); applyAdminDataScope(query);
TripTabCountsVO tripCounts = tripQueryService.tabCounts(query); TripTabCountsVO tripCounts = tripQueryService.tabCounts(query);
vo.setPendingAccept(tripCounts.getPending()); vo.setPendingAccept(tripCounts.getPending());
vo.setTransporting(tripCounts.getDoing()); vo.setTransporting(tripCounts.getDoing());
@@ -181,7 +184,7 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
// 组织类型 / 异常 / 创建时间都是子单行级条件,与状态、关键字、分页一起交给运次读服务, // 组织类型 / 异常 / 创建时间都是子单行级条件,与状态、关键字、分页一起交给运次读服务,
// 一次取数取完整趟车:分组、分档、筛选、排序与分页都在它那边算完 // 一次取数取完整趟车:分组、分档、筛选、排序与分页都在它那边算完
TripQueryVO query = TripQueryVO.forAdmin(); TripQueryVO query = TripQueryVO.forAdmin();
query.setDeptIds(manageableDeptIds()); applyAdminDataScope(query);
query.setStatus(toTripStatus(status)); query.setStatus(toTripStatus(status));
query.setKeyword(keyword); query.setKeyword(keyword);
query.setTransportOrgType(transportType); query.setTransportOrgType(transportType);
@@ -229,7 +232,7 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
Set<Long> exceptionWaybillIds = loadIncompleteExceptionWaybillIds(); Set<Long> exceptionWaybillIds = loadIncompleteExceptionWaybillIds();
TripQueryVO query = TripQueryVO.forAdmin(); TripQueryVO query = TripQueryVO.forAdmin();
query.setDeptIds(manageableDeptIds()); applyAdminDataScope(query);
query.setKeyword(keyword); query.setKeyword(keyword);
// 待处理只要待接单与进行中两档;「未拒单」也是交给运次读服务按子单行筛 // 待处理只要待接单与进行中两档;「未拒单」也是交给运次读服务按子单行筛
query.setStatuses(List.of(0, 1)); query.setStatuses(List.of(0, 1));
@@ -667,11 +670,8 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
SQL_MAX_REJECT_TIME + " AS driver_reject_time", SQL_MAX_CREATE_TIME + " AS create_time"); SQL_MAX_REJECT_TIME + " AS driver_reject_time", SQL_MAX_CREATE_TIME + " AS create_time");
wrapper.eq("driver_accept_status", ACCEPT_REJECTED); wrapper.eq("driver_accept_status", ACCEPT_REJECTED);
wrapper.in("business_status", STATUS_PENDING, STATUS_RUNNING); wrapper.in("business_status", STATUS_PENDING, STATUS_RUNNING);
// 这条查询不经运次读服务,组织收口得自己带上,否则待重派列表与首页重派角标会出别的组织的单 // 这条查询不经运次读服务,数据权限收口得自己带上,否则待重派列表与首页重派角标会越权
List<Long> deptIds = manageableDeptIds(); applyAdminDataScope(wrapper);
if (Func.isNotEmpty(deptIds)) {
wrapper.in("dept_id", deptIds);
}
wrapper.groupBy(TripAggregation.SQL_TRIP_KEY); wrapper.groupBy(TripAggregation.SQL_TRIP_KEY);
return wrapper; return wrapper;
} }
@@ -884,10 +884,50 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
} }
/** /**
* 调度端可见组织:普通用户(含各级角色)只出自己所属组织,超级管理员不过滤。 * 调度端数据权限落到运次查询条件:小程序按可见项目,系统用户按所属组织。
* <p> */
* 小程序不校验「所属组织不能为空」:账号没有组织时不按组织收口,避免首页、待处理等接口直接报错。 private void applyAdminDataScope(TripQueryVO query) {
* 详情与重新派单本身不走这里。 if (query == null) {
return;
}
if (MpUserAuth.isMpUser()) {
query.setProjectIds(manageableProjectIds());
return;
}
query.setDeptIds(manageableDeptIds());
}
/**
* 待重派等直查运单表时的数据权限:小程序按 project_id,系统用户按 dept_id。
*/
private void applyAdminDataScope(QueryWrapper<Waybill> wrapper) {
if (wrapper == null) {
return;
}
if (MpUserAuth.isMpUser()) {
List<Long> projectIds = manageableProjectIds();
if (Func.isNotEmpty(projectIds)) {
wrapper.in("project_id", projectIds);
}
return;
}
List<Long> deptIds = manageableDeptIds();
if (Func.isNotEmpty(deptIds)) {
wrapper.in("dept_id", deptIds);
}
}
/**
* 小程序调度端可见项目:所属组织 + 数据权限范围 + 数据层级范围 → 业务部门在范围内的项目。
* 空列表不过滤;[-1] 表示无权限。
*/
private List<Long> manageableProjectIds() {
List<Long> projectIds = mpUserDataScopeSupport.resolveVisibleProjectIdsForQuery();
return projectIds == null ? List.of() : projectIds;
}
/**
* 系统用户可见组织:普通账号只出所属组织,超管不过滤;组织为空不拦截。
*/ */
private List<Long> manageableDeptIds() { private List<Long> manageableDeptIds() {
if (TransportBusinessSupport.isAdministrator()) { if (TransportBusinessSupport.isAdministrator()) {
@@ -911,10 +951,12 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
} }
private Set<Long> loadIncompleteExceptionWaybillIds() { private Set<Long> loadIncompleteExceptionWaybillIds() {
List<ExceptionDisposal> list = exceptionDisposalService.list(Wrappers.<ExceptionDisposal>lambdaQuery() LambdaQueryWrapper<ExceptionDisposal> wrapper = Wrappers.<ExceptionDisposal>lambdaQuery()
.select(ExceptionDisposal::getWaybillId) .select(ExceptionDisposal::getWaybillId)
.in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING) .in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING)
.isNotNull(ExceptionDisposal::getWaybillId)); .isNotNull(ExceptionDisposal::getWaybillId);
applyExceptionDeptScope(wrapper);
List<ExceptionDisposal> list = exceptionDisposalService.list(wrapper);
Set<Long> ids = new HashSet<>(); Set<Long> ids = new HashSet<>();
for (ExceptionDisposal item : list) { for (ExceptionDisposal item : list) {
if (item.getWaybillId() != null) { if (item.getWaybillId() != null) {
@@ -924,6 +966,38 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
return ids; return ids;
} }
/** 异常与运单同一套项目口径(小程序);系统用户仍按组织 */
private void applyExceptionDeptScope(LambdaQueryWrapper<ExceptionDisposal> wrapper) {
if (MpUserAuth.isMpUser()) {
List<Long> projectIds = manageableProjectIds();
if (Func.isNotEmpty(projectIds)) {
wrapper.in(ExceptionDisposal::getProjectId, projectIds);
}
return;
}
List<Long> deptIds = manageableDeptIds();
if (Func.isNotEmpty(deptIds)) {
wrapper.in(ExceptionDisposal::getDeptId, deptIds);
}
}
/** 风险无项目字段:小程序按关联运单的项目收口;系统用户按组织 */
private void applyRiskDeptScope(LambdaQueryWrapper<RiskDisposal> wrapper) {
if (MpUserAuth.isMpUser()) {
List<Long> projectIds = manageableProjectIds();
if (Func.isNotEmpty(projectIds)) {
String inList = projectIds.stream().map(String::valueOf).collect(Collectors.joining(","));
wrapper.inSql(RiskDisposal::getWaybillId,
"SELECT id FROM blade_waybill WHERE is_deleted = 0 AND project_id IN (" + inList + ")");
}
return;
}
List<Long> deptIds = manageableDeptIds();
if (Func.isNotEmpty(deptIds)) {
wrapper.in(RiskDisposal::getDeptId, deptIds);
}
}
private AdminWaybillCardVO toCard(Waybill waybill, boolean hasException) { private AdminWaybillCardVO toCard(Waybill waybill, boolean hasException) {
AdminWaybillCardVO card = new AdminWaybillCardVO(); AdminWaybillCardVO card = new AdminWaybillCardVO();
card.setId(waybill.getId()); card.setId(waybill.getId());
@@ -995,7 +1069,7 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
return switch (businessStatus) { return switch (businessStatus) {
case STATUS_PENDING, "waiting_dispatch", "dispatching" -> 0; case STATUS_PENDING, "waiting_dispatch", "dispatching" -> 0;
case STATUS_RUNNING -> 1; case STATUS_RUNNING -> 1;
case STATUS_COMPLETED -> 2; case WaybillProcessSupport.STATUS_PENDING_RETURN, STATUS_COMPLETED -> 2;
case STATUS_CANCELLED -> 3; case STATUS_CANCELLED -> 3;
default -> null; default -> null;
}; };
@@ -1051,13 +1125,17 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
} }
private long countIncompleteExceptions() { private long countIncompleteExceptions() {
return exceptionDisposalService.count(Wrappers.<ExceptionDisposal>lambdaQuery() LambdaQueryWrapper<ExceptionDisposal> wrapper = Wrappers.<ExceptionDisposal>lambdaQuery()
.in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING)); .in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING);
applyExceptionDeptScope(wrapper);
return exceptionDisposalService.count(wrapper);
} }
private long countPendingRisks() { private long countPendingRisks() {
return riskDisposalService.count(Wrappers.<RiskDisposal>lambdaQuery() LambdaQueryWrapper<RiskDisposal> wrapper = Wrappers.<RiskDisposal>lambdaQuery()
.eq(RiskDisposal::getDisposalStatus, RISK_PENDING)); .eq(RiskDisposal::getDisposalStatus, RISK_PENDING);
applyRiskDeptScope(wrapper);
return riskDisposalService.count(wrapper);
} }
/** /**
@@ -1099,10 +1177,12 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
} }
private List<TimedTodo> buildExceptionFeed() { private List<TimedTodo> buildExceptionFeed() {
List<ExceptionDisposal> list = exceptionDisposalService.list(Wrappers.<ExceptionDisposal>lambdaQuery() LambdaQueryWrapper<ExceptionDisposal> wrapper = Wrappers.<ExceptionDisposal>lambdaQuery()
.in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING) .in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING)
.orderByDesc(ExceptionDisposal::getReportTime) .orderByDesc(ExceptionDisposal::getReportTime)
.last("LIMIT " + FEED_LIMIT)); .last("LIMIT " + FEED_LIMIT);
applyExceptionDeptScope(wrapper);
List<ExceptionDisposal> list = exceptionDisposalService.list(wrapper);
return list.stream() return list.stream()
.map(disposal -> new TimedTodo(toTodoItem(disposal), exceptionTimeOf(disposal))) .map(disposal -> new TimedTodo(toTodoItem(disposal), exceptionTimeOf(disposal)))
.collect(Collectors.toList()); .collect(Collectors.toList());
@@ -2184,14 +2184,11 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
BigDecimal remainingApplied = money(entity.getAppliedAmount()); BigDecimal remainingApplied = money(entity.getAppliedAmount());
BigDecimal remainingPaid = money(entity.getPaidAmount()); BigDecimal remainingPaid = money(entity.getPaidAmount());
if (customAmounts) { if (customAmounts) {
BigDecimal lineTotal = BigDecimal.ZERO;
for (FormalSettlement settlement : settlements) { for (FormalSettlement settlement : settlements) {
BigDecimal payable = payableOfFormalSettlement(settlement.getId(), settlement.getSettlementAmount(), entity.getId()); BigDecimal payable = payableOfFormalSettlement(settlement.getId(), settlement.getSettlementAmount(), entity.getId());
BigDecimal applied = this.specifiedAppliedAmount(lines, settlement.getId(), null); BigDecimal applied = this.specifiedAppliedAmount(lines, settlement.getId(), null);
this.assertAppliedWithinPayable(settlement.getFormalSettlementNo(), applied, payable); this.assertAppliedWithinPayable(settlement.getFormalSettlementNo(), applied, payable);
lineTotal = lineTotal.add(applied);
} }
this.assertLineTotalMatches(entity, lineTotal);
} }
for (int index = 0; index < settlements.size(); index++) { for (int index = 0; index < settlements.size(); index++) {
FormalSettlement settlement = settlements.get(index); FormalSettlement settlement = settlements.get(index);
@@ -2289,20 +2286,18 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
return; return;
} }
// 单选/多选均落关联行,生成即占用可付款额度(删除草稿或审核不通过后释放) // 单选/多选均落关联行,生成即占用可付款额度(删除草稿或审核不通过后释放)
boolean customAmounts = targetSettlements.size() > 1 && lines != null && !lines.isEmpty(); // 结算行付款金额独立于付款基本信息中的申请付款金额,有明细时按行保存
boolean customAmounts = this.useSpecifiedLineAmounts(lines);
BigDecimal totalBase = targetSettlements.stream() BigDecimal totalBase = targetSettlements.stream()
.map(item -> payableOfPreSettlement(item.getId(), item.getSettlementAmount(), entity.getId())) .map(item -> payableOfPreSettlement(item.getId(), item.getSettlementAmount(), entity.getId()))
.reduce(BigDecimal.ZERO, BigDecimal::add); .reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal remainingApplied = money(entity.getAppliedAmount()); BigDecimal remainingApplied = money(entity.getAppliedAmount());
if (customAmounts) { if (customAmounts) {
BigDecimal lineTotal = BigDecimal.ZERO;
for (PreSettlement settlement : targetSettlements) { for (PreSettlement settlement : targetSettlements) {
BigDecimal payable = payableOfPreSettlement(settlement.getId(), settlement.getSettlementAmount(), entity.getId()); BigDecimal payable = payableOfPreSettlement(settlement.getId(), settlement.getSettlementAmount(), entity.getId());
BigDecimal applied = this.specifiedAppliedAmount(lines, null, settlement.getId()); BigDecimal applied = this.specifiedAppliedAmount(lines, null, settlement.getId());
this.assertAppliedWithinPayable(settlement.getPreSettlementNo(), applied, payable); this.assertAppliedWithinPayable(settlement.getPreSettlementNo(), applied, payable);
lineTotal = lineTotal.add(applied);
} }
this.assertLineTotalMatches(entity, lineTotal);
} }
for (int index = 0; index < targetSettlements.size(); index++) { for (int index = 0; index < targetSettlements.size(); index++) {
PreSettlement settlement = targetSettlements.get(index); PreSettlement settlement = targetSettlements.get(index);
@@ -2353,11 +2348,9 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
} }
} }
private void assertLineTotalMatches(PaymentApplication entity, BigDecimal lineTotal) { /** 前端传入结算行付款金额时,按行落库,不再要求合计等于申请付款金额。 */
BigDecimal appliedAmount = money(entity.getAppliedAmount()).setScale(2, RoundingMode.HALF_UP); private boolean useSpecifiedLineAmounts(List<PaymentApplicationSettlementLineRequest> lines) {
if (lineTotal.setScale(2, RoundingMode.HALF_UP).compareTo(appliedAmount) != 0) { return lines != null && !lines.isEmpty();
throw new ServiceException("申请付款金额须等于各结算单付款金额合计");
}
} }
private void validateQuota(PaymentApplication entity) { private void validateQuota(PaymentApplication entity) {
@@ -2609,44 +2602,11 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
private void validateBill(BillLedger ledger, Long deptId, BigDecimal amount, boolean checkMaturity, private void validateBill(BillLedger ledger, Long deptId, BigDecimal amount, boolean checkMaturity,
Long paymentApplicationId, Long settlementId, Long preSettlementId) { Long paymentApplicationId, Long settlementId, Long preSettlementId) {
if (ledger == null || Objects.equals(ledger.getIsDeleted(), 1)) throw new ServiceException("所选汇票台账不存在"); if (ledger == null || Objects.equals(ledger.getIsDeleted(), 1)) throw new ServiceException("所选汇票台账不存在");
boolean partyMatched = settlementPartiesMatch(ledger, settlementId, preSettlementId);
if (!"received".equals(ledger.getBillType()) && !partyMatched) {
throw new ServiceException("汇票付款仅可选择收票类型的汇票,或出票单位与收票单位均为当前结算收付款方的汇票");
}
if (checkMaturity && (ledger.getMaturityDate() == null || ledger.getMaturityDate().isBefore(LocalDate.now()))) { if (checkMaturity && (ledger.getMaturityDate() == null || ledger.getMaturityDate().isBefore(LocalDate.now()))) {
throw new ServiceException("所选汇票已到期"); throw new ServiceException("所选汇票已到期");
} }
BigDecimal available = money(ledger.getAvailableBalance()).add(occupiedAmount(paymentApplicationId, ledger.getId())); BigDecimal available = money(ledger.getAvailableBalance()).add(occupiedAmount(paymentApplicationId, ledger.getId()));
if (money(amount).compareTo(available) > 0) throw new ServiceException("申请付款金额超过汇票可用余额"); if (money(amount).compareTo(available) > 0) throw new ServiceException("申请付款金额超过汇票可用余额");
if (!partyMatched && !departmentAvailable(ledger, deptId)) throw new ServiceException("当前使用部门不在汇票可用部门范围内");
}
/** 出票单位、收票单位与结算付款方、收款方一致(顺序不限) */
private boolean settlementPartiesMatch(BillLedger ledger, Long settlementId, Long preSettlementId) {
String payerName = null;
String payeeName = null;
if (settlementId != null) {
FormalSettlement settlement = formalSettlementMapper.selectById(settlementId);
if (settlement != null && !Objects.equals(settlement.getIsDeleted(), 1)) {
payerName = settlement.getPayerName();
payeeName = settlement.getPayeeName();
}
}
if ((payerName == null || payeeName == null) && preSettlementId != null) {
PreSettlement settlement = preSettlementMapper.selectById(preSettlementId);
if (settlement != null && !Objects.equals(settlement.getIsDeleted(), 1)) {
payerName = settlement.getPayerName();
payeeName = settlement.getPayeeName();
}
}
String issuer = ledger.getIssuerName() == null ? "" : ledger.getIssuerName().trim();
String receiver = ledger.getReceiverName() == null ? "" : ledger.getReceiverName().trim();
String payer = payerName == null ? "" : payerName.trim();
String payee = payeeName == null ? "" : payeeName.trim();
if (issuer.isEmpty() || receiver.isEmpty() || payer.isEmpty() || payee.isEmpty()) {
return false;
}
return (issuer.equals(payer) && receiver.equals(payee)) || (issuer.equals(payee) && receiver.equals(payer));
} }
private void useBillBalance(PaymentApplication entity) { private void useBillBalance(PaymentApplication entity) {
@@ -2802,18 +2762,6 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
return entity; return entity;
} }
private boolean departmentAvailable(BillLedger ledger, Long deptId) {
if (Func.isEmpty(ledger.getAvailableDeptIdsJson())) return false;
try {
Object parsed = JsonUtil.parse(ledger.getAvailableDeptIdsJson(), List.class);
if (!(parsed instanceof List<?> values)) return false;
if (values.stream().anyMatch(value -> "all".equals(String.valueOf(value)))) return true;
return deptId != null && values.stream().anyMatch(value -> String.valueOf(deptId).equals(String.valueOf(value)));
} catch (Exception exception) {
throw new ServiceException("汇票可用部门配置不正确");
}
}
@Override @Override
public boolean isBillPayment(String paymentMethod) { public boolean isBillPayment(String paymentMethod) {
if (Func.isEmpty(paymentMethod)) return false; if (Func.isEmpty(paymentMethod)) return false;
@@ -203,13 +203,15 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
vo.setAdvances(advances); vo.setAdvances(advances);
vo.setProjectAdvanceWriteOffs(projectAdvanceWriteOffService.listBySettlement( vo.setProjectAdvanceWriteOffs(projectAdvanceWriteOffService.listBySettlement(
TransportBusiConstant.SETTLEMENT_BILL_PRE, id)); TransportBusiConstant.SETTLEMENT_BILL_PRE, id));
vo.setChangeRecords(changeRecordMapper.selectList(Wrappers.<PreSettlementChangeRecord>lambdaQuery() List<PreSettlementChangeRecord> changeRecords = changeRecordMapper.selectList(
Wrappers.<PreSettlementChangeRecord>lambdaQuery()
.eq(PreSettlementChangeRecord::getPreSettlementId, id) .eq(PreSettlementChangeRecord::getPreSettlementId, id)
.eq(PreSettlementChangeRecord::getIsDeleted, 0) .eq(PreSettlementChangeRecord::getIsDeleted, 0)
.and(wrapper -> wrapper.isNull(PreSettlementChangeRecord::getChangeContent) .orderByDesc(PreSettlementChangeRecord::getChangeTime));
.or() // 审批通过/驳回类不展示在变更记录中
.ne(PreSettlementChangeRecord::getChangeContent, "预结算审批通过")) vo.setChangeRecords(changeRecords.stream()
.orderByDesc(PreSettlementChangeRecord::getChangeTime))); .filter(record -> !isApprovalChangeContent(record.getChangeContent()))
.toList());
vo.setPrintTemplates(resolvePrintTemplates(settlement)); vo.setPrintTemplates(resolvePrintTemplates(settlement));
return vo; return vo;
} }
@@ -349,6 +351,8 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
settlement.setLocalCurrency(LOCAL_CURRENCY); settlement.setLocalCurrency(LOCAL_CURRENCY);
} }
fillContract(settlement, contract); fillContract(settlement, contract);
settlement.setBusinessModel(limitText(request.getBusinessModel(), 64, "业务模式"));
settlement.setTransportType(limitText(request.getTransportType(), 64, "运输方式"));
settlement.setExchangeRateDate(request.getExchangeRateDate()); settlement.setExchangeRateDate(request.getExchangeRateDate());
settlement.setExchangeRate(request.getExchangeRate()); settlement.setExchangeRate(request.getExchangeRate());
settlement.setAttachmentsJson(request.getAttachmentsJson()); settlement.setAttachmentsJson(request.getAttachmentsJson());
@@ -441,7 +445,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
settlement.setCurrentNode("已驳回"); settlement.setCurrentNode("已驳回");
settlement.setCurrentProcessor(AuthUtil.getUserName()); settlement.setCurrentProcessor(AuthUtil.getUserName());
updateById(settlement); updateById(settlement);
saveChange(settlement.getId(), "结算单基本信息", null, "调整", "预结算审批驳回", reason); // 审批驳回不落变更记录
} }
@Override @Override
@@ -1614,6 +1618,14 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
return source + " " + name; return source + " " + name;
} }
/** 审批通过、驳回等审批流转不落变更记录。 */
private boolean isApprovalChangeContent(String content) {
if (Func.isEmpty(content)) {
return false;
}
return content.contains("审批通过") || content.contains("审批驳回") || "已驳回".equals(content);
}
private void saveChange(Long settlementId, String changeType, Integer lineNo, String operationType, private void saveChange(Long settlementId, String changeType, Integer lineNo, String operationType,
String content, String reason) { String content, String reason) {
saveChange(settlementId, changeType, lineNo, operationType, content, reason, null, null); saveChange(settlementId, changeType, lineNo, operationType, content, reason, null, null);
@@ -1621,6 +1633,9 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
private void saveChange(Long settlementId, String changeType, Integer lineNo, String operationType, private void saveChange(Long settlementId, String changeType, Integer lineNo, String operationType,
String content, String reason, Map<String, Object> beforeData, Map<String, Object> afterData) { String content, String reason, Map<String, Object> beforeData, Map<String, Object> afterData) {
if (isApprovalChangeContent(content)) {
return;
}
PreSettlementChangeRecord record = new PreSettlementChangeRecord(); PreSettlementChangeRecord record = new PreSettlementChangeRecord();
record.setPreSettlementId(settlementId); record.setPreSettlementId(settlementId);
record.setChangeType(changeType); record.setChangeType(changeType);
@@ -34,6 +34,7 @@ import org.springblade.core.tenant.annotation.TenantIgnore;
import org.springblade.core.tool.jackson.JsonUtil; import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.BeanUtil; import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.Func; import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.SysCache;
import org.springblade.system.cache.UserCache; import org.springblade.system.cache.UserCache;
import org.springblade.system.pojo.entity.Dept; import org.springblade.system.pojo.entity.Dept;
import org.springblade.transport.constant.HcsSyncConstant; import org.springblade.transport.constant.HcsSyncConstant;
@@ -511,8 +512,7 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
projectApply.setUndertakeDeptName(dept.getDeptName()); projectApply.setUndertakeDeptName(dept.getDeptName());
} }
if (Func.isEmpty(projectApply.getBusinessDeptId())) { if (Func.isEmpty(projectApply.getBusinessDeptId())) {
projectApply.setBusinessDeptId(firstUndertakeDeptId(projectApply.getUndertakeDeptId())); fillDefaultBusinessDept(projectApply);
projectApply.setBusinessDeptName(firstUndertakeDeptName(projectApply.getUndertakeDeptName()));
} }
if (Func.isEmpty(projectApply.getHandlerUserId())) { if (Func.isEmpty(projectApply.getHandlerUserId())) {
projectApply.setHandlerUserId(AuthUtil.getUserId()); projectApply.setHandlerUserId(AuthUtil.getUserId());
@@ -771,7 +771,7 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
private void validateSubmit(ProjectApply projectApply) { private void validateSubmit(ProjectApply projectApply) {
validateDraft(projectApply); validateDraft(projectApply);
TransportBusinessSupport.validateRequired(projectApply.getProjectShortName(), "请输入项目简称"); TransportBusinessSupport.validateRequired(projectApply.getProjectShortName(), "请输入项目简称");
TransportBusinessSupport.validateRequired(projectApply.getBusinessDeptName(), "请选择业务部门"); validateBusinessDept(projectApply);
TransportBusinessSupport.validateRequired(projectApply.getUndertakeDeptName(), "请选择平台公司"); TransportBusinessSupport.validateRequired(projectApply.getUndertakeDeptName(), "请选择平台公司");
TransportBusinessSupport.validateRequired(projectApply.getBusinessMode(), "请选择业务模式"); TransportBusinessSupport.validateRequired(projectApply.getBusinessMode(), "请选择业务模式");
TransportBusinessSupport.validateRequired(projectApply.getCustomerNames(), "请选择客户"); TransportBusinessSupport.validateRequired(projectApply.getCustomerNames(), "请选择客户");
@@ -1089,6 +1089,48 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
return exception; return exception;
} }
/**
* 新增时默认业务部门:取当前用户所属部门中第一个「是否业务部门=是」的组织。
*/
private void fillDefaultBusinessDept(ProjectApply projectApply) {
List<Long> deptIds = Func.toLongList(AuthUtil.getDeptId());
if (Func.isEmpty(deptIds)) {
return;
}
for (Long deptId : deptIds) {
if (Func.isEmpty(deptId) || deptId <= 0) {
continue;
}
Dept dept = SysCache.getDept(deptId);
if (dept != null && Objects.equals(dept.getIsBusinessDept(), 1)) {
projectApply.setBusinessDeptId(dept.getId());
projectApply.setBusinessDeptName(dept.getDeptName());
return;
}
}
}
/**
* 校验业务部门必须是组织中标记为「是否业务部门=是」的部门。
*/
private void validateBusinessDept(ProjectApply projectApply) {
Long businessDeptId = projectApply.getBusinessDeptId();
if (Func.isEmpty(businessDeptId)) {
throw new ServiceException("请选择业务部门");
}
Dept dept = SysCache.getDept(businessDeptId);
if (dept == null || Objects.equals(dept.getIsDeleted(), 1)) {
throw new ServiceException("业务部门不存在");
}
if (!Objects.equals(dept.getIsBusinessDept(), 1)) {
throw new ServiceException("请选择组织中标记为业务部门的部门");
}
if (Func.isNotEmpty(dept.getDeptName())) {
projectApply.setBusinessDeptName(dept.getDeptName());
}
TransportBusinessSupport.validateRequired(projectApply.getBusinessDeptName(), "请选择业务部门");
}
private void normalizeUndertakeDept(ProjectApply projectApply) { private void normalizeUndertakeDept(ProjectApply projectApply) {
if (projectApply == null) { if (projectApply == null) {
return; return;
@@ -671,8 +671,10 @@ public class ReceivablePayableDetailServiceImpl
throw new ServiceException("费用调整试算数据不能为空"); throw new ServiceException("费用调整试算数据不能为空");
} }
ReceivablePayableDetail detail = getExisting(request.getDetailId()); ReceivablePayableDetail detail = getExisting(request.getDetailId());
if (!"pending".equals(detail.getSettlementStatus())) { // 试算不落库,预结算/正式结算调整明细也需按合同计费方案重算运费及其他按量费用
throw new ServiceException("仅待结算明细允许调整试算"); String settlementStatus = Func.toStr(detail.getSettlementStatus(), "pending");
if (!List.of("pending", "pre_settled", "formal_settled").contains(settlementStatus)) {
throw new ServiceException("当前结算状态不允许费用试算");
} }
ReceivablePayableCargoFee fee = cargoFeeMapper.selectOne( ReceivablePayableCargoFee fee = cargoFeeMapper.selectOne(
Wrappers.<ReceivablePayableCargoFee>lambdaQuery() Wrappers.<ReceivablePayableCargoFee>lambdaQuery()
@@ -3293,6 +3295,7 @@ public class ReceivablePayableDetailServiceImpl
private String waybillStatusName(String status) { private String waybillStatusName(String status) {
return switch (status == null ? "" : status) { return switch (status == null ? "" : status) {
case "completed" -> "已完成"; case "completed" -> "已完成";
case "pending_return" -> "待确认回单";
case "processing", "running", "in_progress", "inProgress" -> "进行中"; case "processing", "running", "in_progress", "inProgress" -> "进行中";
case "pending", "created" -> "待执行"; case "pending", "created" -> "待执行";
case "cancelled", "canceled" -> "已取消"; case "cancelled", "canceled" -> "已取消";
@@ -3,9 +3,9 @@ package org.springblade.transport.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import org.springblade.core.log.exception.ServiceException; import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl; import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.utils.BeanUtil; import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.Func; import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.UserCache; import org.springblade.system.cache.UserCache;
@@ -16,6 +16,7 @@ import org.springblade.transport.pojo.entity.RiskDisposal;
import org.springblade.transport.pojo.vo.RiskDisposalBatchResultVO; import org.springblade.transport.pojo.vo.RiskDisposalBatchResultVO;
import org.springblade.transport.pojo.vo.RiskDisposalVO; import org.springblade.transport.pojo.vo.RiskDisposalVO;
import org.springblade.transport.service.IRiskDisposalService; import org.springblade.transport.service.IRiskDisposalService;
import org.springblade.transport.support.MpUserDataScopeSupport;
import org.springblade.transport.support.TransportBusinessSupport; import org.springblade.transport.support.TransportBusinessSupport;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -25,12 +26,15 @@ import java.util.List;
import java.util.Objects; import java.util.Objects;
@Service @Service
@RequiredArgsConstructor
public class RiskDisposalServiceImpl extends BaseServiceImpl<RiskDisposalMapper, RiskDisposal> implements IRiskDisposalService { public class RiskDisposalServiceImpl extends BaseServiceImpl<RiskDisposalMapper, RiskDisposal> implements IRiskDisposalService {
private static final String STATUS_PENDING = "pending"; private static final String STATUS_PENDING = "pending";
private static final String STATUS_PROCESSED = "processed"; private static final String STATUS_PROCESSED = "processed";
private static final String STATUS_IGNORED = "ignored"; private static final String STATUS_IGNORED = "ignored";
private final MpUserDataScopeSupport mpUserDataScopeSupport;
@Override @Override
public IPage<RiskDisposalVO> selectPage(IPage<RiskDisposal> page, RiskDisposalVO query) { public IPage<RiskDisposalVO> selectPage(IPage<RiskDisposal> page, RiskDisposalVO query) {
IPage<RiskDisposal> entityPage = page(page, buildQuery(query)); IPage<RiskDisposal> entityPage = page(page, buildQuery(query));
@@ -84,15 +88,16 @@ public class RiskDisposalServiceImpl extends BaseServiceImpl<RiskDisposalMapper,
} }
private LambdaQueryWrapper<RiskDisposal> buildQuery(RiskDisposalVO query) { private LambdaQueryWrapper<RiskDisposal> buildQuery(RiskDisposalVO query) {
return Wrappers.<RiskDisposal>lambdaQuery() LambdaQueryWrapper<RiskDisposal> wrapper = Wrappers.<RiskDisposal>lambdaQuery()
.eq(RiskDisposal::getIsDeleted, 0) .eq(RiskDisposal::getIsDeleted, 0)
.like(Func.isNotEmpty(query.getRiskNo()), RiskDisposal::getRiskNo, query.getRiskNo()) .like(Func.isNotEmpty(query.getRiskNo()), RiskDisposal::getRiskNo, query.getRiskNo())
.like(Func.isNotEmpty(query.getRuleName()), RiskDisposal::getRuleName, query.getRuleName()) .like(Func.isNotEmpty(query.getRuleName()), RiskDisposal::getRuleName, query.getRuleName())
.like(Func.isNotEmpty(query.getWaybillNo()), RiskDisposal::getWaybillNo, query.getWaybillNo()) .like(Func.isNotEmpty(query.getWaybillNo()), RiskDisposal::getWaybillNo, query.getWaybillNo())
.like(Func.isNotEmpty(query.getProjectName()), RiskDisposal::getProjectName, query.getProjectName()) .like(Func.isNotEmpty(query.getProjectName()), RiskDisposal::getProjectName, query.getProjectName())
.eq(Func.isNotEmpty(query.getRiskLevel()), RiskDisposal::getRiskLevel, query.getRiskLevel()) .eq(Func.isNotEmpty(query.getRiskLevel()), RiskDisposal::getRiskLevel, query.getRiskLevel())
.eq(Func.isNotEmpty(query.getDisposalStatus()), RiskDisposal::getDisposalStatus, query.getDisposalStatus()) .eq(Func.isNotEmpty(query.getDisposalStatus()), RiskDisposal::getDisposalStatus, query.getDisposalStatus());
.orderByDesc(RiskDisposal::getCreateTime); mpUserDataScopeSupport.applyWaybillProjectScopeIfMpDispatcher(wrapper, RiskDisposal::getWaybillId);
return wrapper.orderByDesc(RiskDisposal::getCreateTime);
} }
private RiskDisposalVO toVO(RiskDisposal entity) { private RiskDisposalVO toVO(RiskDisposal entity) {
@@ -119,8 +119,9 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
.orderByDesc(FormalSettlement::getCreateTime)); .orderByDesc(FormalSettlement::getCreateTime));
Set<Long> tailPaymentSettlementIds = tailPaymentSettlementIds(rows.stream() Set<Long> tailPaymentSettlementIds = tailPaymentSettlementIds(rows.stream()
.map(FormalSettlement::getId).filter(Objects::nonNull).toList()); .map(FormalSettlement::getId).filter(Objects::nonNull).toList());
// 仅排除已完成收/付款、已发起尾款付款的正式结算单。
// 注意:sourceType=「预结算合并」是由预结算转正式的常规来源,不是尾款单,不能据此过滤。
rows = rows.stream().filter(row -> !isSettlementCompleted(row) rows = rows.stream().filter(row -> !isSettlementCompleted(row)
&& !isTailSettlement(row)
&& !tailPaymentSettlementIds.contains(row.getId())).toList(); && !tailPaymentSettlementIds.contains(row.getId())).toList();
List<Map<String, Object>> result = new ArrayList<>(); List<Map<String, Object>> result = new ArrayList<>();
for (FormalSettlement row : rows) { for (FormalSettlement row : rows) {
@@ -467,9 +468,6 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
if (isSettlementCompleted(formal)) { if (isSettlementCompleted(formal)) {
throw new ServiceException("该正式结算单已完成收/付款,不能进行结算调整"); throw new ServiceException("该正式结算单已完成收/付款,不能进行结算调整");
} }
if (isTailSettlement(formal)) {
throw new ServiceException("尾款结算正式结算单不能进行结算调整");
}
if (hasTailPayment(formal.getId())) { if (hasTailPayment(formal.getId())) {
throw new ServiceException("该正式结算单已发起尾款结算,不能进行结算调整"); throw new ServiceException("该正式结算单已发起尾款结算,不能进行结算调整");
} }
@@ -480,9 +478,6 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
|| (money(formal.getSettlementAmount()).signum() > 0 || (money(formal.getSettlementAmount()).signum() > 0
&& money(formal.getPaidAmount()).compareTo(money(formal.getSettlementAmount())) >= 0); && money(formal.getPaidAmount()).compareTo(money(formal.getSettlementAmount())) >= 0);
} }
private boolean isTailSettlement(FormalSettlement formal) {
return formal != null && "预结算合并".equals(formal.getSourceType());
}
private boolean hasTailPayment(Long formalSettlementId) { private boolean hasTailPayment(Long formalSettlementId) {
return formalSettlementId != null && tailPaymentSettlementIds(List.of(formalSettlementId)) return formalSettlementId != null && tailPaymentSettlementIds(List.of(formalSettlementId))
.contains(formalSettlementId); .contains(formalSettlementId);
@@ -76,6 +76,7 @@ import org.springblade.transport.service.IReceivablePayableDetailService;
import org.springblade.transport.service.ISettlementImportBatchService; import org.springblade.transport.service.ISettlementImportBatchService;
import org.springblade.transport.service.IWaybillImportBatchService; import org.springblade.transport.service.IWaybillImportBatchService;
import org.springblade.transport.service.IWaybillService; import org.springblade.transport.service.IWaybillService;
import com.fasterxml.jackson.core.type.TypeReference;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@@ -220,12 +221,14 @@ public class SettlementImportBatchServiceImpl
rows = buildRowsFromWaybills(listBatchWaybills(batch)); rows = buildRowsFromWaybills(listBatchWaybills(batch));
} }
vo.setRows(rows); vo.setRows(rows);
// 费用列名优先取库字段;若历史数据解析失败或缺失,从明细 feeItems 回补,避免草稿再打开丢列
vo.setFeeItemNames(mergeFeeItemNames(vo.getFeeItemNames(), rows));
return vo; return vo;
} }
@Override @Override
public void exportTemplate(HttpServletResponse response) { public void exportTemplate(HttpServletResponse response) {
SettlementImportTemplateExporter.export(response, loadTransportTypeLabels(), loadEnabledFeeItemNames()); SettlementImportTemplateExporter.export(response, loadTransportTypeLabels());
} }
@Override @Override
@@ -867,21 +870,56 @@ public class SettlementImportBatchServiceImpl
} }
private List<String> resolveFeeItemNames(SettlementImportBatchRequest request) { private List<String> resolveFeeItemNames(SettlementImportBatchRequest request) {
LinkedHashSet<String> names = new LinkedHashSet<>();
if (request.getFeeItemNames() != null) { if (request.getFeeItemNames() != null) {
return request.getFeeItemNames().stream().filter(Func::isNotEmpty).distinct().toList(); request.getFeeItemNames().stream()
.filter(Func::isNotEmpty)
.map(String::trim)
.filter(Func::isNotEmpty)
.forEach(names::add);
} }
Set<String> names = new LinkedHashSet<>();
if (Func.isNotEmpty(request.getRows())) { if (Func.isNotEmpty(request.getRows())) {
for (Map<String, Object> row : request.getRows()) { for (Map<String, Object> row : request.getRows()) {
Object feeItems = row.get("feeItems"); collectFeeItemNamesFromRow(row, names);
if (feeItems instanceof Map<?, ?> raw) {
raw.keySet().stream().filter(Objects::nonNull).map(String::valueOf).forEach(names::add);
}
} }
} }
return new ArrayList<>(names); return new ArrayList<>(names);
} }
/** 合并请求/库中的费用列名与明细行 feeItems 的键,保持插入顺序。 */
private List<String> mergeFeeItemNames(List<String> preferred, List<Map<String, Object>> rows) {
LinkedHashSet<String> names = new LinkedHashSet<>();
if (Func.isNotEmpty(preferred)) {
preferred.stream()
.filter(Func::isNotEmpty)
.map(String::trim)
.filter(Func::isNotEmpty)
.forEach(names::add);
}
if (Func.isNotEmpty(rows)) {
for (Map<String, Object> row : rows) {
collectFeeItemNamesFromRow(row, names);
}
}
return new ArrayList<>(names);
}
private void collectFeeItemNamesFromRow(Map<String, Object> row, Set<String> names) {
if (row == null || names == null) {
return;
}
Object feeItems = row.get("feeItems");
if (!(feeItems instanceof Map<?, ?> raw)) {
return;
}
raw.keySet().stream()
.filter(Objects::nonNull)
.map(String::valueOf)
.map(String::trim)
.filter(Func::isNotEmpty)
.forEach(names::add);
}
private List<Map<String, Object>> buildFeeRows(List<Map<String, Object>> rows, List<Waybill> waybills) { private List<Map<String, Object>> buildFeeRows(List<Map<String, Object>> rows, List<Waybill> waybills) {
List<Map<String, Object>> primaryRows = primaryRows(rows); List<Map<String, Object>> primaryRows = primaryRows(rows);
List<Map<String, Object>> feeRows = new ArrayList<>(); List<Map<String, Object>> feeRows = new ArrayList<>();
@@ -1061,12 +1099,35 @@ public class SettlementImportBatchServiceImpl
vo.setSettlementTypeName(settlementTypeName(batch.getSettlementType())); vo.setSettlementTypeName(settlementTypeName(batch.getSettlementType()));
vo.setSettlementBillTypeName(settlementBillTypeName(batch.getSettlementBillType())); vo.setSettlementBillTypeName(settlementBillTypeName(batch.getSettlementBillType()));
vo.setWaybillModeName(waybillModeName(batch.getWaybillMode())); vo.setWaybillModeName(waybillModeName(batch.getWaybillMode()));
if (Func.isNotEmpty(batch.getFeeItemNamesJson())) { vo.setFeeItemNames(parseFeeItemNamesJson(batch.getFeeItemNamesJson()));
vo.setFeeItemNames(JsonUtil.parseArray(batch.getFeeItemNamesJson(), String.class));
}
return vo; return vo;
} }
/**
* 解析费用列名 JSON。
* 注意:不能用 JsonUtil.parseArray(..., String.class),其内部按 List&lt;Map&gt; 解析,字符串数组会失败导致草稿再打开丢费用列。
*/
private List<String> parseFeeItemNamesJson(String feeItemNamesJson) {
if (Func.isEmpty(feeItemNamesJson)) {
return List.of();
}
try {
List<String> names = JsonUtil.parse(feeItemNamesJson, new TypeReference<List<String>>() {
});
if (names == null || names.isEmpty()) {
return List.of();
}
return names.stream()
.filter(Func::isNotEmpty)
.map(String::trim)
.filter(Func::isNotEmpty)
.distinct()
.toList();
} catch (Exception exception) {
return List.of();
}
}
private String importStatusName(String status) { private String importStatusName(String status) {
if (STATUS_DRAFT.equals(status)) { if (STATUS_DRAFT.equals(status)) {
return "草稿"; return "草稿";
@@ -1350,20 +1411,6 @@ public class SettlementImportBatchServiceImpl
return new ArrayList<>(labels); return new ArrayList<>(labels);
} }
private List<String> loadEnabledFeeItemNames() {
R<List<FeeItem>> response = sysClient.getFeeItems(false);
if (response == null || !response.isSuccess() || response.getData() == null) {
return List.of();
}
return response.getData().stream()
.filter(item -> item != null && (item.getStatus() == null || Objects.equals(item.getStatus(), 1)))
.map(FeeItem::getName)
.filter(Func::isNotEmpty)
.map(String::trim)
.distinct()
.toList();
}
/** 历史已完成批次无 rows_json 时,按关联运单回填查看明细。 */ /** 历史已完成批次无 rows_json 时,按关联运单回填查看明细。 */
private List<Map<String, Object>> buildRowsFromWaybills(List<Waybill> waybills) { private List<Map<String, Object>> buildRowsFromWaybills(List<Waybill> waybills) {
if (Func.isEmpty(waybills)) { if (Func.isEmpty(waybills)) {
@@ -601,6 +601,13 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
waybill.setContractId(plan.getContractId()); waybill.setContractId(plan.getContractId());
waybill.setContractName(plan.getContractName()); waybill.setContractName(plan.getContractName());
waybill.setCustomerName(plan.getCustomerName()); waybill.setCustomerName(plan.getCustomerName());
// 计划单若来自总单,调度运单须继承总单号与段号,供总单进度汇总
if (Func.isEmpty(waybill.getMasterNo()) && Func.isNotEmpty(plan.getMasterNo())) {
waybill.setMasterNo(plan.getMasterNo());
}
if (Func.isEmpty(waybill.getRelationNo()) && Func.isNotEmpty(plan.getRelationNo())) {
waybill.setRelationNo(plan.getRelationNo());
}
if (Func.isEmpty(waybill.getDepartureAddress())) { if (Func.isEmpty(waybill.getDepartureAddress())) {
waybill.setDepartureAddressId(plan.getDepartureAddressId()); waybill.setDepartureAddressId(plan.getDepartureAddressId());
waybill.setDepartureName(plan.getDepartureName()); waybill.setDepartureName(plan.getDepartureName());
@@ -205,6 +205,8 @@ public class TransportReconciliationServiceImpl
} }
List<TransportReconciliationInternal> internals = internalRows(id); List<TransportReconciliationInternal> internals = internalRows(id);
List<TransportReconciliationExternal> externals = externalRows(id); List<TransportReconciliationExternal> externals = externalRows(id);
internals.forEach(this::clearOptionalDecimalSentinels);
externals.forEach(this::normalizeExternalOptionalDecimals);
vo.setInternalDetails(internals); vo.setInternalDetails(internals);
vo.setExternalDetails(externals); vo.setExternalDetails(externals);
vo.setFeeSummary(feeSummary(vo.getFormalSettlementId(), externals)); vo.setFeeSummary(feeSummary(vo.getFormalSettlementId(), externals));
@@ -298,6 +300,7 @@ public class TransportReconciliationServiceImpl
List<TransportReconciliationExternal> validExternals = new ArrayList<>(); List<TransportReconciliationExternal> validExternals = new ArrayList<>();
for (int index = 0; index < rows.size(); index++) { for (int index = 0; index < rows.size(); index++) {
VehicleReconciliationExcel row = rows.get(index); VehicleReconciliationExcel row = rows.get(index);
normalizeImportVehicleDecimals(row);
List<String> validationErrors = validateImportVehicle(row); List<String> validationErrors = validateImportVehicle(row);
if (Func.isNotEmpty(validationErrors)) { if (Func.isNotEmpty(validationErrors)) {
// 原因同时写回原行:异常携带的是原表 rows, // 原因同时写回原行:异常携带的是原表 rows,
@@ -342,6 +345,7 @@ public class TransportReconciliationServiceImpl
external.setExternalLineNo(index + 1); external.setExternalLineNo(index + 1);
external.setActualDepartureTime(parseTime(row.getActualDepartureTime(), "实际发货时间")); external.setActualDepartureTime(parseTime(row.getActualDepartureTime(), "实际发货时间"));
external.setActualCompletionTime(parseTimeNullable(row.getActualCompletionTime(), "实际完成时间")); external.setActualCompletionTime(parseTimeNullable(row.getActualCompletionTime(), "实际完成时间"));
normalizeExternalOptionalDecimals(external);
Map<String, BigDecimal> feeItems = row.getFeeItems(); Map<String, BigDecimal> feeItems = row.getFeeItems();
if (feeItems == null || feeItems.isEmpty()) { if (feeItems == null || feeItems.isEmpty()) {
feeItems = new HashMap<>(); feeItems = new HashMap<>();
@@ -427,6 +431,7 @@ public class TransportReconciliationServiceImpl
List<TransportReconciliationExternal> validExternals = new ArrayList<>(); List<TransportReconciliationExternal> validExternals = new ArrayList<>();
for (int index = 0; index < rows.size(); index++) { for (int index = 0; index < rows.size(); index++) {
CargoReconciliationExcel row = rows.get(index); CargoReconciliationExcel row = rows.get(index);
normalizeImportCargoDecimals(row);
List<String> validationErrors = validateImportCargo(row); List<String> validationErrors = validateImportCargo(row);
if (Func.isNotEmpty(validationErrors)) { if (Func.isNotEmpty(validationErrors)) {
// 原因同时写回原行:异常携带的是原表 rows, // 原因同时写回原行:异常携带的是原表 rows,
@@ -471,6 +476,7 @@ public class TransportReconciliationServiceImpl
external.setExternalLineNo(index + 1); external.setExternalLineNo(index + 1);
external.setActualDepartureTime(parseTime(row.getActualDepartureTime(), "实际发货时间")); external.setActualDepartureTime(parseTime(row.getActualDepartureTime(), "实际发货时间"));
external.setActualCompletionTime(parseTimeNullable(row.getActualCompletionTime(), "实际完成时间")); external.setActualCompletionTime(parseTimeNullable(row.getActualCompletionTime(), "实际完成时间"));
normalizeExternalOptionalDecimals(external);
Map<String, BigDecimal> feeItems = row.getFeeItems(); Map<String, BigDecimal> feeItems = row.getFeeItems();
if (feeItems == null || feeItems.isEmpty()) { if (feeItems == null || feeItems.isEmpty()) {
feeItems = new LinkedHashMap<>(); feeItems = new LinkedHashMap<>();
@@ -729,10 +735,12 @@ public class TransportReconciliationServiceImpl
target.setModel(source.getModel()); target.setModel(source.getModel());
target.setTransportQuantity(nonNegative(source.getTransportQuantity(), "运输量")); target.setTransportQuantity(nonNegative(source.getTransportQuantity(), "运输量"));
target.setQuantityUnit(source.getQuantityUnit()); target.setQuantityUnit(source.getQuantityUnit());
target.setMileage(source.getMileage()); target.setMileage(normalizeOptionalDecimal(source.getMileage()));
target.setBatchNo(source.getBatchNo()); target.setBatchNo(source.getBatchNo());
target.setUnitPrice(source.getUnitPrice() == null ? null : nonNegative(source.getUnitPrice(), "运输单价")); BigDecimal unitPrice = normalizeOptionalDecimal(source.getUnitPrice());
target.setFreightAmount(source.getFreightAmount() == null ? null : nonNegative(source.getFreightAmount(), "运输费")); target.setUnitPrice(unitPrice == null ? null : nonNegative(unitPrice, "运输单价"));
BigDecimal freightAmount = normalizeOptionalDecimal(source.getFreightAmount());
target.setFreightAmount(freightAmount == null ? null : nonNegative(freightAmount, "运输费"));
target.setFeeItemsJson(source.getFeeItemsJson()); target.setFeeItemsJson(source.getFeeItemsJson());
target.setSettlementAmount(nonNegative(source.getSettlementAmount(), "结算费用合计")); target.setSettlementAmount(nonNegative(source.getSettlementAmount(), "结算费用合计"));
target.setMatchedInternalId(source.getMatchedInternalId()); target.setMatchedInternalId(source.getMatchedInternalId());
@@ -824,7 +832,6 @@ public class TransportReconciliationServiceImpl
applyAmount(bill, internal, external); applyAmount(bill, internal, external);
copyExternalToInternal(internal, external); copyExternalToInternal(internal, external);
internalMapper.updateById(internal); internalMapper.updateById(internal);
updateWaybill(internal, external);
} }
refreshFormalSummaryFees(bill.getFormalSettlementId()); refreshFormalSummaryFees(bill.getFormalSettlementId());
recalculateSettlement(bill); recalculateSettlement(bill);
@@ -867,50 +874,14 @@ public class TransportReconciliationServiceImpl
internal.setModel(external.getModel()); internal.setModel(external.getModel());
internal.setTransportQuantity(external.getTransportQuantity()); internal.setTransportQuantity(external.getTransportQuantity());
internal.setQuantityUnit(external.getQuantityUnit()); internal.setQuantityUnit(external.getQuantityUnit());
internal.setMileage(external.getMileage()); internal.setMileage(normalizeOptionalDecimal(external.getMileage()));
internal.setBatchNo(external.getBatchNo()); internal.setBatchNo(external.getBatchNo());
internal.setUnitPrice(external.getUnitPrice()); internal.setUnitPrice(normalizeOptionalDecimal(external.getUnitPrice()));
internal.setFreightAmount(external.getFreightAmount()); internal.setFreightAmount(normalizeOptionalDecimal(external.getFreightAmount()));
internal.setFeeItemsJson(external.getFeeItemsJson()); internal.setFeeItemsJson(external.getFeeItemsJson());
internal.setSettlementAmount(external.getSettlementAmount()); internal.setSettlementAmount(external.getSettlementAmount());
} }
private void updateWaybill(TransportReconciliationInternal internal,
TransportReconciliationExternal external) {
Long waybillId = null;
FormalSettlementDetail detail = internal.getFormalSettlementDetailId() == null ? null
: formalDetailMapper.selectById(internal.getFormalSettlementDetailId());
if (detail != null) waybillId = detail.getWaybillId();
if (waybillId == null && internal.getSourceDetailId() != null) {
ReceivablePayableDetail source = receivablePayableMapper.selectById(internal.getSourceDetailId());
if (source != null) waybillId = source.getWaybillId();
}
Waybill waybill = waybillId == null ? null : waybillMapper.selectById(waybillId);
if (waybill == null && Func.isNotEmpty(internal.getWaybillNo())) {
waybill = waybillMapper.selectOne(Wrappers.<Waybill>lambdaQuery()
.eq(Waybill::getWaybillNo, internal.getWaybillNo()));
}
if (waybill == null) return;
waybill.setVehicleNo(external.getVehicleNo());
waybill.setDepartureAddress(external.getDepartureAddress());
waybill.setArrivalAddress(external.getArrivalAddress());
waybill.setStartDate(external.getActualDepartureTime() == null ? null : external.getActualDepartureTime().toLocalDate());
waybill.setEndDate(external.getActualCompletionTime() == null ? null : external.getActualCompletionTime().toLocalDate());
waybill.setTransportType(external.getTransportType());
waybill.setCargoName(external.getCargoName());
waybill.setCargoType(external.getCargoType());
waybill.setSpecification(external.getSpecification());
waybill.setModel(external.getModel());
waybill.setQuantity(external.getTransportQuantity());
waybill.setQuantityUnit(external.getQuantityUnit());
waybill.setMileage(external.getMileage());
waybill.setBatchNo(external.getBatchNo());
waybill.setUnitPrice(external.getUnitPrice());
waybill.setFreightJson(external.getFeeItemsJson());
waybill.setOtherFeeTotal(money(external.getSettlementAmount()).subtract(money(external.getFreightAmount())).max(BigDecimal.ZERO));
waybillMapper.updateById(waybill);
}
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void complete(Long id) { public void complete(Long id) {
@@ -1309,7 +1280,8 @@ public class TransportReconciliationServiceImpl
bill.setInternalBillCount(internals.size()); bill.setExternalBillCount(externals.size()); bill.setInternalBillCount(internals.size()); bill.setExternalBillCount(externals.size());
int internalUnmatched = (int) internals.stream().filter(item -> !MATCHED.equals(item.getMatchResult())).count(); int internalUnmatched = (int) internals.stream().filter(item -> !MATCHED.equals(item.getMatchResult())).count();
int externalUnmatched = (int) externals.stream().filter(item -> !MATCHED.equals(item.getMatchStatus())).count(); int externalUnmatched = (int) externals.stream().filter(item -> !MATCHED.equals(item.getMatchStatus())).count();
bill.setMatchedCount(matched); bill.setUnmatchedCount(internalUnmatched + externalUnmatched); // 无法匹配按内部账单条数统计,不叠加外部未匹配数
bill.setMatchedCount(matched); bill.setUnmatchedCount(internalUnmatched);
bill.setDifferenceCount(Math.abs(internals.size() - externals.size()) + Math.min(internalUnmatched, externalUnmatched)); bill.setDifferenceCount(Math.abs(internals.size() - externals.size()) + Math.min(internalUnmatched, externalUnmatched));
bill.setInternalQuantity(sumInternalQuantity(internals)); bill.setExternalQuantity(sumExternalQuantity(externals)); bill.setInternalQuantity(sumInternalQuantity(internals)); bill.setExternalQuantity(sumExternalQuantity(externals));
bill.setDifferenceQuantity(bill.getInternalQuantity().subtract(bill.getExternalQuantity()).abs()); bill.setDifferenceQuantity(bill.getInternalQuantity().subtract(bill.getExternalQuantity()).abs());
@@ -1507,6 +1479,52 @@ public class TransportReconciliationServiceImpl
} }
private BigDecimal money(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; } private BigDecimal money(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; }
private BigDecimal nonNegative(BigDecimal value, String field) { if (value == null || value.compareTo(BigDecimal.ZERO) < 0) throw new ServiceException(field + "不能小于0"); return value; } private BigDecimal nonNegative(BigDecimal value, String field) { if (value == null || value.compareTo(BigDecimal.ZERO) < 0) throw new ServiceException(field + "不能小于0"); return value; }
/**
* BladeX 数字空值哨兵 -1 与 Excel 未填统一归一为 null,避免导入后单价显示成 -1。
*/
private BigDecimal normalizeOptionalDecimal(BigDecimal value) {
if (value == null) {
return null;
}
return value.compareTo(BigDecimal.ONE.negate()) == 0 ? null : value;
}
private void normalizeImportVehicleDecimals(VehicleReconciliationExcel row) {
if (row == null) {
return;
}
row.setUnitPrice(normalizeOptionalDecimal(row.getUnitPrice()));
row.setMileage(normalizeOptionalDecimal(row.getMileage()));
row.setFreightAmount(normalizeOptionalDecimal(row.getFreightAmount()));
}
private void normalizeImportCargoDecimals(CargoReconciliationExcel row) {
if (row == null) {
return;
}
row.setUnitPrice(normalizeOptionalDecimal(row.getUnitPrice()));
row.setMileage(normalizeOptionalDecimal(row.getMileage()));
row.setFreightAmount(normalizeOptionalDecimal(row.getFreightAmount()));
}
private void normalizeExternalOptionalDecimals(TransportReconciliationExternal external) {
if (external == null) {
return;
}
external.setUnitPrice(normalizeOptionalDecimal(external.getUnitPrice()));
external.setMileage(normalizeOptionalDecimal(external.getMileage()));
external.setFreightAmount(normalizeOptionalDecimal(external.getFreightAmount()));
}
private void clearOptionalDecimalSentinels(TransportReconciliationInternal internal) {
if (internal == null) {
return;
}
internal.setUnitPrice(normalizeOptionalDecimal(internal.getUnitPrice()));
internal.setMileage(normalizeOptionalDecimal(internal.getMileage()));
internal.setFreightAmount(normalizeOptionalDecimal(internal.getFreightAmount()));
}
private String limit(String value, int max) { if (value != null && value.length() > max) throw new ServiceException("备注不能超过" + max + "个字"); return value; } private String limit(String value, int max) { if (value != null && value.length() > max) throw new ServiceException("备注不能超过" + max + "个字"); return value; }
private BigDecimal sumInternalQuantity(List<TransportReconciliationInternal> rows) { return rows.stream().map(TransportReconciliationInternal::getTransportQuantity).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); } private BigDecimal sumInternalQuantity(List<TransportReconciliationInternal> rows) { return rows.stream().map(TransportReconciliationInternal::getTransportQuantity).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); }
private BigDecimal sumExternalQuantity(List<TransportReconciliationExternal> rows) { return rows.stream().map(TransportReconciliationExternal::getTransportQuantity).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); } private BigDecimal sumExternalQuantity(List<TransportReconciliationExternal> rows) { return rows.stream().map(TransportReconciliationExternal::getTransportQuantity).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); }
@@ -244,6 +244,7 @@ public class TripQueryServiceImpl implements ITripQueryService {
.or().ne(Waybill::getDriverAcceptStatus, WaybillProcessSupport.ACCEPT_REJECTED)); .or().ne(Waybill::getDriverAcceptStatus, WaybillProcessSupport.ACCEPT_REJECTED));
applyScope(wrapper, condition); applyScope(wrapper, condition);
applyDeptScope(wrapper, condition); applyDeptScope(wrapper, condition);
applyProjectScope(wrapper, condition);
applyOrgTypeFilter(wrapper, condition); applyOrgTypeFilter(wrapper, condition);
applyKeyword(wrapper, condition.getKeyword()); applyKeyword(wrapper, condition.getKeyword());
List<Waybill> rows = waybillService.list(wrapper); List<Waybill> rows = waybillService.list(wrapper);
@@ -264,6 +265,18 @@ public class TripQueryServiceImpl implements ITripQueryService {
wrapper.in(Waybill::getDeptId, condition.getDeptIds()); wrapper.in(Waybill::getDeptId, condition.getDeptIds());
} }
/**
* 项目收口:条件里给了项目 ID 集合时只出这些项目的运单行,空集合表示不过滤。
* <p>
* 小程序调度端先按用户数据权限解析可见项目(业务部门在范围内),再把项目 ID 传进来。
*/
private void applyProjectScope(LambdaQueryWrapper<Waybill> wrapper, TripQueryVO condition) {
if (Func.isEmpty(condition.getProjectIds())) {
return;
}
wrapper.in(Waybill::getProjectId, condition.getProjectIds());
}
/** /**
* 运输组织类型按子单行筛:它在同一趟车里必然是同一个值(配载的每张子单带同一个配载单号, * 运输组织类型按子单行筛:它在同一趟车里必然是同一个值(配载的每张子单带同一个配载单号,
* 未配载的每张自成一趟车),所以这一层不会把一趟车拆开。 * 未配载的每张自成一趟车),所以这一层不会把一趟车拆开。
@@ -803,6 +816,7 @@ public class TripQueryServiceImpl implements ITripQueryService {
copy.setDriverPhone(source.getDriverPhone()); copy.setDriverPhone(source.getDriverPhone());
copy.setAllDrivers(source.isAllDrivers()); copy.setAllDrivers(source.isAllDrivers());
copy.setDeptIds(source.getDeptIds()); copy.setDeptIds(source.getDeptIds());
copy.setProjectIds(source.getProjectIds());
copy.setKeyword(source.getKeyword()); copy.setKeyword(source.getKeyword());
copy.setTransportOrgType(source.getTransportOrgType()); copy.setTransportOrgType(source.getTransportOrgType());
copy.setException(source.getException()); copy.setException(source.getException());
@@ -36,7 +36,10 @@ import org.springblade.transport.service.IWaybillService;
import org.springblade.transport.wrapper.VoucherManageWrapper; import org.springblade.transport.wrapper.VoucherManageWrapper;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import io.minio.MinioClient; import io.minio.MinioClient;
import io.minio.GetPresignedObjectUrlArgs; import io.minio.GetPresignedObjectUrlArgs;
@@ -86,6 +89,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
private final LoadingManageMapper loadingManageMapper; private final LoadingManageMapper loadingManageMapper;
private final ApplicationEventPublisher eventPublisher; private final ApplicationEventPublisher eventPublisher;
private final ObjectProvider<MinioClient> minioClientProvider; private final ObjectProvider<MinioClient> minioClientProvider;
private final TransactionTemplate requiresNewTransactionTemplate;
private final String minioBucketName; private final String minioBucketName;
private final String minioRootDirectory; private final String minioRootDirectory;
private final String minioTransformEndpoint; private final String minioTransformEndpoint;
@@ -98,6 +102,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
VoucherImageMapper voucherImageMapper, VoucherFileMapper voucherFileMapper, VoucherImageMapper voucherImageMapper, VoucherFileMapper voucherFileMapper,
LoadingManageMapper loadingManageMapper, LoadingManageMapper loadingManageMapper,
ApplicationEventPublisher eventPublisher, ObjectProvider<MinioClient> minioClientProvider, ApplicationEventPublisher eventPublisher, ObjectProvider<MinioClient> minioClientProvider,
PlatformTransactionManager transactionManager,
@Value("${file.storage.minio.bucket-name:${minio.bucket-name:}}") String minioBucketName, @Value("${file.storage.minio.bucket-name:${minio.bucket-name:}}") String minioBucketName,
@Value("${file.storage.minio.root-directory:${minio.root-directory:}}") String minioRootDirectory, @Value("${file.storage.minio.root-directory:${minio.root-directory:}}") String minioRootDirectory,
@Value("${file.storage.minio.transform-endpoint:${minio.transform-endpoint:}}") String minioTransformEndpoint, @Value("${file.storage.minio.transform-endpoint:${minio.transform-endpoint:}}") String minioTransformEndpoint,
@@ -111,6 +116,9 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
this.loadingManageMapper = loadingManageMapper; this.loadingManageMapper = loadingManageMapper;
this.eventPublisher = eventPublisher; this.eventPublisher = eventPublisher;
this.minioClientProvider = minioClientProvider; this.minioClientProvider = minioClientProvider;
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
this.requiresNewTransactionTemplate = transactionTemplate;
this.minioBucketName = minioBucketName; this.minioBucketName = minioBucketName;
this.minioRootDirectory = minioRootDirectory; this.minioRootDirectory = minioRootDirectory;
this.minioTransformEndpoint = minioTransformEndpoint; this.minioTransformEndpoint = minioTransformEndpoint;
@@ -1156,8 +1164,10 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
int imageCount = 0; int imageCount = 0;
int matchedImageCount = 0; int matchedImageCount = 0;
Set<Long> relatedWaybillIds = new HashSet<>(); Set<Long> relatedWaybillIds = new HashSet<>();
assertParsableZipArchive(voucher.getFileName(), voucher.getFileUrl());
Path archivePath = downloadSourceFile(voucher.getFileUrl()); Path archivePath = downloadSourceFile(voucher.getFileUrl());
try { try {
assertZipMagic(archivePath);
Charset archiveCharset = detectArchiveCharset(archivePath); Charset archiveCharset = detectArchiveCharset(archivePath);
log.info("[凭证处理] 已识别压缩包文件名编码 voucherId={}, charset={}", voucherId, archiveCharset.name()); log.info("[凭证处理] 已识别压缩包文件名编码 voucherId={}, charset={}", voucherId, archiveCharset.name());
try (InputStream source = Files.newInputStream(archivePath); try (InputStream source = Files.newInputStream(archivePath);
@@ -1234,6 +1244,9 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
voucherId, fileCount, imageCount, matchedImageCount, relatedWaybillIds.size()); voucherId, fileCount, imageCount, matchedImageCount, relatedWaybillIds.size());
} }
} }
}
if (fileCount == 0) {
throw new ServiceException("压缩包无法解析或内没有找到有效文件,请检查后重新上传");
} }
log.info("[凭证处理] 进度 90%:文件解压上传完成 voucherId={}, fileCount={}, imageCount={}, matchedImageCount={}", log.info("[凭证处理] 进度 90%:文件解压上传完成 voucherId={}, fileCount={}, imageCount={}, matchedImageCount={}",
voucherId, fileCount, imageCount, matchedImageCount); voucherId, fileCount, imageCount, matchedImageCount);
@@ -1256,12 +1269,53 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
deleteTempArchive(archivePath); deleteTempArchive(archivePath);
} }
} catch (Exception exception) { } catch (Exception exception) {
// 独立事务落「处理失败」,避免本方法事务回滚把失败状态冲掉后一直卡在「处理中」。
markProcessFailed(voucher.getId());
log.error("凭证压缩包处理失败 voucherId:{}", voucherId, exception);
// 不向上抛:解析失败属业务终态,MQ 消费确认后不再重试。
}
}
/**
* 在新事务中写入处理失败,确保即使当前解析事务回滚,列表也能看到失败态并允许重新上传。
*/
private void markProcessFailed(Long voucherId) {
if (voucherId == null) {
return;
}
requiresNewTransactionTemplate.executeWithoutResult(status -> {
VoucherManage update = new VoucherManage(); VoucherManage update = new VoucherManage();
update.setId(voucher.getId()); update.setId(voucherId);
update.setProcessStatus("处理失败"); update.setProcessStatus("处理失败");
updateById(update); updateById(update);
log.error("凭证压缩包处理失败 voucherId:{}", voucherId, exception); });
throw new ServiceException("凭证压缩包处理失败"); }
/**
* 上传后缀为 7z 或非 zip 时直接判失败:后台仅支持标准 zip 解压。
*/
private void assertParsableZipArchive(String fileName, String fileUrl) {
String lowerName = Func.toStr(fileName, "").trim().toLowerCase(Locale.ROOT);
String lowerUrl = Func.toStr(fileUrl, "").trim().toLowerCase(Locale.ROOT);
if (lowerName.endsWith(".7z") || lowerUrl.contains(".7z")) {
throw new ServiceException("暂不支持 7z 压缩包,请转换为 zip 后重新上传");
}
boolean looksLikeZip = lowerName.endsWith(".zip") || lowerUrl.contains(".zip");
if (!looksLikeZip) {
throw new ServiceException("仅支持 zip 压缩包,当前文件无法解析");
}
}
/**
* 校验本地临时文件是否为 zip(PK 头),排除随意改后缀的文档。
*/
private void assertZipMagic(Path archivePath) throws Exception {
byte[] header = new byte[4];
try (InputStream inputStream = Files.newInputStream(archivePath)) {
int read = inputStream.read(header);
if (read < 4 || header[0] != 'P' || header[1] != 'K') {
throw new ServiceException("文件不是有效的 zip 压缩包,无法解析");
}
} }
} }
@@ -967,6 +967,7 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
*/ */
private static boolean passedAcceptStage(String businessStatus) { private static boolean passedAcceptStage(String businessStatus) {
return "running".equals(businessStatus) return "running".equals(businessStatus)
|| WaybillProcessSupport.STATUS_PENDING_RETURN.equals(businessStatus)
|| "completed".equals(businessStatus) || "completed".equals(businessStatus)
|| "cancelled".equals(businessStatus); || "cancelled".equals(businessStatus);
} }
@@ -1058,8 +1059,9 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
waybill.setProcessJson(oldRecord.getProcessJson()); waybill.setProcessJson(oldRecord.getProcessJson());
} }
} else { } else {
// 新建手工运单不应带配载号;总单/计划调度写入的 masterNo、relationNo 必须保留,
// 否则总单进度按 masterNo 汇总会一直显示「已调度 0」。
waybill.setLoadingNo(null); waybill.setLoadingNo(null);
waybill.setMasterNo(null);
waybill.setMileageRemark(null); waybill.setMileageRemark(null);
} }
// 新建若已带业务状态(如复制提交已完成)则在过程校正后回写,保证与原运单一致 // 新建若已带业务状态(如复制提交已完成)则在过程校正后回写,保证与原运单一致
@@ -1507,6 +1509,13 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public boolean confirmReturnReceipt(Long id) { public boolean confirmReturnReceipt(Long id) {
Waybill waybill = loadEditable(id, true); Waybill waybill = loadEditable(id, true);
String status = waybill.getBusinessStatus();
// pending_return 为正规待确认态;running/processing 兼容历史「打完卡仍停在进行中」的数据
if (!WaybillProcessSupport.STATUS_PENDING_RETURN.equals(status)
&& !"running".equals(status)
&& !"processing".equals(status)) {
throw new ServiceException("当前状态不允许确认回单");
}
String processJson = LiveProcessConfigSupport.resolve( String processJson = LiveProcessConfigSupport.resolve(
LiveProcessConfigSupport.byProject(processConfigService, waybill.getProjectId()), LiveProcessConfigSupport.byProject(processConfigService, waybill.getProjectId()),
waybill.getProcessJson()); waybill.getProcessJson());
@@ -1585,7 +1594,12 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
BusinessRemoveResultVO result = new BusinessRemoveResultVO(); BusinessRemoveResultVO result = new BusinessRemoveResultVO();
for (Waybill waybill : listByIds(idList)) { for (Waybill waybill : listByIds(idList)) {
try { try {
// 待确认回单走确认回单校验(打卡节点 + 未了结异常);其它状态仍走普通完成
if (WaybillProcessSupport.STATUS_PENDING_RETURN.equals(waybill.getBusinessStatus())) {
confirmReturnReceipt(waybill.getId());
} else {
complete(waybill.getId()); complete(waybill.getId());
}
result.setSuccessCount(result.getSuccessCount() + 1); result.setSuccessCount(result.getSuccessCount() + 1);
} catch (Exception exception) { } catch (Exception exception) {
log.error("批量完成运单失败,waybillId:{}, waybillNo:{}, failureReason:{}", log.error("批量完成运单失败,waybillId:{}, waybillNo:{}, failureReason:{}",
@@ -29,7 +29,7 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
/** /**
* 可配载准入判据:公路 + 待执行 + 未关联其它配载单。 * 可配载准入判据:公路 + 待执行/进行中 + 未关联其它配载单。
* <p> * <p>
* 接单状态不进判据:拒单票照样可配载。配载提交会把它的接单事实清成待接单, * 接单状态不进判据:拒单票照样可配载。配载提交会把它的接单事实清成待接单,
* 否则新司机看不到这张票——司机端入口一律排除已拒绝的运单。 * 否则新司机看不到这张票——司机端入口一律排除已拒绝的运单。
@@ -38,8 +38,11 @@ import java.util.Locale;
*/ */
public final class LoadableAdmissionSupport { public final class LoadableAdmissionSupport {
/** 可配载只放行待执行:已完成、已取消、草稿与调度中间态一律不可配载 */ /** 可配载业务状态:待执行、进行中;已完成、已取消、草稿与调度中间态一律不可配载 */
public static final String LOADABLE_BUSINESS_STATUS = WaybillProcessSupport.STATUS_PENDING; public static final List<String> LOADABLE_BUSINESS_STATUSES = List.of(
WaybillProcessSupport.STATUS_PENDING,
WaybillProcessSupport.STATUS_RUNNING
);
/** /**
* 公路运输方式的同义写法,按「包含」比对,与前端运单管理页的识别口径一致。 * 公路运输方式的同义写法,按「包含」比对,与前端运单管理页的识别口径一致。
@@ -56,7 +59,7 @@ public final class LoadableAdmissionSupport {
} }
/** /**
* 不可配载的原因,按判定顺序排列:公路 → 待执行 → 未关联其它配载单。 * 不可配载的原因,按判定顺序排列:公路 → 待执行/进行中 → 未关联其它配载单。
* <p> * <p>
* 判据只产出原因,中文文案由调用方映射——面向用户的措辞不进这一层。 * 判据只产出原因,中文文案由调用方映射——面向用户的措辞不进这一层。
*/ */
@@ -65,8 +68,8 @@ public final class LoadableAdmissionSupport {
/** 运输方式不是公路 */ /** 运输方式不是公路 */
NOT_ROAD, NOT_ROAD,
/** 业务状态不是待执行 */ /** 业务状态不是待执行或进行中 */
NOT_PENDING, NOT_LOADABLE_STATUS,
/** 已关联其它配载单 */ /** 已关联其它配载单 */
ON_OTHER_MANIFEST ON_OTHER_MANIFEST
@@ -119,8 +122,8 @@ public final class LoadableAdmissionSupport {
if (waybill == null || !isRoadTransport(waybill.getTransportType())) { if (waybill == null || !isRoadTransport(waybill.getTransportType())) {
return UnloadableReason.NOT_ROAD; return UnloadableReason.NOT_ROAD;
} }
if (!LOADABLE_BUSINESS_STATUS.equals(text(waybill.getBusinessStatus()))) { if (!LOADABLE_BUSINESS_STATUSES.contains(text(waybill.getBusinessStatus()))) {
return UnloadableReason.NOT_PENDING; return UnloadableReason.NOT_LOADABLE_STATUS;
} }
String loadingNo = text(waybill.getLoadingNo()); String loadingNo = text(waybill.getLoadingNo());
if (loadingNo.isEmpty() || loadingNo.equals(text(ownLoadingNo))) { if (loadingNo.isEmpty() || loadingNo.equals(text(ownLoadingNo))) {
@@ -148,13 +151,13 @@ public final class LoadableAdmissionSupport {
} }
/** /**
* 把整条可配载判据加到运单查询上:公路 + 待执行 + 未配载。 * 把整条可配载判据加到运单查询上:公路 + 待执行/进行中 + 未配载。
* *
* @param wrapper 运单列表查询条件 * @param wrapper 运单列表查询条件
*/ */
public static void applyLoadableConditions(LambdaQueryWrapper<Waybill> wrapper) { public static void applyLoadableConditions(LambdaQueryWrapper<Waybill> wrapper) {
applyRoadTransportCondition(wrapper); applyRoadTransportCondition(wrapper);
wrapper.eq(Waybill::getBusinessStatus, LOADABLE_BUSINESS_STATUS); wrapper.in(Waybill::getBusinessStatus, LOADABLE_BUSINESS_STATUSES);
wrapper.and(nested -> nested.isNull(Waybill::getLoadingNo).or().eq(Waybill::getLoadingNo, "")); wrapper.and(nested -> nested.isNull(Waybill::getLoadingNo).or().eq(Waybill::getLoadingNo, ""));
} }
@@ -0,0 +1,184 @@
/**
* 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.support;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import lombok.RequiredArgsConstructor;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.feign.IMpUserClient;
import org.springblade.system.pojo.enums.MpUserIdentity;
import org.springblade.system.pojo.vo.MpUserDataScopeVO;
import org.springblade.system.utils.MpUserAuth;
import org.springblade.transport.pojo.entity.ProjectApply;
import org.springblade.transport.service.IProjectApplyService;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 小程序调度端数据权限。
* <p>
* 口径:所属组织 + 数据权限范围 + 数据层级范围 → 可见组织 →
* 业务部门落在可见组织内的项目 → 再按这些项目收口运单/异常/风险。
*
* @author Chill
*/
@Component
@RequiredArgsConstructor
public class MpUserDataScopeSupport {
/**
* 无权限时的占位 ID:查询 in(-1) 保证查不到数据(空列表在运次查询里表示不过滤)。
*/
private static final Long NONE_ID = -1L;
private final IMpUserClient mpUserClient;
private final IProjectApplyService projectApplyService;
/**
* 当前登录是否为小程序调度端用户(含 admin 身份)
*/
public boolean isMpDispatcher() {
return MpUserAuth.isMpUser()
&& MpUserIdentity.has(AuthUtil.getUserRole(), MpUserIdentity.ADMIN);
}
/**
* 解析当前小程序用户可见组织;非小程序用户返回 null(调用方走原逻辑)。
*/
public MpUserDataScopeVO resolveCurrentMpUserScope() {
if (!MpUserAuth.isMpUser()) {
return null;
}
return resolveByUserId(AuthUtil.getUserId());
}
/**
* 解析指定小程序用户可见组织;Feign 失败时按无权限处理。
*/
public MpUserDataScopeVO resolveByUserId(Long userId) {
if (Func.isEmpty(userId) || userId <= 0) {
return MpUserDataScopeVO.empty();
}
R<MpUserDataScopeVO> result = mpUserClient.resolveDataScope(userId);
if (result == null || !result.isSuccess() || result.getData() == null) {
return MpUserDataScopeVO.empty();
}
return result.getData();
}
/**
* 转为查询用的组织 ID 约定:空=不过滤;非空=in;无权限=[-1]。
*/
public List<Long> toQueryDeptIds(MpUserDataScopeVO scope) {
if (scope == null || scope.isAllDept()) {
return List.of();
}
if (Func.isEmpty(scope.getDeptIds())) {
return List.of(NONE_ID);
}
return scope.getDeptIds();
}
/**
* 解析当前小程序用户可见项目 ID(业务部门落在数据权限组织范围内)。
* <ul>
* <li>空列表 = 全部组织 / 不过滤</li>
* <li>非空 = 仅这些项目</li>
* <li>无权限或范围内无项目 = [-1]</li>
* </ul>
* 非小程序用户返回 null。
*/
public List<Long> resolveVisibleProjectIdsForQuery() {
MpUserDataScopeVO scope = resolveCurrentMpUserScope();
if (scope == null) {
return null;
}
return resolveProjectIds(scope);
}
/**
* 按数据权限结果解析可见项目 ID。
*/
public List<Long> resolveProjectIds(MpUserDataScopeVO scope) {
if (scope == null || scope.isAllDept()) {
return List.of();
}
List<Long> deptIds = scope.getDeptIds();
if (Func.isEmpty(deptIds)) {
return List.of(NONE_ID);
}
List<ProjectApply> projectList = projectApplyService.list(Wrappers.<ProjectApply>lambdaQuery()
.select(ProjectApply::getId)
.in(ProjectApply::getBusinessDeptId, deptIds));
if (Func.isEmpty(projectList)) {
return List.of(NONE_ID);
}
List<Long> projectIds = projectList.stream()
.map(ProjectApply::getId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
return Func.isEmpty(projectIds) ? List.of(NONE_ID) : projectIds;
}
/**
* 当前为小程序调度用户时,按可见项目写入 project 列;非调度用户不改动。
*/
public <T> void applyProjectScopeIfMpDispatcher(LambdaQueryWrapper<T> wrapper, SFunction<T, ?> projectColumn) {
if (!isMpDispatcher() || wrapper == null || projectColumn == null) {
return;
}
List<Long> projectIds = resolveVisibleProjectIdsForQuery();
if (Func.isEmpty(projectIds)) {
return;
}
wrapper.in(projectColumn, projectIds);
}
/**
* 风险等无 projectId 字段时:按关联运单的项目收口。
*/
public <T> void applyWaybillProjectScopeIfMpDispatcher(LambdaQueryWrapper<T> wrapper, SFunction<T, ?> waybillIdColumn) {
if (!isMpDispatcher() || wrapper == null || waybillIdColumn == null) {
return;
}
List<Long> projectIds = resolveVisibleProjectIdsForQuery();
if (Func.isEmpty(projectIds)) {
return;
}
String inList = projectIds.stream().map(String::valueOf).collect(Collectors.joining(","));
wrapper.inSql(waybillIdColumn,
"SELECT id FROM blade_waybill WHERE is_deleted = 0 AND project_id IN (" + inList + ")");
}
}
@@ -49,6 +49,8 @@ public final class WaybillProcessSupport {
public static final String STATUS_PENDING = "pending"; public static final String STATUS_PENDING = "pending";
public static final String STATUS_RUNNING = "running"; public static final String STATUS_RUNNING = "running";
/** 司机打卡已完成、待运管员确认回单 */
public static final String STATUS_PENDING_RETURN = "pending_return";
public static final String ACCEPT_PENDING = "pending"; public static final String ACCEPT_PENDING = "pending";
public static final String ACCEPT_ACCEPTED = "accepted"; public static final String ACCEPT_ACCEPTED = "accepted";
public static final String ACCEPT_REJECTED = "rejected"; public static final String ACCEPT_REJECTED = "rejected";
@@ -134,14 +136,17 @@ public final class WaybillProcessSupport {
} }
/** /**
* 运单已结束:已完成或已取消。此后不再接受司机打卡。 * 运单已结束:已完成、已取消,或待确认回单。此后不再接受司机打卡。
* <p> * <p>
* 只认这两个状态,大小写不敏感。草稿、待执行、待调度、调度中的运单司机仍要能打卡, * 待确认回单表示司机侧节点已打完,等待运管确认,不能再打卡。
* 草稿、待执行、待调度、调度中的运单司机仍要能打卡,
* 所以不能按 {@link #isTerminalBusinessStatus} 的「终态或调度中间态」口径判定。 * 所以不能按 {@link #isTerminalBusinessStatus} 的「终态或调度中间态」口径判定。
*/ */
public static boolean isPunchEnded(String businessStatus) { public static boolean isPunchEnded(String businessStatus) {
String status = stringVal(businessStatus); String status = stringVal(businessStatus);
return "completed".equalsIgnoreCase(status) || "cancelled".equalsIgnoreCase(status); return "completed".equalsIgnoreCase(status)
|| "cancelled".equalsIgnoreCase(status)
|| STATUS_PENDING_RETURN.equalsIgnoreCase(status);
} }
/** /**
@@ -155,6 +160,9 @@ public final class WaybillProcessSupport {
if ("cancelled".equalsIgnoreCase(status)) { if ("cancelled".equalsIgnoreCase(status)) {
return "运单已取消"; return "运单已取消";
} }
if (STATUS_PENDING_RETURN.equalsIgnoreCase(status)) {
return "运单待确认回单";
}
return null; return null;
} }
@@ -213,7 +221,8 @@ public final class WaybillProcessSupport {
|| "completed".equals(businessStatus) || "completed".equals(businessStatus)
|| "cancelled".equals(businessStatus) || "cancelled".equals(businessStatus)
|| "waiting_dispatch".equals(businessStatus) || "waiting_dispatch".equals(businessStatus)
|| "dispatching".equals(businessStatus); || "dispatching".equals(businessStatus)
|| STATUS_PENDING_RETURN.equals(businessStatus);
} }
/** /**
@@ -221,7 +230,7 @@ public final class WaybillProcessSupport {
* <p> * <p>
* 需要接单(接单节点 confirmMode=yes)且尚未接单(未响应 / 已拒绝)→ pending(待执行); * 需要接单(接单节点 confirmMode=yes)且尚未接单(未响应 / 已拒绝)→ pending(待执行);
* 已接单 → running(进行中);不需要接单 → running(仅当当前为空或 pending 时提升)。 * 已接单 → running(进行中);不需要接单 → running(仅当当前为空或 pending 时提升)。
* draft / completed / cancelled 等终态或调度中间态不改动。 * draft / completed / cancelled / pending_return 等终态或调度中间态不改动。
*/ */
public static String normalizeBusinessStatus(String businessStatus, String processJson) { public static String normalizeBusinessStatus(String businessStatus, String processJson) {
return normalizeBusinessStatus(businessStatus, processJson, null); return normalizeBusinessStatus(businessStatus, processJson, null);
@@ -10,6 +10,9 @@ package org.springblade.transport.wrapper;
import org.springblade.core.mp.support.BaseEntityWrapper; import org.springblade.core.mp.support.BaseEntityWrapper;
import org.springblade.core.tool.utils.BeanUtil; import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.DictBizCache;
import org.springblade.system.cache.DictCache;
import org.springblade.system.cache.UserCache; import org.springblade.system.cache.UserCache;
import org.springblade.transport.pojo.entity.FormalSettlement; import org.springblade.transport.pojo.entity.FormalSettlement;
import org.springblade.transport.pojo.vo.FormalSettlementVO; import org.springblade.transport.pojo.vo.FormalSettlementVO;
@@ -46,6 +49,28 @@ public class FormalSettlementWrapper extends BaseEntityWrapper<FormalSettlement,
default -> entity.getApprovalStatus(); default -> entity.getApprovalStatus();
}); });
vo.setAdjustStatusName("adjusted".equals(entity.getAdjustStatus()) ? "已调整" : null); vo.setAdjustStatusName("adjusted".equals(entity.getAdjustStatus()) ? "已调整" : null);
vo.setBusinessModelName(dictBizName("business_model", entity.getBusinessModel()));
vo.setTransportTypeName(dictBizName("transport_type", entity.getTransportType()));
return vo; return vo;
} }
private String dictBizName(String code, String value) {
if (Func.isEmpty(value)) {
return value;
}
String name = "";
try {
name = DictBizCache.getValue(code, value);
} catch (Exception exception) {
name = "";
}
if (Func.isEmpty(name)) {
try {
name = DictCache.getValue(code, value);
} catch (Exception exception) {
name = "";
}
}
return Func.isEmpty(name) ? value : name;
}
} }
@@ -10,6 +10,9 @@ package org.springblade.transport.wrapper;
import org.springblade.core.mp.support.BaseEntityWrapper; import org.springblade.core.mp.support.BaseEntityWrapper;
import org.springblade.core.tool.utils.BeanUtil; import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.DictBizCache;
import org.springblade.system.cache.DictCache;
import org.springblade.system.cache.UserCache; import org.springblade.system.cache.UserCache;
import org.springblade.transport.pojo.entity.PreSettlement; import org.springblade.transport.pojo.entity.PreSettlement;
import org.springblade.transport.pojo.vo.PreSettlementVO; import org.springblade.transport.pojo.vo.PreSettlementVO;
@@ -34,6 +37,8 @@ public class PreSettlementWrapper extends BaseEntityWrapper<PreSettlement, PreSe
vo.setUpdateUserName(UserCache.getUserRealName(entity.getUpdateUser())); vo.setUpdateUserName(UserCache.getUserRealName(entity.getUpdateUser()));
vo.setApprovalStatusName(approvalStatusName(entity.getApprovalStatus())); vo.setApprovalStatusName(approvalStatusName(entity.getApprovalStatus()));
vo.setSettlementTypeName("receivable".equals(entity.getSettlementType()) ? "应收" : "应付"); vo.setSettlementTypeName("receivable".equals(entity.getSettlementType()) ? "应收" : "应付");
vo.setBusinessModelName(dictBizName("business_model", entity.getBusinessModel()));
vo.setTransportTypeName(dictBizName("transport_type", entity.getTransportType()));
return vo; return vo;
} }
@@ -48,4 +53,24 @@ public class PreSettlementWrapper extends BaseEntityWrapper<PreSettlement, PreSe
}; };
} }
private String dictBizName(String code, String value) {
if (Func.isEmpty(value)) {
return value;
}
String name = "";
try {
name = DictBizCache.getValue(code, value);
} catch (Exception exception) {
name = "";
}
if (Func.isEmpty(name)) {
try {
name = DictCache.getValue(code, value);
} catch (Exception exception) {
name = "";
}
}
return Func.isEmpty(name) ? value : name;
}
} }
@@ -122,6 +122,7 @@ public class WaybillWrapper extends BaseEntityWrapper<Waybill, WaybillVO> {
case "waiting_dispatch" -> "待调度"; case "waiting_dispatch" -> "待调度";
case "dispatching" -> "调度中"; case "dispatching" -> "调度中";
case "running", "processing", "in_progress", "inProgress" -> "进行中"; case "running", "processing", "in_progress", "inProgress" -> "进行中";
case "pending_return" -> "待确认回单";
case "completed" -> "已完成"; case "completed" -> "已完成";
case "cancelled" -> "已取消"; case "cancelled" -> "已取消";
case "closed" -> "已关闭"; case "closed" -> "已关闭";
@@ -52,10 +52,13 @@ class ExceptionDisposalListQueryTest {
@BeforeEach @BeforeEach
void setUp() throws Exception { void setUp() throws Exception {
disposalMapper = mock(ExceptionDisposalMapper.class); disposalMapper = mock(ExceptionDisposalMapper.class);
org.springblade.transport.support.MpUserDataScopeSupport scopeSupport =
mock(org.springblade.transport.support.MpUserDataScopeSupport.class);
service = new ExceptionDisposalServiceImpl( service = new ExceptionDisposalServiceImpl(
mock(ExceptionDisposalFollowRecordMapper.class), mock(ExceptionDisposalFollowRecordMapper.class),
mock(IWaybillService.class), mock(IWaybillService.class),
mock(IDriverWaybillService.class)); mock(IDriverWaybillService.class),
scopeSupport);
inject("baseMapper", disposalMapper); inject("baseMapper", disposalMapper);
when(disposalMapper.selectPage(any(), any())).thenReturn(new Page<ExceptionDisposal>(1, 10)); when(disposalMapper.selectPage(any(), any())).thenReturn(new Page<ExceptionDisposal>(1, 10));
} }
@@ -71,7 +71,8 @@ class ExceptionDisposalReportGateTest {
waybillService = mock(IWaybillService.class); waybillService = mock(IWaybillService.class);
driverWaybillService = mock(IDriverWaybillService.class); driverWaybillService = mock(IDriverWaybillService.class);
service = new ExceptionDisposalServiceImpl( service = new ExceptionDisposalServiceImpl(
mock(ExceptionDisposalFollowRecordMapper.class), waybillService, driverWaybillService); mock(ExceptionDisposalFollowRecordMapper.class), waybillService, driverWaybillService,
mock(org.springblade.transport.support.MpUserDataScopeSupport.class));
inject("baseMapper", disposalMapper); inject("baseMapper", disposalMapper);
when(waybillService.getById(WAYBILL_ID)).thenReturn(waybill()); when(waybillService.getById(WAYBILL_ID)).thenReturn(waybill());
@@ -139,13 +139,14 @@ class LoadingManageChildStatusWriteTest {
} }
@Test @Test
@DisplayName("不需接单的项目:提交时子单直接落进行中,不留待执行等读路径去提升") @DisplayName("不需接单的项目:提交时配载单与子单均为进行中")
void submitWritesRunningForProjectWithoutAcceptNode() { void submitWritesRunningForProjectWithoutAcceptNode() {
stubWaybills(waybill(NO_ACCEPT_WAYBILL_ID, NO_ACCEPT_PROJECT_ID)); stubWaybills(waybill(NO_ACCEPT_WAYBILL_ID, NO_ACCEPT_PROJECT_ID));
stubConfigs(config(NO_ACCEPT_PROJECT_ID, NO_ACCEPT_JSON)); stubConfigs(config(NO_ACCEPT_PROJECT_ID, NO_ACCEPT_JSON));
assertTrue(service.submit(loadingForm(NO_ACCEPT_WAYBILL_ID))); assertTrue(service.submit(loadingForm(NO_ACCEPT_WAYBILL_ID)));
assertEquals("running", capturedLoadingStatus());
CapturedUpdate statusUpdate = statusSyncUpdates().get(0); CapturedUpdate statusUpdate = statusSyncUpdates().get(0);
assertEquals("running", statusUpdate.setValue("business_status")); assertEquals("running", statusUpdate.setValue("business_status"));
assertEquals(List.of(NO_ACCEPT_WAYBILL_ID), statusUpdate.whereIds()); assertEquals(List.of(NO_ACCEPT_WAYBILL_ID), statusUpdate.whereIds());
@@ -153,13 +154,14 @@ class LoadingManageChildStatusWriteTest {
} }
@Test @Test
@DisplayName("需接单的项目:提交时子单落待执行,等新司机接单") @DisplayName("需接单的项目:提交时配载单与子单均为待执行,等新司机接单")
void submitWritesPendingForProjectWithAcceptNode() { void submitWritesPendingForProjectWithAcceptNode() {
stubWaybills(waybill(NEED_ACCEPT_WAYBILL_ID, NEED_ACCEPT_PROJECT_ID)); stubWaybills(waybill(NEED_ACCEPT_WAYBILL_ID, NEED_ACCEPT_PROJECT_ID));
stubConfigs(config(NEED_ACCEPT_PROJECT_ID, NEED_ACCEPT_JSON)); stubConfigs(config(NEED_ACCEPT_PROJECT_ID, NEED_ACCEPT_JSON));
assertTrue(service.submit(loadingForm(NEED_ACCEPT_WAYBILL_ID))); assertTrue(service.submit(loadingForm(NEED_ACCEPT_WAYBILL_ID)));
assertEquals("pending", capturedLoadingStatus());
assertEquals("pending", statusSyncUpdates().get(0).setValue("business_status")); assertEquals("pending", statusSyncUpdates().get(0).setValue("business_status"));
} }
@@ -367,12 +369,18 @@ class LoadingManageChildStatusWriteTest {
waybill.setProjectId(projectId); waybill.setProjectId(projectId);
waybill.setProcessJson(processJson); waybill.setProcessJson(processJson);
waybill.setDeptId(DEPT_ID_LONG); waybill.setDeptId(DEPT_ID_LONG);
// 可配载只放行公路 + 待执行 + 未配载,夹具的票都是这种票 // 可配载放行公路 + 待执行/进行中 + 未配载,夹具默认待执行
waybill.setTransportType("公路运输"); waybill.setTransportType("公路运输");
waybill.setBusinessStatus("pending"); waybill.setBusinessStatus("pending");
return waybill; return waybill;
} }
private String capturedLoadingStatus() {
ArgumentCaptor<LoadingManage> captor = ArgumentCaptor.forClass(LoadingManage.class);
verify(service, atLeastOnce()).saveOrUpdate(captor.capture());
return captor.getValue().getBusinessStatus();
}
private void stubWaybills(Waybill... waybills) { private void stubWaybills(Waybill... waybills) {
when(waybillMapper.selectList(ArgumentMatchers.<Wrapper<Waybill>>any())).thenReturn(List.of(waybills)); when(waybillMapper.selectList(ArgumentMatchers.<Wrapper<Waybill>>any())).thenReturn(List.of(waybills));
} }
@@ -84,7 +84,8 @@ class ManageAdminHomeFeedTest {
mock(IRiskDisposalService.class), mock(IRiskDisposalService.class),
mock(IDriverWaybillService.class), mock(IDriverWaybillService.class),
mock(ITripQueryService.class), mock(ITripQueryService.class),
mock(ILoadingManageService.class)); mock(ILoadingManageService.class),
mock(org.springblade.transport.support.MpUserDataScopeSupport.class));
when(exceptionService.list(ArgumentMatchers.<Wrapper<ExceptionDisposal>>any())) when(exceptionService.list(ArgumentMatchers.<Wrapper<ExceptionDisposal>>any()))
.thenReturn(new ArrayList<>()); .thenReturn(new ArrayList<>());
when(waybillService.list(ArgumentMatchers.<Wrapper<Waybill>>any())) when(waybillService.list(ArgumentMatchers.<Wrapper<Waybill>>any()))
@@ -101,7 +101,8 @@ class ManageTripListDetailTest {
mock(IRiskDisposalService.class), mock(IRiskDisposalService.class),
driverWaybillService, driverWaybillService,
tripQueryService, tripQueryService,
loadingManageService); loadingManageService,
mock(org.springblade.transport.support.MpUserDataScopeSupport.class));
when(exceptionDisposalService.list(ArgumentMatchers.<Wrapper<ExceptionDisposal>>any())) when(exceptionDisposalService.list(ArgumentMatchers.<Wrapper<ExceptionDisposal>>any()))
.thenReturn(new ArrayList<>()); .thenReturn(new ArrayList<>());
when(waybillService.list(ArgumentMatchers.<Wrapper<Waybill>>any())).thenReturn(new ArrayList<>()); when(waybillService.list(ArgumentMatchers.<Wrapper<Waybill>>any())).thenReturn(new ArrayList<>());
@@ -29,6 +29,7 @@ import org.springblade.transport.service.ILoadingManageService;
import org.springblade.transport.service.IRiskDisposalService; import org.springblade.transport.service.IRiskDisposalService;
import org.springblade.transport.service.ITripQueryService; import org.springblade.transport.service.ITripQueryService;
import org.springblade.transport.service.IWaybillService; import org.springblade.transport.service.IWaybillService;
import org.springblade.transport.support.MpUserDataScopeSupport;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
@@ -80,6 +81,8 @@ class ManageWaybillOrgScopeTest {
waybillService = mock(IWaybillService.class); waybillService = mock(IWaybillService.class);
tripQueryService = mock(ITripQueryService.class); tripQueryService = mock(ITripQueryService.class);
IExceptionDisposalService exceptionDisposalService = mock(IExceptionDisposalService.class); IExceptionDisposalService exceptionDisposalService = mock(IExceptionDisposalService.class);
MpUserDataScopeSupport mpUserDataScopeSupport = mock(MpUserDataScopeSupport.class);
when(mpUserDataScopeSupport.resolveVisibleProjectIdsForQuery()).thenReturn(null);
service = new ManageWaybillServiceImpl( service = new ManageWaybillServiceImpl(
waybillService, waybillService,
mock(IDriverService.class), mock(IDriverService.class),
@@ -87,7 +90,8 @@ class ManageWaybillOrgScopeTest {
mock(IRiskDisposalService.class), mock(IRiskDisposalService.class),
mock(IDriverWaybillService.class), mock(IDriverWaybillService.class),
tripQueryService, tripQueryService,
mock(ILoadingManageService.class)); mock(ILoadingManageService.class),
mpUserDataScopeSupport);
when(exceptionDisposalService.list(ArgumentMatchers.<Wrapper<ExceptionDisposal>>any())) when(exceptionDisposalService.list(ArgumentMatchers.<Wrapper<ExceptionDisposal>>any()))
.thenReturn(new ArrayList<>()); .thenReturn(new ArrayList<>());
when(waybillService.list(ArgumentMatchers.<Wrapper<Waybill>>any())).thenReturn(new ArrayList<>()); when(waybillService.list(ArgumentMatchers.<Wrapper<Waybill>>any())).thenReturn(new ArrayList<>());
@@ -68,7 +68,7 @@ class WaybillLoadableQueryTest {
} }
@Test @Test
@DisplayName("开关开启:公路词表、待执行、未配载一次全部进 wrapper,接单状态不参与") @DisplayName("开关开启:公路词表、待执行/进行中、未配载一次全部进 wrapper,接单状态不参与")
void loadableSwitchAppliesWholePredicate() { void loadableSwitchAppliesWholePredicate() {
LambdaQueryWrapper<Waybill> wrapper = capturedQuery(loadableQuery()); LambdaQueryWrapper<Waybill> wrapper = capturedQuery(loadableQuery());
String sql = String.valueOf(wrapper.getTargetSql()); String sql = String.valueOf(wrapper.getTargetSql());
@@ -79,10 +79,8 @@ class WaybillLoadableQueryTest {
assertTrue(params.contains("%" + keyword + "%"), assertTrue(params.contains("%" + keyword + "%"),
"公路词表要整份进条件,只写 road 会漏掉中文取值的运单:" + params); "公路词表要整份进条件,只写 road 会漏掉中文取值的运单:" + params);
} }
assertTrue(sql.contains("business_status =") && params.contains("pending"), assertTrue(sql.contains("business_status") && params.contains("pending") && params.contains("running"),
"只放行待执行:" + sql + " " + params); "放行待执行与进行中:" + sql + " " + params);
assertFalse(params.contains("running"),
"进行中一律不可配载,不能沿用「待执行与进行中都可配载」的旧口径:" + params);
assertTrue(sql.contains("loading_no IS NULL") && sql.contains("loading_no ="), assertTrue(sql.contains("loading_no IS NULL") && sql.contains("loading_no ="),
"未配载要同时覆盖 NULL 与空串:" + sql); "未配载要同时覆盖 NULL 与空串:" + sql);
assertFalse(sql.contains("driver_accept_status"), assertFalse(sql.contains("driver_accept_status"),
@@ -50,10 +50,12 @@ class LoadableAdmissionSupportTest {
// ------------------------------------------------------------ 单张运单准入 // ------------------------------------------------------------ 单张运单准入
@Test @Test
@DisplayName("公路 + 待执行 + 未配载才可配载") @DisplayName("公路 + 待执行/进行中 + 未配载才可配载")
void loadableOnlyWhenAllConditionsHold() { void loadableOnlyWhenAllConditionsHold() {
assertTrue(LoadableAdmissionSupport.isLoadable(waybill("road", "pending", null, "pending"), null)); assertTrue(LoadableAdmissionSupport.isLoadable(waybill("road", "pending", null, "pending"), null));
assertTrue(LoadableAdmissionSupport.isLoadable(waybill("公路运输", "pending", "", null), null)); assertTrue(LoadableAdmissionSupport.isLoadable(waybill("公路运输", "pending", "", null), null));
assertTrue(LoadableAdmissionSupport.isLoadable(waybill("road", "running", null, null), null),
"进行中也可配载");
} }
@Test @Test
@@ -64,12 +66,10 @@ class LoadableAdmissionSupportTest {
} }
@Test @Test
@DisplayName("非公路、非待执行、已关联配载单各自单独即可拒绝") @DisplayName("非公路、非待执行/进行中、已关联配载单各自单独即可拒绝")
void eachConditionRejectsOnItsOwn() { void eachConditionRejectsOnItsOwn() {
assertFalse(LoadableAdmissionSupport.isLoadable(waybill("railway", "pending", null, "pending"), null), assertFalse(LoadableAdmissionSupport.isLoadable(waybill("railway", "pending", null, "pending"), null),
"非公路不可配载"); "非公路不可配载");
assertFalse(LoadableAdmissionSupport.isLoadable(waybill("road", "running", null, null), null),
"进行中不可配载:不需要接单的项目保存后即为进行中,那是过程配置的产物,不代表可配载");
assertFalse(LoadableAdmissionSupport.isLoadable(waybill("road", "draft", null, null), null)); assertFalse(LoadableAdmissionSupport.isLoadable(waybill("road", "draft", null, null), null));
assertFalse(LoadableAdmissionSupport.isLoadable(waybill("road", "completed", null, null), null)); assertFalse(LoadableAdmissionSupport.isLoadable(waybill("road", "completed", null, null), null));
assertFalse(LoadableAdmissionSupport.isLoadable(waybill("road", "cancelled", null, null), null)); assertFalse(LoadableAdmissionSupport.isLoadable(waybill("road", "cancelled", null, null), null));
@@ -79,18 +79,19 @@ class LoadableAdmissionSupportTest {
} }
@Test @Test
@DisplayName("不可配载原因按 公路 → 待执行 → 未关联其它配载单 取第一条") @DisplayName("不可配载原因按 公路 → 待执行/进行中 → 未关联其它配载单 取第一条")
void unloadableReasonFollowsPredicateOrder() { void unloadableReasonFollowsPredicateOrder() {
assertEquals(LoadableAdmissionSupport.UnloadableReason.NOT_ROAD, assertEquals(LoadableAdmissionSupport.UnloadableReason.NOT_ROAD,
LoadableAdmissionSupport.unloadableReason( LoadableAdmissionSupport.unloadableReason(
waybill("railway", "running", "PZ20260101-002", null), "PZ20260101-001")); waybill("railway", "completed", "PZ20260101-002", null), "PZ20260101-001"));
assertEquals(LoadableAdmissionSupport.UnloadableReason.NOT_PENDING, assertEquals(LoadableAdmissionSupport.UnloadableReason.NOT_LOADABLE_STATUS,
LoadableAdmissionSupport.unloadableReason( LoadableAdmissionSupport.unloadableReason(
waybill("road", "running", "PZ20260101-002", null), "PZ20260101-001")); waybill("road", "completed", "PZ20260101-002", null), "PZ20260101-001"));
assertEquals(LoadableAdmissionSupport.UnloadableReason.ON_OTHER_MANIFEST, assertEquals(LoadableAdmissionSupport.UnloadableReason.ON_OTHER_MANIFEST,
LoadableAdmissionSupport.unloadableReason( LoadableAdmissionSupport.unloadableReason(
waybill("road", "pending", "PZ20260101-002", null), "PZ20260101-001")); waybill("road", "pending", "PZ20260101-002", null), "PZ20260101-001"));
assertNull(LoadableAdmissionSupport.unloadableReason(waybill("road", "pending", null, null), null)); assertNull(LoadableAdmissionSupport.unloadableReason(waybill("road", "pending", null, null), null));
assertNull(LoadableAdmissionSupport.unloadableReason(waybill("road", "running", null, null), null));
assertNull(LoadableAdmissionSupport.unloadableReason( assertNull(LoadableAdmissionSupport.unloadableReason(
waybill("road", "pending", "PZ20260101-001", null), "PZ20260101-001")); waybill("road", "pending", "PZ20260101-001", null), "PZ20260101-001"));
} }
@@ -337,12 +337,14 @@ class WaybillProcessSupportTest {
// ------------------------------------------------------------ 结束门禁 // ------------------------------------------------------------ 结束门禁
@Test @Test
@DisplayName("结束门禁:只认已完成与已取消,大小写不敏感") @DisplayName("结束门禁:已完成、已取消、待确认回单,大小写不敏感")
void punchEndedOnlyForCompletedAndCancelled() { void punchEndedOnlyForCompletedAndCancelled() {
assertTrue(WaybillProcessSupport.isPunchEnded("completed")); assertTrue(WaybillProcessSupport.isPunchEnded("completed"));
assertTrue(WaybillProcessSupport.isPunchEnded("COMPLETED")); assertTrue(WaybillProcessSupport.isPunchEnded("COMPLETED"));
assertTrue(WaybillProcessSupport.isPunchEnded("cancelled")); assertTrue(WaybillProcessSupport.isPunchEnded("cancelled"));
assertTrue(WaybillProcessSupport.isPunchEnded("Cancelled")); assertTrue(WaybillProcessSupport.isPunchEnded("Cancelled"));
assertTrue(WaybillProcessSupport.isPunchEnded("pending_return"));
assertTrue(WaybillProcessSupport.isPunchEnded("PENDING_RETURN"));
} }
@Test @Test
@@ -358,11 +360,12 @@ class WaybillProcessSupportTest {
} }
@Test @Test
@DisplayName("结束门禁文案:完成与取消各一句,非结束态没有文案") @DisplayName("结束门禁文案:完成、取消、待确认回单各一句,非结束态没有文案")
void punchEndedReasonOnlyForEndedStatuses() { void punchEndedReasonOnlyForEndedStatuses() {
assertEquals("运单已完成", WaybillProcessSupport.punchEndedReason("completed")); assertEquals("运单已完成", WaybillProcessSupport.punchEndedReason("completed"));
assertEquals("运单已完成", WaybillProcessSupport.punchEndedReason(" COMPLETED ")); assertEquals("运单已完成", WaybillProcessSupport.punchEndedReason(" COMPLETED "));
assertEquals("运单已取消", WaybillProcessSupport.punchEndedReason("cancelled")); assertEquals("运单已取消", WaybillProcessSupport.punchEndedReason("cancelled"));
assertEquals("运单待确认回单", WaybillProcessSupport.punchEndedReason("pending_return"));
assertNull(WaybillProcessSupport.punchEndedReason(null)); assertNull(WaybillProcessSupport.punchEndedReason(null));
assertNull(WaybillProcessSupport.punchEndedReason("")); assertNull(WaybillProcessSupport.punchEndedReason(""));
assertNull(WaybillProcessSupport.punchEndedReason(" ")); assertNull(WaybillProcessSupport.punchEndedReason(" "));
@@ -311,6 +311,7 @@ CREATE TABLE `blade_dept` (
`mnemonic_code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '助记码', `mnemonic_code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '助记码',
`carrier_customer_id` bigint NULL DEFAULT NULL COMMENT '承运商客商档案主键', `carrier_customer_id` bigint NULL DEFAULT NULL COMMENT '承运商客商档案主键',
`is_platform_company` tinyint NOT NULL DEFAULT 0 COMMENT '是否平台公司:0否,1是', `is_platform_company` tinyint NOT NULL DEFAULT 0 COMMENT '是否平台公司:0否,1是',
`is_business_dept` tinyint NOT NULL DEFAULT 0 COMMENT '是否业务部门:0否,1是',
`oa_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'OA主键', `oa_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'OA主键',
`oa_sup_sub_com_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'OA上级主键', `oa_sup_sub_com_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'OA上级主键',
`sort` int NULL DEFAULT NULL COMMENT '排序', `sort` int NULL DEFAULT NULL COMMENT '排序',
@@ -8,6 +8,7 @@ CREATE TABLE `blade_credit_score_quantification` (
`name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '评定表名称', `name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '评定表名称',
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
`standard_description` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标准说明', `standard_description` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标准说明',
`max_minus_score` decimal(10,2) NULL DEFAULT NULL COMMENT '减分项目最高扣分限制',
`create_user` bigint NULL DEFAULT NULL COMMENT '创建人', `create_user` bigint NULL DEFAULT NULL COMMENT '创建人',
`create_dept` bigint NULL DEFAULT NULL COMMENT '创建部门', `create_dept` bigint NULL DEFAULT NULL COMMENT '创建部门',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
@@ -0,0 +1,421 @@
-- ============================================================================
-- 评分量化表跨环境复制:按 ID 生成 INSERT 脚本
-- ============================================================================
-- 用法(在 DEV 库执行):
-- 1. 修改下方 @quantification_id 为要复制的评分量化表主键
-- 2. 整段执行,结果集中每一行是一条可直接在 UAT/PROD 执行的 SQL
-- 3. 先复制「清理」语句,再复制各表 INSERT,按结果顺序在目标库执行
--
-- 覆盖表(按外键依赖顺序):
-- blade_credit_score_quantification
-- blade_credit_score_category
-- blade_credit_score_item
-- blade_credit_score_item_option
-- blade_credit_rating_standard
--
-- 说明:
-- - 保留原主键与关联 ID,便于 UAT/PROD 与 DEV 配置一致
-- - 仅导出 is_deleted = 0 的有效数据
-- - 目标库若已存在同名评定表(tenant_id + name + is_deleted 唯一),请先处理冲突
-- ============================================================================
-- >>> 请修改为要导出的评分量化表 ID <<<
SET @quantification_id := 0;
-- ----------------------------------------------------------------------------
-- 0. 导出前校验:确认主表存在及子表数量
-- ----------------------------------------------------------------------------
SELECT
q.id,
q.tenant_id,
q.name,
q.status,
q.max_minus_score,
(SELECT COUNT(1) FROM blade_credit_score_category c
WHERE c.quantification_id = q.id AND c.is_deleted = 0) AS category_count,
(SELECT COUNT(1) FROM blade_credit_score_item i
WHERE i.quantification_id = q.id AND i.is_deleted = 0) AS item_count,
(SELECT COUNT(1) FROM blade_credit_score_item_option o
WHERE o.quantification_id = q.id AND o.is_deleted = 0) AS option_count,
(SELECT COUNT(1) FROM blade_credit_rating_standard s
WHERE s.quantification_id = q.id AND s.is_deleted = 0) AS standard_count
FROM blade_credit_score_quantification q
WHERE q.id = @quantification_id
AND q.is_deleted = 0;
-- ----------------------------------------------------------------------------
-- 1. 目标库清理语句(先删子表再删主表;结果复制到 UAT/PROD 优先执行)
-- ----------------------------------------------------------------------------
SELECT CONCAT(
'-- ========== 目标库清理(quantification_id=', @quantification_id, ') ==========\n',
'DELETE FROM `blade_credit_score_item_option` WHERE `quantification_id` = ', @quantification_id, ';\n',
'DELETE FROM `blade_credit_score_item` WHERE `quantification_id` = ', @quantification_id, ';\n',
'DELETE FROM `blade_credit_score_category` WHERE `quantification_id` = ', @quantification_id, ';\n',
'DELETE FROM `blade_credit_rating_standard` WHERE `quantification_id` = ', @quantification_id, ';\n',
'DELETE FROM `blade_credit_score_quantification` WHERE `id` = ', @quantification_id, ';\n',
'-- 若目标库存在同名不同 ID 的评定表,可按名称清理(按需取消注释):\n',
'-- DELETE o FROM `blade_credit_score_item_option` o\n',
'-- INNER JOIN `blade_credit_score_quantification` q ON q.id = o.quantification_id\n',
'-- WHERE q.tenant_id = ', IFNULL(QUOTE((SELECT tenant_id FROM blade_credit_score_quantification WHERE id = @quantification_id LIMIT 1)), 'NULL'),
' AND q.name = ', IFNULL(QUOTE((SELECT name FROM blade_credit_score_quantification WHERE id = @quantification_id LIMIT 1)), 'NULL'),
' AND q.is_deleted = 0;\n',
'-- DELETE i FROM `blade_credit_score_item` i\n',
'-- INNER JOIN `blade_credit_score_quantification` q ON q.id = i.quantification_id\n',
'-- WHERE q.tenant_id = ', IFNULL(QUOTE((SELECT tenant_id FROM blade_credit_score_quantification WHERE id = @quantification_id LIMIT 1)), 'NULL'),
' AND q.name = ', IFNULL(QUOTE((SELECT name FROM blade_credit_score_quantification WHERE id = @quantification_id LIMIT 1)), 'NULL'),
' AND q.is_deleted = 0;\n',
'-- DELETE c FROM `blade_credit_score_category` c\n',
'-- INNER JOIN `blade_credit_score_quantification` q ON q.id = c.quantification_id\n',
'-- WHERE q.tenant_id = ', IFNULL(QUOTE((SELECT tenant_id FROM blade_credit_score_quantification WHERE id = @quantification_id LIMIT 1)), 'NULL'),
' AND q.name = ', IFNULL(QUOTE((SELECT name FROM blade_credit_score_quantification WHERE id = @quantification_id LIMIT 1)), 'NULL'),
' AND q.is_deleted = 0;\n',
'-- DELETE s FROM `blade_credit_rating_standard` s\n',
'-- INNER JOIN `blade_credit_score_quantification` q ON q.id = s.quantification_id\n',
'-- WHERE q.tenant_id = ', IFNULL(QUOTE((SELECT tenant_id FROM blade_credit_score_quantification WHERE id = @quantification_id LIMIT 1)), 'NULL'),
' AND q.name = ', IFNULL(QUOTE((SELECT name FROM blade_credit_score_quantification WHERE id = @quantification_id LIMIT 1)), 'NULL'),
' AND q.is_deleted = 0;\n',
'-- DELETE FROM `blade_credit_score_quantification`\n',
'-- WHERE tenant_id = ', IFNULL(QUOTE((SELECT tenant_id FROM blade_credit_score_quantification WHERE id = @quantification_id LIMIT 1)), 'NULL'),
' AND name = ', IFNULL(QUOTE((SELECT name FROM blade_credit_score_quantification WHERE id = @quantification_id LIMIT 1)), 'NULL'),
' AND is_deleted = 0;'
) AS sql_text;
-- ----------------------------------------------------------------------------
-- 2. 主表 blade_credit_score_quantification
-- ----------------------------------------------------------------------------
SELECT CONCAT(
'INSERT INTO `blade_credit_score_quantification` (',
'`id`, `tenant_id`, `name`, `remark`, `standard_description`, `max_minus_score`, ',
'`create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`, `is_deleted`',
') VALUES (',
id, ', ',
IFNULL(QUOTE(tenant_id), 'NULL'), ', ',
IFNULL(QUOTE(name), 'NULL'), ', ',
IFNULL(QUOTE(remark), 'NULL'), ', ',
IFNULL(QUOTE(standard_description), 'NULL'), ', ',
IFNULL(CAST(max_minus_score AS CHAR), 'NULL'), ', ',
IFNULL(CAST(create_user AS CHAR), 'NULL'), ', ',
IFNULL(CAST(create_dept AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(update_user AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(update_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(status AS CHAR), 'NULL'), ', ',
IFNULL(CAST(is_deleted AS CHAR), '0'),
');'
) AS sql_text
FROM blade_credit_score_quantification
WHERE id = @quantification_id
AND is_deleted = 0;
-- ----------------------------------------------------------------------------
-- 3. 分类 blade_credit_score_category
-- ----------------------------------------------------------------------------
SELECT CONCAT(
'INSERT INTO `blade_credit_score_category` (',
'`id`, `tenant_id`, `quantification_id`, `category_code`, `category_name`, `sort`, ',
'`create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`, `is_deleted`',
') VALUES (',
id, ', ',
IFNULL(QUOTE(tenant_id), 'NULL'), ', ',
quantification_id, ', ',
IFNULL(QUOTE(category_code), 'NULL'), ', ',
IFNULL(QUOTE(category_name), 'NULL'), ', ',
IFNULL(CAST(sort AS CHAR), '0'), ', ',
IFNULL(CAST(create_user AS CHAR), 'NULL'), ', ',
IFNULL(CAST(create_dept AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(update_user AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(update_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(status AS CHAR), 'NULL'), ', ',
IFNULL(CAST(is_deleted AS CHAR), '0'),
');'
) AS sql_text
FROM blade_credit_score_category
WHERE quantification_id = @quantification_id
AND is_deleted = 0
ORDER BY sort, id;
-- ----------------------------------------------------------------------------
-- 4. 评分项目 blade_credit_score_item
-- ----------------------------------------------------------------------------
SELECT CONCAT(
'INSERT INTO `blade_credit_score_item` (',
'`id`, `tenant_id`, `quantification_id`, `category_id`, `category_code`, `item_name`, ',
'`option_type`, `base_value`, `score`, `score_description`, `option_description`, `sort`, ',
'`create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`, `is_deleted`',
') VALUES (',
id, ', ',
IFNULL(QUOTE(tenant_id), 'NULL'), ', ',
quantification_id, ', ',
category_id, ', ',
IFNULL(QUOTE(category_code), 'NULL'), ', ',
IFNULL(QUOTE(item_name), 'NULL'), ', ',
IFNULL(QUOTE(option_type), 'NULL'), ', ',
IFNULL(CAST(base_value AS CHAR), 'NULL'), ', ',
IFNULL(CAST(score AS CHAR), '0.00'), ', ',
IFNULL(QUOTE(score_description), 'NULL'), ', ',
IFNULL(QUOTE(option_description), 'NULL'), ', ',
IFNULL(CAST(sort AS CHAR), '0'), ', ',
IFNULL(CAST(create_user AS CHAR), 'NULL'), ', ',
IFNULL(CAST(create_dept AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(update_user AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(update_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(status AS CHAR), 'NULL'), ', ',
IFNULL(CAST(is_deleted AS CHAR), '0'),
');'
) AS sql_text
FROM blade_credit_score_item
WHERE quantification_id = @quantification_id
AND is_deleted = 0
ORDER BY sort, id;
-- ----------------------------------------------------------------------------
-- 5. 评分项目选项 blade_credit_score_item_option
-- ----------------------------------------------------------------------------
SELECT CONCAT(
'INSERT INTO `blade_credit_score_item_option` (',
'`id`, `tenant_id`, `quantification_id`, `item_id`, `option_name`, ',
'`change_type`, `change_value`, `change_unit`, `score_type`, `score`, `sort`, ',
'`create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`, `is_deleted`',
') VALUES (',
id, ', ',
IFNULL(QUOTE(tenant_id), 'NULL'), ', ',
quantification_id, ', ',
item_id, ', ',
IFNULL(QUOTE(option_name), 'NULL'), ', ',
IFNULL(QUOTE(change_type), 'NULL'), ', ',
IFNULL(CAST(change_value AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(change_unit), 'NULL'), ', ',
IFNULL(QUOTE(score_type), 'NULL'), ', ',
IFNULL(CAST(score AS CHAR), '0.00'), ', ',
IFNULL(CAST(sort AS CHAR), '0'), ', ',
IFNULL(CAST(create_user AS CHAR), 'NULL'), ', ',
IFNULL(CAST(create_dept AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(update_user AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(update_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(status AS CHAR), 'NULL'), ', ',
IFNULL(CAST(is_deleted AS CHAR), '0'),
');'
) AS sql_text
FROM blade_credit_score_item_option
WHERE quantification_id = @quantification_id
AND is_deleted = 0
ORDER BY sort, id;
-- ----------------------------------------------------------------------------
-- 6. 信用等级评估标准 blade_credit_rating_standard
-- ----------------------------------------------------------------------------
SELECT CONCAT(
'INSERT INTO `blade_credit_rating_standard` (',
'`id`, `tenant_id`, `quantification_id`, `credit_level`, ',
'`score_rate_lower`, `score_rate_upper`, `score_rate_increase`, ',
'`credit_limit_lower`, `credit_limit_upper`, `credit_limit_increase`, ',
'`standard_description`, `sort`, ',
'`create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`, `is_deleted`',
') VALUES (',
id, ', ',
IFNULL(QUOTE(tenant_id), 'NULL'), ', ',
quantification_id, ', ',
IFNULL(QUOTE(credit_level), 'NULL'), ', ',
IFNULL(CAST(score_rate_lower AS CHAR), 'NULL'), ', ',
IFNULL(CAST(score_rate_upper AS CHAR), 'NULL'), ', ',
IFNULL(CAST(score_rate_increase AS CHAR), '1'), ', ',
IFNULL(CAST(credit_limit_lower AS CHAR), '0.00'), ', ',
IFNULL(CAST(credit_limit_upper AS CHAR), '0.00'), ', ',
IFNULL(CAST(credit_limit_increase AS CHAR), '0.00'), ', ',
IFNULL(QUOTE(standard_description), 'NULL'), ', ',
IFNULL(CAST(sort AS CHAR), '0'), ', ',
IFNULL(CAST(create_user AS CHAR), 'NULL'), ', ',
IFNULL(CAST(create_dept AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(update_user AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(update_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(status AS CHAR), 'NULL'), ', ',
IFNULL(CAST(is_deleted AS CHAR), '0'),
');'
) AS sql_text
FROM blade_credit_rating_standard
WHERE quantification_id = @quantification_id
AND is_deleted = 0
ORDER BY sort, id;
-- ----------------------------------------------------------------------------
-- 7. 汇总:一次性输出全部 SQL(便于整段复制;MySQL 客户端需允许 GROUP_CONCAT 长度)
-- ----------------------------------------------------------------------------
SET SESSION group_concat_max_len = 1024 * 1024 * 16;
SELECT CONCAT_WS(
'\n',
-- 清理
CONCAT(
'-- ========== 目标库清理(quantification_id=', @quantification_id, ') ==========\n',
'DELETE FROM `blade_credit_score_item_option` WHERE `quantification_id` = ', @quantification_id, ';\n',
'DELETE FROM `blade_credit_score_item` WHERE `quantification_id` = ', @quantification_id, ';\n',
'DELETE FROM `blade_credit_score_category` WHERE `quantification_id` = ', @quantification_id, ';\n',
'DELETE FROM `blade_credit_rating_standard` WHERE `quantification_id` = ', @quantification_id, ';\n',
'DELETE FROM `blade_credit_score_quantification` WHERE `id` = ', @quantification_id, ';'
),
'',
'-- ========== 主表 ==========',
(SELECT GROUP_CONCAT(
CONCAT(
'INSERT INTO `blade_credit_score_quantification` (',
'`id`, `tenant_id`, `name`, `remark`, `standard_description`, `max_minus_score`, ',
'`create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`, `is_deleted`',
') VALUES (',
id, ', ',
IFNULL(QUOTE(tenant_id), 'NULL'), ', ',
IFNULL(QUOTE(name), 'NULL'), ', ',
IFNULL(QUOTE(remark), 'NULL'), ', ',
IFNULL(QUOTE(standard_description), 'NULL'), ', ',
IFNULL(CAST(max_minus_score AS CHAR), 'NULL'), ', ',
IFNULL(CAST(create_user AS CHAR), 'NULL'), ', ',
IFNULL(CAST(create_dept AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(update_user AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(update_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(status AS CHAR), 'NULL'), ', ',
IFNULL(CAST(is_deleted AS CHAR), '0'),
');'
)
SEPARATOR '\n'
)
FROM blade_credit_score_quantification
WHERE id = @quantification_id AND is_deleted = 0),
'',
'-- ========== 分类 ==========',
IFNULL((SELECT GROUP_CONCAT(
CONCAT(
'INSERT INTO `blade_credit_score_category` (',
'`id`, `tenant_id`, `quantification_id`, `category_code`, `category_name`, `sort`, ',
'`create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`, `is_deleted`',
') VALUES (',
id, ', ',
IFNULL(QUOTE(tenant_id), 'NULL'), ', ',
quantification_id, ', ',
IFNULL(QUOTE(category_code), 'NULL'), ', ',
IFNULL(QUOTE(category_name), 'NULL'), ', ',
IFNULL(CAST(sort AS CHAR), '0'), ', ',
IFNULL(CAST(create_user AS CHAR), 'NULL'), ', ',
IFNULL(CAST(create_dept AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(update_user AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(update_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(status AS CHAR), 'NULL'), ', ',
IFNULL(CAST(is_deleted AS CHAR), '0'),
');'
)
ORDER BY sort, id
SEPARATOR '\n'
)
FROM blade_credit_score_category
WHERE quantification_id = @quantification_id AND is_deleted = 0), ''),
'',
'-- ========== 评分项目 ==========',
IFNULL((SELECT GROUP_CONCAT(
CONCAT(
'INSERT INTO `blade_credit_score_item` (',
'`id`, `tenant_id`, `quantification_id`, `category_id`, `category_code`, `item_name`, ',
'`option_type`, `base_value`, `score`, `score_description`, `option_description`, `sort`, ',
'`create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`, `is_deleted`',
') VALUES (',
id, ', ',
IFNULL(QUOTE(tenant_id), 'NULL'), ', ',
quantification_id, ', ',
category_id, ', ',
IFNULL(QUOTE(category_code), 'NULL'), ', ',
IFNULL(QUOTE(item_name), 'NULL'), ', ',
IFNULL(QUOTE(option_type), 'NULL'), ', ',
IFNULL(CAST(base_value AS CHAR), 'NULL'), ', ',
IFNULL(CAST(score AS CHAR), '0.00'), ', ',
IFNULL(QUOTE(score_description), 'NULL'), ', ',
IFNULL(QUOTE(option_description), 'NULL'), ', ',
IFNULL(CAST(sort AS CHAR), '0'), ', ',
IFNULL(CAST(create_user AS CHAR), 'NULL'), ', ',
IFNULL(CAST(create_dept AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(update_user AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(update_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(status AS CHAR), 'NULL'), ', ',
IFNULL(CAST(is_deleted AS CHAR), '0'),
');'
)
ORDER BY sort, id
SEPARATOR '\n'
)
FROM blade_credit_score_item
WHERE quantification_id = @quantification_id AND is_deleted = 0), ''),
'',
'-- ========== 评分项目选项 ==========',
IFNULL((SELECT GROUP_CONCAT(
CONCAT(
'INSERT INTO `blade_credit_score_item_option` (',
'`id`, `tenant_id`, `quantification_id`, `item_id`, `option_name`, ',
'`change_type`, `change_value`, `change_unit`, `score_type`, `score`, `sort`, ',
'`create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`, `is_deleted`',
') VALUES (',
id, ', ',
IFNULL(QUOTE(tenant_id), 'NULL'), ', ',
quantification_id, ', ',
item_id, ', ',
IFNULL(QUOTE(option_name), 'NULL'), ', ',
IFNULL(QUOTE(change_type), 'NULL'), ', ',
IFNULL(CAST(change_value AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(change_unit), 'NULL'), ', ',
IFNULL(QUOTE(score_type), 'NULL'), ', ',
IFNULL(CAST(score AS CHAR), '0.00'), ', ',
IFNULL(CAST(sort AS CHAR), '0'), ', ',
IFNULL(CAST(create_user AS CHAR), 'NULL'), ', ',
IFNULL(CAST(create_dept AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(update_user AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(update_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(status AS CHAR), 'NULL'), ', ',
IFNULL(CAST(is_deleted AS CHAR), '0'),
');'
)
ORDER BY sort, id
SEPARATOR '\n'
)
FROM blade_credit_score_item_option
WHERE quantification_id = @quantification_id AND is_deleted = 0), ''),
'',
'-- ========== 信用等级评估标准 ==========',
IFNULL((SELECT GROUP_CONCAT(
CONCAT(
'INSERT INTO `blade_credit_rating_standard` (',
'`id`, `tenant_id`, `quantification_id`, `credit_level`, ',
'`score_rate_lower`, `score_rate_upper`, `score_rate_increase`, ',
'`credit_limit_lower`, `credit_limit_upper`, `credit_limit_increase`, ',
'`standard_description`, `sort`, ',
'`create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`, `is_deleted`',
') VALUES (',
id, ', ',
IFNULL(QUOTE(tenant_id), 'NULL'), ', ',
quantification_id, ', ',
IFNULL(QUOTE(credit_level), 'NULL'), ', ',
IFNULL(CAST(score_rate_lower AS CHAR), 'NULL'), ', ',
IFNULL(CAST(score_rate_upper AS CHAR), 'NULL'), ', ',
IFNULL(CAST(score_rate_increase AS CHAR), '1'), ', ',
IFNULL(CAST(credit_limit_lower AS CHAR), '0.00'), ', ',
IFNULL(CAST(credit_limit_upper AS CHAR), '0.00'), ', ',
IFNULL(CAST(credit_limit_increase AS CHAR), '0.00'), ', ',
IFNULL(QUOTE(standard_description), 'NULL'), ', ',
IFNULL(CAST(sort AS CHAR), '0'), ', ',
IFNULL(CAST(create_user AS CHAR), 'NULL'), ', ',
IFNULL(CAST(create_dept AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(update_user AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(update_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(status AS CHAR), 'NULL'), ', ',
IFNULL(CAST(is_deleted AS CHAR), '0'),
');'
)
ORDER BY sort, id
SEPARATOR '\n'
)
FROM blade_credit_rating_standard
WHERE quantification_id = @quantification_id AND is_deleted = 0), '')
) AS full_export_sql;
@@ -0,0 +1,5 @@
-- 评分量化表:减分项目最高扣分限制
-- 客商自评/复评时,所有减分项合计扣分不得超过该值;超出则按该上限计分
ALTER TABLE `blade_credit_score_quantification`
ADD COLUMN `max_minus_score` decimal(10,2) NULL DEFAULT NULL COMMENT '减分项目最高扣分限制' AFTER `standard_description`;
+5 -2
View File
@@ -18,21 +18,24 @@ CREATE TABLE `blade_customer_archive` (
`registered_region_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '注册地址行政区划', `registered_region_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '注册地址行政区划',
`registered_detail_address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '注册地址详细地址', `registered_detail_address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '注册地址详细地址',
`legal_person` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '法人/负责人', `legal_person` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '法人/负责人',
`contact_phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '联系电话', `contact_phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '联系电话',
`dept_id` bigint NULL DEFAULT NULL COMMENT '所属组织ID', `dept_id` bigint NULL DEFAULT NULL COMMENT '所属组织ID',
`dept_ids` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '所属组织ID集合', `dept_ids` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '所属组织ID集合',
`dept_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '所属组织', `dept_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '所属组织',
`invoice_tax_rate` decimal(6,2) NULL DEFAULT NULL COMMENT '开票税点', `invoice_tax_rate` decimal(6,2) NULL DEFAULT NULL COMMENT '开票税点',
`business_scope` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '经营范围', `business_scope` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '经营范围',
`network_freight_platform` tinyint NULL DEFAULT NULL COMMENT '网络货运平台:0否,1是', `network_freight_platform` tinyint NULL DEFAULT NULL COMMENT '网络货运平台:0否,1是',
`business_term_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '营业期限类型', `business_term_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '营业期限类型',
`business_start_date` date NULL DEFAULT NULL COMMENT '营业期限开始日',
`business_end_date` date NULL DEFAULT NULL COMMENT '营业期限截止日', `business_end_date` date NULL DEFAULT NULL COMMENT '营业期限截止日',
`registered_capital` decimal(18,2) NULL DEFAULT NULL COMMENT '注册资金(万元)', `registered_capital` decimal(18,2) NULL DEFAULT NULL COMMENT '注册资金(万元)',
`paid_in_capital` decimal(18,2) NULL DEFAULT NULL COMMENT '实缴资本(万元)',
`principal` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客商负责人', `principal` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客商负责人',
`mnemonic_code` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '助记码', `mnemonic_code` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '助记码',
`customer_level` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客户等级', `customer_level` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客户等级',
`max_credit_limit` decimal(18,2) NULL DEFAULT NULL COMMENT '最大资金使用额度(万元)', `max_credit_limit` decimal(18,2) NULL DEFAULT NULL COMMENT '最大资金使用额度(万元)',
`apply_credit_limit` decimal(18,2) NULL DEFAULT NULL COMMENT '申请总资金使用额度(万元)', `apply_credit_limit` decimal(18,2) NULL DEFAULT NULL COMMENT '申请总资金使用额度(万元)',
`used_credit_limit` decimal(18,2) NULL DEFAULT NULL COMMENT '已使用额度(万元)',
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
`qualification_attachments` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '客商材料JSON', `qualification_attachments` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '客商材料JSON',
`access_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'temporary' COMMENT '准入类型:temporary临时,formal正式', `access_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'temporary' COMMENT '准入类型:temporary临时,formal正式',
@@ -0,0 +1,7 @@
-- 客商档案:实缴资本、营业期限开始日、已使用额度;经营范围加长;联系电话改为可空(改由联系人维护)
ALTER TABLE `blade_customer_archive`
ADD COLUMN `business_start_date` date NULL DEFAULT NULL COMMENT '营业期限开始日' AFTER `business_term_type`,
ADD COLUMN `paid_in_capital` decimal(18,2) NULL DEFAULT NULL COMMENT '实缴资本(万元)' AFTER `registered_capital`,
ADD COLUMN `used_credit_limit` decimal(18,2) NULL DEFAULT NULL COMMENT '已使用额度(万元)' AFTER `apply_credit_limit`,
MODIFY COLUMN `business_scope` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '经营范围',
MODIFY COLUMN `contact_phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '联系电话';
@@ -0,0 +1,5 @@
-- 机构表:新增「是否业务部门」字段,默认否
-- 全量 OA 同步时须保留本地已配置值,不得覆盖
ALTER TABLE `blade_dept`
ADD COLUMN `is_business_dept` tinyint NOT NULL DEFAULT 0 COMMENT '是否业务部门:0否,1是' AFTER `is_platform_company`;
@@ -9,6 +9,7 @@ CREATE TABLE IF NOT EXISTS `blade_formal_settlement` (
`dept_id` bigint(20) DEFAULT NULL, `dept_name` varchar(100) DEFAULT NULL, `contract_id` bigint(20) NOT NULL, `dept_id` bigint(20) DEFAULT NULL, `dept_name` varchar(100) DEFAULT NULL, `contract_id` bigint(20) NOT NULL,
`contract_no` varchar(100) DEFAULT NULL, `contract_name` varchar(100) NOT NULL, `payer_name` varchar(200) DEFAULT NULL, `contract_no` varchar(100) DEFAULT NULL, `contract_name` varchar(100) NOT NULL, `payer_name` varchar(200) DEFAULT NULL,
`payee_name` varchar(200) DEFAULT NULL, `currency` varchar(20) NOT NULL DEFAULT 'RMB', `settlement_amount` decimal(18,2) NOT NULL DEFAULT '0.00', `payee_name` varchar(200) DEFAULT NULL, `currency` varchar(20) NOT NULL DEFAULT 'RMB', `settlement_amount` decimal(18,2) NOT NULL DEFAULT '0.00',
`business_model` varchar(64) DEFAULT NULL COMMENT '业务模式', `transport_type` varchar(64) DEFAULT NULL COMMENT '运输方式',
`local_currency` varchar(20) NOT NULL DEFAULT 'RMB', `local_settlement_amount` decimal(18,2) DEFAULT NULL, `local_currency` varchar(20) NOT NULL DEFAULT 'RMB', `local_settlement_amount` decimal(18,2) DEFAULT NULL,
`applied_payment_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '申请付款金额含预付', `applied_payment_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '申请付款金额含预付',
`paid_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '已收已付合计含预付', `paid_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '已收已付合计含预付',
@@ -18,6 +18,8 @@ CREATE TABLE IF NOT EXISTS `blade_mp_user` (
`openid` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信小程序 openid', `openid` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信小程序 openid',
`identities` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '小程序身份,逗号分隔:driver,admin', `identities` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '小程序身份,逗号分隔:driver,admin',
`dept_id` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '部门id', `dept_id` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '部门id',
`data_scope_range` tinyint NOT NULL DEFAULT 1 COMMENT '数据权限范围:1所属组织,2全部组织,3自定义',
`data_level_range` tinyint NOT NULL DEFAULT 1 COMMENT '数据层级范围:1包含下级,2仅本级',
`post_id` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '岗位id', `post_id` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '岗位id',
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
`create_user` bigint NULL DEFAULT NULL COMMENT '创建人', `create_user` bigint NULL DEFAULT NULL COMMENT '创建人',
@@ -0,0 +1,12 @@
-- 小程序用户:数据权限范围、数据层级范围、自定义组织数据权限
ALTER TABLE `blade_mp_user`
ADD COLUMN `data_scope_range` tinyint NOT NULL DEFAULT 1 COMMENT '数据权限范围:1所属组织,2全部组织,3自定义' AFTER `dept_id`,
ADD COLUMN `data_level_range` tinyint NOT NULL DEFAULT 1 COMMENT '数据层级范围:1包含下级,2仅本级' AFTER `data_scope_range`;
CREATE TABLE IF NOT EXISTS `blade_mp_user_data_scope` (
`id` bigint NOT NULL COMMENT '主键',
`user_id` bigint NOT NULL COMMENT '小程序用户主键',
`dept_id` bigint NOT NULL COMMENT '组织主键',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_blade_mp_user_data_scope_user_dept` (`user_id`, `dept_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='小程序用户组织数据权限';
@@ -24,6 +24,8 @@ CREATE TABLE IF NOT EXISTS `blade_pre_settlement` (
`payee_name` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '收款方', `payee_name` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '收款方',
`currency` varchar(20) COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'RMB' COMMENT '结算币种', `currency` varchar(20) COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'RMB' COMMENT '结算币种',
`settlement_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '结算金额', `settlement_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '结算金额',
`business_model` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '业务模式',
`transport_type` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '运输方式',
`local_currency` varchar(20) COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'RMB' COMMENT '本位币', `local_currency` varchar(20) COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'RMB' COMMENT '本位币',
`local_settlement_amount` decimal(18,2) DEFAULT NULL COMMENT '本位币合计', `local_settlement_amount` decimal(18,2) DEFAULT NULL COMMENT '本位币合计',
`exchange_rate_date` date DEFAULT NULL COMMENT '汇率日期', `exchange_rate_date` date DEFAULT NULL COMMENT '汇率日期',
@@ -0,0 +1,8 @@
-- 预结算单、正式结算单:业务模式、运输方式(主表头字段)
ALTER TABLE `blade_pre_settlement`
ADD COLUMN `business_model` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '业务模式' AFTER `settlement_amount`,
ADD COLUMN `transport_type` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '运输方式' AFTER `business_model`;
ALTER TABLE `blade_formal_settlement`
ADD COLUMN `business_model` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '业务模式' AFTER `settlement_amount`,
ADD COLUMN `transport_type` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '运输方式' AFTER `business_model`;
@@ -7,8 +7,12 @@
-- 3. 只统计各功能列表对应的主表。随主单保存的明细、联系人、附件、变更流水不重复计数。 -- 3. 只统计各功能列表对应的主表。随主单保存的明细、联系人、附件、变更流水不重复计数。
-- 4. 含已逻辑删除的数据,因为这些记录确实被创建过。 -- 4. 含已逻辑删除的数据,因为这些记录确实被创建过。
-- 5. 客商档案菜单挂在「车/船务管理」下,这里按统计口径单独计入「客商管理」。 -- 5. 客商档案菜单挂在「车/船务管理」下,这里按统计口径单独计入「客商管理」。
-- 6. 未纳入:结算管理、首付款管理、在途管理,以及系统管理下的开票项目。 -- 6. 未纳入:在途管理,以及系统管理下的开票项目。
-- 7. 导入运单产生的运单计入「业务模块-运单管理」,不另计导入批次。 -- 7. 导入运单产生的运单计入「业务模块-运单管理」,不另计导入批次。
-- 导入结算单产生的正式结算单计入「结算管理」的付款结算单或收款结算单,不另计导入批次。
-- 8. 付款结算单、收款结算单共用 blade_formal_settlement,按 settlement_type 区分(payable/receivable)。
-- 应收、应付共用 blade_receivable_payable_detail,同样按 settlement_type 区分。
-- 9. 菜单名称为「收付款管理」,统计模块名与菜单一致。
SET NAMES utf8mb4; SET NAMES utf8mb4;
@@ -396,7 +400,124 @@ SELECT t.create_user, 8, '运力模块', 4, '车辆调度', 'blade_transport_veh
FROM blade_transport_vehicle_dispatch t FROM blade_transport_vehicle_dispatch t
INNER JOIN tmp_stat_user u ON u.id = t.create_user INNER JOIN tmp_stat_user u ON u.id = t.create_user
WHERE t.create_time >= @stat_start AND t.create_time < @stat_end_exclusive WHERE t.create_time >= @stat_start AND t.create_time < @stat_end_exclusive
GROUP BY t.create_user; GROUP BY t.create_user
;
INSERT INTO tmp_stat_detail (create_user, module_sort, module_name, biz_sort, biz_name, table_name, cnt)
SELECT t.create_user, 9, '结算管理', 1, '应收', 'blade_receivable_payable_detail', COUNT(*)
FROM blade_receivable_payable_detail t
INNER JOIN tmp_stat_user u ON u.id = t.create_user
WHERE t.create_time >= @stat_start AND t.create_time < @stat_end_exclusive
AND t.settlement_type = 'receivable'
GROUP BY t.create_user
;
INSERT INTO tmp_stat_detail (create_user, module_sort, module_name, biz_sort, biz_name, table_name, cnt)
SELECT t.create_user, 9, '结算管理', 2, '应付', 'blade_receivable_payable_detail', COUNT(*)
FROM blade_receivable_payable_detail t
INNER JOIN tmp_stat_user u ON u.id = t.create_user
WHERE t.create_time >= @stat_start AND t.create_time < @stat_end_exclusive
AND t.settlement_type = 'payable'
GROUP BY t.create_user
;
INSERT INTO tmp_stat_detail (create_user, module_sort, module_name, biz_sort, biz_name, table_name, cnt)
SELECT t.create_user, 9, '结算管理', 3, '预付结算单', 'blade_pre_settlement', COUNT(*)
FROM blade_pre_settlement t
INNER JOIN tmp_stat_user u ON u.id = t.create_user
WHERE t.create_time >= @stat_start AND t.create_time < @stat_end_exclusive
GROUP BY t.create_user
;
INSERT INTO tmp_stat_detail (create_user, module_sort, module_name, biz_sort, biz_name, table_name, cnt)
SELECT t.create_user, 9, '结算管理', 4, '付款结算单', 'blade_formal_settlement', COUNT(*)
FROM blade_formal_settlement t
INNER JOIN tmp_stat_user u ON u.id = t.create_user
WHERE t.create_time >= @stat_start AND t.create_time < @stat_end_exclusive
AND t.settlement_type = 'payable'
GROUP BY t.create_user
;
INSERT INTO tmp_stat_detail (create_user, module_sort, module_name, biz_sort, biz_name, table_name, cnt)
SELECT t.create_user, 9, '结算管理', 5, '收款结算单', 'blade_formal_settlement', COUNT(*)
FROM blade_formal_settlement t
INNER JOIN tmp_stat_user u ON u.id = t.create_user
WHERE t.create_time >= @stat_start AND t.create_time < @stat_end_exclusive
AND t.settlement_type = 'receivable'
GROUP BY t.create_user
;
INSERT INTO tmp_stat_detail (create_user, module_sort, module_name, biz_sort, biz_name, table_name, cnt)
SELECT t.create_user, 9, '结算管理', 6, '运输对账', 'blade_transport_reconciliation', COUNT(*)
FROM blade_transport_reconciliation t
INNER JOIN tmp_stat_user u ON u.id = t.create_user
WHERE t.create_time >= @stat_start AND t.create_time < @stat_end_exclusive
GROUP BY t.create_user
;
INSERT INTO tmp_stat_detail (create_user, module_sort, module_name, biz_sort, biz_name, table_name, cnt)
SELECT t.create_user, 9, '结算管理', 7, '结算调整单', 'blade_settlement_adjustment', COUNT(*)
FROM blade_settlement_adjustment t
INNER JOIN tmp_stat_user u ON u.id = t.create_user
WHERE t.create_time >= @stat_start AND t.create_time < @stat_end_exclusive
GROUP BY t.create_user
;
INSERT INTO tmp_stat_detail (create_user, module_sort, module_name, biz_sort, biz_name, table_name, cnt)
SELECT t.create_user, 10, '收付款管理', 1, '付款管理', 'blade_payment_application', COUNT(*)
FROM blade_payment_application t
INNER JOIN tmp_stat_user u ON u.id = t.create_user
WHERE t.create_time >= @stat_start AND t.create_time < @stat_end_exclusive
GROUP BY t.create_user
;
INSERT INTO tmp_stat_detail (create_user, module_sort, module_name, biz_sort, biz_name, table_name, cnt)
SELECT t.create_user, 10, '收付款管理', 2, '开票管理', 'blade_invoice_application', COUNT(*)
FROM blade_invoice_application t
INNER JOIN tmp_stat_user u ON u.id = t.create_user
WHERE t.create_time >= @stat_start AND t.create_time < @stat_end_exclusive
GROUP BY t.create_user
;
INSERT INTO tmp_stat_detail (create_user, module_sort, module_name, biz_sort, biz_name, table_name, cnt)
SELECT t.create_user, 10, '收付款管理', 3, '收票管理', 'blade_invoice_receipt', COUNT(*)
FROM blade_invoice_receipt t
INNER JOIN tmp_stat_user u ON u.id = t.create_user
WHERE t.create_time >= @stat_start AND t.create_time < @stat_end_exclusive
GROUP BY t.create_user
;
INSERT INTO tmp_stat_detail (create_user, module_sort, module_name, biz_sort, biz_name, table_name, cnt)
SELECT t.create_user, 10, '收付款管理', 4, '收款流水', 'blade_kingdee_receipt_flow', COUNT(*)
FROM blade_kingdee_receipt_flow t
INNER JOIN tmp_stat_user u ON u.id = t.create_user
WHERE t.create_time >= @stat_start AND t.create_time < @stat_end_exclusive
GROUP BY t.create_user
;
INSERT INTO tmp_stat_detail (create_user, module_sort, module_name, biz_sort, biz_name, table_name, cnt)
SELECT t.create_user, 10, '收付款管理', 5, '认领记录', 'blade_receipt_claim', COUNT(*)
FROM blade_receipt_claim t
INNER JOIN tmp_stat_user u ON u.id = t.create_user
WHERE t.create_time >= @stat_start AND t.create_time < @stat_end_exclusive
GROUP BY t.create_user
;
INSERT INTO tmp_stat_detail (create_user, module_sort, module_name, biz_sort, biz_name, table_name, cnt)
SELECT t.create_user, 10, '收付款管理', 6, '汇票台账', 'blade_bill_ledger', COUNT(*)
FROM blade_bill_ledger t
INNER JOIN tmp_stat_user u ON u.id = t.create_user
WHERE t.create_time >= @stat_start AND t.create_time < @stat_end_exclusive
GROUP BY t.create_user
;
INSERT INTO tmp_stat_detail (create_user, module_sort, module_name, biz_sort, biz_name, table_name, cnt)
SELECT t.create_user, 10, '收付款管理', 7, '汇票付款', 'blade_bill_payment', COUNT(*)
FROM blade_bill_payment t
INNER JOIN tmp_stat_user u ON u.id = t.create_user
WHERE t.create_time >= @stat_start AND t.create_time < @stat_end_exclusive
GROUP BY t.create_user
;
-- 结果 1:账号匹配情况。用户 ID 为空表示库中没有该账号。 -- 结果 1:账号匹配情况。用户 ID 为空表示库中没有该账号。
SELECT SELECT
@@ -422,6 +543,8 @@ SELECT
IFNULL(SUM(CASE WHEN d.module_name = '业务模块' THEN d.cnt ELSE 0 END), 0) AS `业务模块`, IFNULL(SUM(CASE WHEN d.module_name = '业务模块' THEN d.cnt ELSE 0 END), 0) AS `业务模块`,
IFNULL(SUM(CASE WHEN d.module_name = '车/船务模块' THEN d.cnt ELSE 0 END), 0) AS `车/船务模块`, IFNULL(SUM(CASE WHEN d.module_name = '车/船务模块' THEN d.cnt ELSE 0 END), 0) AS `车/船务模块`,
IFNULL(SUM(CASE WHEN d.module_name = '运力模块' THEN d.cnt ELSE 0 END), 0) AS `运力模块`, IFNULL(SUM(CASE WHEN d.module_name = '运力模块' THEN d.cnt ELSE 0 END), 0) AS `运力模块`,
IFNULL(SUM(CASE WHEN d.module_name = '结算管理' THEN d.cnt ELSE 0 END), 0) AS `结算管理`,
IFNULL(SUM(CASE WHEN d.module_name = '收付款管理' THEN d.cnt ELSE 0 END), 0) AS `收付款管理`,
IFNULL(SUM(d.cnt), 0) AS `合计` IFNULL(SUM(d.cnt), 0) AS `合计`
FROM tmp_stat_account a FROM tmp_stat_account a
LEFT JOIN tmp_stat_user u ON u.account = a.account LEFT JOIN tmp_stat_user u ON u.account = a.account