diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/Dept.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/Dept.java index e5958c76..0446ece9 100644 --- a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/Dept.java +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/Dept.java @@ -207,6 +207,12 @@ public class Dept extends TenantEntity { @Schema(description = "是否平台公司:0否,1是") private Integer isPlatformCompany; + /** + * 是否业务部门:0否,1是 + */ + @Schema(description = "是否业务部门:0否,1是") + private Integer isBusinessDept; + /** * OA主键,仅同步记录,不对外展示 */ diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java index d001adb4..706c321d 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java @@ -26,6 +26,10 @@ public class FormalSettlementSaveRequest implements Serializable { private List sourcePreSettlementIds; private List sourceDetailIds; private List detailAdjustments; + /** 业务模式(业务字典 business_model) */ + private String businessModel; + /** 运输方式(业务字典 transport_type) */ + private String transportType; private LocalDate exchangeRateDate; private BigDecimal exchangeRate; private String attachmentsJson; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementSaveRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementSaveRequest.java index b2c670b7..d30e8955 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementSaveRequest.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementSaveRequest.java @@ -55,6 +55,12 @@ public class PreSettlementSaveRequest implements Serializable { @Schema(description = "结算类型:receivable/payable") private String settlementType; + @Schema(description = "业务模式(业务字典 business_model)") + private String businessModel; + + @Schema(description = "运输方式(业务字典 transport_type)") + private String transportType; + @Schema(description = "汇率日期") private LocalDate exchangeRateDate; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractManage.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractManage.java index 04f6a1b4..1d3ab279 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractManage.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractManage.java @@ -199,6 +199,10 @@ public class ContractManage extends TenantEntity { @TableField(updateStrategy = FieldStrategy.ALWAYS) private String changeContent; + @TableField(exist = false) + @Schema(description = "变更类型") + private String changeType; + @Schema(description = "变更原因") @TableField(updateStrategy = FieldStrategy.ALWAYS) private String changeReason; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CreditScoreQuantification.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CreditScoreQuantification.java index 75b05be0..7caf0be1 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CreditScoreQuantification.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CreditScoreQuantification.java @@ -32,6 +32,7 @@ import lombok.EqualsAndHashCode; import org.springblade.core.tenant.mp.TenantEntity; import java.io.Serial; +import java.math.BigDecimal; /** * 评分量化表实体类 @@ -62,5 +63,10 @@ public class CreditScoreQuantification extends TenantEntity { */ @Schema(description = "标准说明") private String standardDescription; + /** + * 减分项目最高扣分限制:客商评分时所有减分项合计不得超过该值,超出按该上限计分 + */ + @Schema(description = "减分项目最高扣分限制") + private BigDecimal maxMinusScore; } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java index 5ea8bec1..242d0f65 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java @@ -115,6 +115,10 @@ public class CustomerArchive extends TenantEntity { @Schema(description = "营业期限类型") private String businessTermType; + @Schema(description = "营业期限开始日") + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private LocalDate businessStartDate; + @Schema(description = "营业期限截止日") @TableField(updateStrategy = FieldStrategy.ALWAYS) private LocalDate businessEndDate; @@ -123,6 +127,10 @@ public class CustomerArchive extends TenantEntity { @TableField(updateStrategy = FieldStrategy.ALWAYS) private BigDecimal registeredCapital; + @Schema(description = "实缴资本(万元)") + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal paidInCapital; + @Schema(description = "客商负责人") private String principal; @@ -140,6 +148,10 @@ public class CustomerArchive extends TenantEntity { @TableField(updateStrategy = FieldStrategy.ALWAYS) private BigDecimal applyCreditLimit; + @Schema(description = "已使用额度(万元),新建可录历史值,后续由收付款业务累加/减更新") + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal usedCreditLimit; + @Schema(description = "备注") private String remark; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlement.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlement.java index cc4ddd1a..20c42775 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlement.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlement.java @@ -88,6 +88,14 @@ public class FormalSettlement extends TenantEntity { * 结算金额(结算币种,含全部合计费用) */ private BigDecimal settlementAmount; + /** + * 业务模式(业务字典 business_model) + */ + private String businessModel; + /** + * 运输方式(业务字典 transport_type) + */ + private String transportType; /** * 本位币,默认 RMB */ diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlement.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlement.java index 7181170c..a333caf0 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlement.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlement.java @@ -92,6 +92,12 @@ public class PreSettlement extends TenantEntity { @Schema(description = "结算金额") private BigDecimal settlementAmount; + @Schema(description = "业务模式(业务字典 business_model)") + private String businessModel; + + @Schema(description = "运输方式(业务字典 transport_type)") + private String transportType; + @Schema(description = "本位币") private String localCurrency; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerCreditScoreVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerCreditScoreVO.java index 6f54a69f..6162ec5c 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerCreditScoreVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerCreditScoreVO.java @@ -29,6 +29,7 @@ import lombok.EqualsAndHashCode; import org.springblade.transport.pojo.entity.CustomerCreditScore; import java.io.Serial; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; @@ -57,4 +58,11 @@ public class CustomerCreditScoreVO extends CustomerCreditScore { @Schema(description = "评估标准") private List standards = new ArrayList<>(); + /** + * 来自评分量化表的减分项目最高扣分限制(非落库字段,供前端计分使用) + */ + @TableField(exist = false) + @Schema(description = "减分项目最高扣分限制") + private BigDecimal maxMinusScore; + } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java index 1c68aef6..b879bd0c 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java @@ -43,6 +43,8 @@ public class FormalSettlementVO extends FormalSettlement implements KingdeeUnloc @TableField(exist = false) private String ids; @TableField(exist = false) private String approvalStatusName; @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 String attachmentSyncStatus; @TableField(exist = false) private String attachmentSyncStatusName; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PreSettlementVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PreSettlementVO.java index 36955339..ac4f8a73 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PreSettlementVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PreSettlementVO.java @@ -85,6 +85,14 @@ public class PreSettlementVO extends PreSettlement { @Schema(description = "结算类型名称") private String settlementTypeName; + @TableField(exist = false) + @Schema(description = "业务模式名称") + private String businessModelName; + + @TableField(exist = false) + @Schema(description = "运输方式名称") + private String transportTypeName; + @TableField(exist = false) @Schema(description = "结算明细") private List details; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TripQueryVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TripQueryVO.java index 569e96ca..750dd6d0 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TripQueryVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TripQueryVO.java @@ -56,6 +56,9 @@ public class TripQueryVO implements Serializable { @Schema(description = "可见组织 ID 集合:非空时只出这些组织的运单行,空集合表示不按组织过滤") private List deptIds; + @Schema(description = "可见项目 ID 集合:非空时只出这些项目的运单行,空集合表示不按项目过滤(小程序调度端优先用此口径)") + private List projectIds; + @Schema(description = "状态:0待接单/1进行中/2已完成/3已取消,不传为全部(不含已取消)") private Integer status; diff --git a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IMpUserClient.java b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IMpUserClient.java index b381750b..b28625da 100644 --- a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IMpUserClient.java +++ b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IMpUserClient.java @@ -27,6 +27,7 @@ import org.springblade.core.tool.api.R; import org.springblade.system.pojo.dto.MpUserEnsureDTO; import org.springblade.system.pojo.entity.MpUser; import org.springblade.system.pojo.entity.MpUserInfo; +import org.springblade.system.pojo.vo.MpUserDataScopeVO; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -56,6 +57,7 @@ public interface IMpUserClient { String BIND_OPENID = API_PREFIX + "/bind-openid"; String SET_PASSWORD = API_PREFIX + "/set-password"; String ENSURE_USER = API_PREFIX + "/ensure-user"; + String RESOLVE_DATA_SCOPE = API_PREFIX + "/resolve-data-scope"; /** * 按主键查询 @@ -63,6 +65,12 @@ public interface IMpUserClient { @GetMapping(USER_BY_ID) R userById(@RequestParam("userId") Long userId); + /** + * 解析小程序用户可见组织(所属组织 + 数据权限范围 + 数据层级范围) + */ + @GetMapping(RESOLVE_DATA_SCOPE) + R resolveDataScope(@RequestParam("userId") Long userId); + /** * 按租户 + 手机号查询 */ diff --git a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/entity/MpUser.java b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/entity/MpUser.java index 6d613811..11896b43 100644 --- a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/entity/MpUser.java +++ b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/entity/MpUser.java @@ -25,6 +25,7 @@ */ package org.springblade.system.pojo.entity; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -35,6 +36,7 @@ import org.springblade.core.tool.sensitive.SensitiveType; import java.io.Serial; import java.util.Date; +import java.util.List; /** * 小程序用户实体(仅用于小程序登录,与 blade_user 分离) @@ -113,10 +115,26 @@ public class MpUser extends TenantEntity { @Schema(description = "小程序身份") private String identities; /** - * 部门id + * 部门id(多选,逗号分隔) */ @Schema(description = "部门id") 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 dataScopeDeptIds; /** * 岗位id */ diff --git a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/entity/MpUserDataScope.java b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/entity/MpUserDataScope.java new file mode 100644 index 00000000..1115694d --- /dev/null +++ b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/entity/MpUserDataScope.java @@ -0,0 +1,44 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 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; + +} diff --git a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/vo/MpUserDataScopeVO.java b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/vo/MpUserDataScopeVO.java new file mode 100644 index 00000000..6d9f38bf --- /dev/null +++ b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/vo/MpUserDataScopeVO.java @@ -0,0 +1,91 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * 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; + +/** + * 小程序用户数据权限解析结果 + *

+ * 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 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 deptIds) { + MpUserDataScopeVO scope = new MpUserDataScopeVO(); + scope.setAllDept(false); + scope.setDeptIds(deptIds == null ? new ArrayList<>() : new ArrayList<>(deptIds)); + return scope; + } + +} diff --git a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/vo/MpUserVO.java b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/vo/MpUserVO.java index c2ced763..f65620ee 100644 --- a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/vo/MpUserVO.java +++ b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/vo/MpUserVO.java @@ -42,6 +42,13 @@ public class MpUserVO extends MpUser { @Serial private static final long serialVersionUID = 1L; + /** + * 所属组织名称(逗号分隔) + */ + @TableField(exist = false) + @Schema(description = "所属组织名称") + private String deptName; + /** * 身份名称(逗号分隔中文) */ diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java index 47ef9a82..eafb7b30 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java @@ -260,6 +260,17 @@ public class DeptController extends BladeController { return R.data(deptService.listPlatformCompany()); } + /** + * 业务部门下拉(是否业务部门=是) + */ + @PreAuth(AuthConstant.PERMIT_ALL) + @GetMapping("/business-dept-select") + @ApiOperationSupport(order = 12) + @Operation(summary = "业务部门下拉", description = "返回是否业务部门=是的部门列表") + public R> businessDeptSelect() { + return R.data(deptService.listBusinessDept()); + } + /** * 获取部门的主管信息 */ diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/MpUserController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/MpUserController.java index b4c84f8e..435ba1d3 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/MpUserController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/MpUserController.java @@ -71,7 +71,8 @@ public class MpUserController extends BladeController { @ApiOperationSupport(order = 1) @Operation(summary = "详情", description = "传入id") public R 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) { return R.fail("小程序用户不存在"); } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/feign/MpUserClient.java b/blade-service/blade-system/src/main/java/org/springblade/system/feign/MpUserClient.java index fae7ebfa..57dbd07c 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/feign/MpUserClient.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/feign/MpUserClient.java @@ -29,10 +29,12 @@ import org.springblade.core.tool.api.R; import org.springblade.system.pojo.dto.MpUserEnsureDTO; import org.springblade.system.pojo.entity.MpUser; import org.springblade.system.pojo.entity.MpUserInfo; +import org.springblade.system.pojo.vo.MpUserDataScopeVO; import org.springblade.system.service.IMpUserService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** @@ -54,6 +56,12 @@ public class MpUserClient implements IMpUserClient { return R.data(mpUserService.getById(userId)); } + @Override + @GetMapping(RESOLVE_DATA_SCOPE) + public R resolveDataScope(@RequestParam("userId") Long userId) { + return R.data(mpUserService.resolveDataScope(userId)); + } + @Override @GetMapping(USER_BY_PHONE) public R userByPhone(String tenantId, String phone) { diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/DeptMapper.xml b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/DeptMapper.xml index bfbaf61f..1f38b0fd 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/DeptMapper.xml +++ b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/DeptMapper.xml @@ -20,6 +20,7 @@ + @@ -43,6 +44,7 @@ + diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MpUserDataScopeMapper.java b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MpUserDataScopeMapper.java new file mode 100644 index 00000000..80704440 --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MpUserDataScopeMapper.java @@ -0,0 +1,21 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 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 { +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/IDeptService.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/IDeptService.java index d9094bf0..39ccb769 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/IDeptService.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/IDeptService.java @@ -106,6 +106,13 @@ public interface IDeptService extends IService { */ List listPlatformCompany(); + /** + * 业务部门下拉(是否业务部门=是) + * + * @return 业务部门列表 + */ + List listBusinessDept(); + /** * 根据部门名称精确匹配获取部门ID集合 * diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/IMpUserService.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/IMpUserService.java index 6b20f61d..718041cb 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/IMpUserService.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/IMpUserService.java @@ -27,6 +27,7 @@ import org.springblade.core.mp.base.BaseService; import org.springblade.system.pojo.dto.MpUserEnsureDTO; import org.springblade.system.pojo.entity.MpUser; import org.springblade.system.pojo.entity.MpUserInfo; +import org.springblade.system.pojo.vo.MpUserDataScopeVO; import java.util.List; import java.util.Map; @@ -80,6 +81,18 @@ public interface IMpUserService extends BaseService { */ IPage selectMpUserPage(IPage page, MpUser query); + /** + * 详情(含自定义数据权限组织) + */ + MpUser detail(Long id); + + /** + * 解析小程序用户可见组织(所属组织 + 数据权限范围 + 数据层级范围) + *

+ * allDept=true 表示全部组织;否则 deptIds 为可见组织,空列表表示无权限。 + */ + MpUserDataScopeVO resolveDataScope(Long userId); + /** * 保存小程序用户 */ diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DeptServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DeptServiceImpl.java index dd24564f..551918ed 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DeptServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DeptServiceImpl.java @@ -337,6 +337,18 @@ public class DeptServiceImpl extends ServiceImpl implements ID return list(queryWrapper); } + @Override + public List listBusinessDept() { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() + .eq(Dept::getIsBusinessDept, 1) + .orderByAsc(Dept::getSort) + .orderByAsc(Dept::getId); + if (!AuthUtil.isAdministrator()) { + queryWrapper.eq(Dept::getTenantId, AuthUtil.getTenantId()); + } + return list(queryWrapper); + } + @Override public String getDeptIds(String tenantId, String deptNames) { List deptList = baseMapper.selectList(Wrappers.query().lambda().eq(Dept::getTenantId, tenantId).in(Dept::getDeptName, Func.toStrList(deptNames))); @@ -443,6 +455,7 @@ public class DeptServiceImpl extends ServiceImpl implements ID rootDept.setIsDeleted(BladeConstant.DB_NOT_DELETED); rootDept.setIsOa(0); rootDept.setIsPlatformCompany(0); + rootDept.setIsBusinessDept(0); rootDept.setSyncTime(new Date()); this.save(rootDept); log.info("已创建 OA 同步集团顶级 id={} name={} code={}", @@ -536,6 +549,7 @@ public class DeptServiceImpl extends ServiceImpl implements ID newRoot.setIsDeleted(BladeConstant.DB_NOT_DELETED); newRoot.setIsOa(0); newRoot.setIsPlatformCompany(0); + newRoot.setIsBusinessDept(0); newRoot.setSyncTime(new Date()); this.save(newRoot); log.info("迁移创建新集团顶级 id={}(来自历史 id={})", newRootId, legacyRootId); @@ -598,6 +612,7 @@ public class DeptServiceImpl extends ServiceImpl implements ID externalDept.setIsDeleted(BladeConstant.DB_NOT_DELETED); externalDept.setIsOa(0); externalDept.setIsPlatformCompany(0); + externalDept.setIsBusinessDept(0); externalDept.setSyncTime(new Date()); this.save(externalDept); log.info("已创建外部组织顶级 id={}", externalDept.getId()); @@ -678,6 +693,9 @@ public class DeptServiceImpl extends ServiceImpl implements ID if (dept.getIsPlatformCompany() == null) { dept.setIsPlatformCompany(0); } + if (dept.getIsBusinessDept() == null) { + dept.setIsBusinessDept(0); + } // 排序未填时默认 1 if (dept.getSort() == null) { dept.setSort(1); @@ -804,6 +822,7 @@ public class DeptServiceImpl extends ServiceImpl implements ID dept.setIsDeleted(BladeConstant.DB_NOT_DELETED); dept.setIsOa(1); dept.setIsPlatformCompany(0); + dept.setIsBusinessDept(0); dept.setSyncTime(new Date()); boolean saved = save(dept); if (saved) { @@ -877,7 +896,9 @@ public class DeptServiceImpl extends ServiceImpl implements ID private void validateContractInitialSerial(Dept dept) { Integer serial = dept.getContractInitialSerial(); - if (serial == null) { + // -1 为空占位,未填写时按 null 落库,不自动保留 -1 + if (serial == null || serial == -1) { + dept.setContractInitialSerial(null); return; } if (serial < 1 || serial > 999) { diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MpUserServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MpUserServiceImpl.java index 125b63ce..f3f280af 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MpUserServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MpUserServiceImpl.java @@ -38,16 +38,21 @@ import org.springblade.core.tool.utils.Func; import org.springblade.core.tool.utils.StringUtil; import org.springblade.core.tool.utils.WebUtil; 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.pojo.dto.MpUserEnsureDTO; 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.enums.MpUserIdentity; +import org.springblade.system.pojo.vo.MpUserDataScopeVO; import org.springblade.system.service.IMpUserService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -72,6 +77,19 @@ public class MpUserServiceImpl extends BaseServiceImpl imp */ private static final String TENANT_HEADER = "Tenant-Id"; + /** 数据权限范围:仅所属组织 */ + 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 public MpUser getByPhone(String tenantId, String phone) { if (Func.isBlank(tenantId) || Func.isBlank(phone)) { @@ -213,6 +231,64 @@ public class MpUserServiceImpl extends BaseServiceImpl imp .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 baseDeptIds; + if (Objects.equals(dataScopeRange, DATA_SCOPE_CUSTOM)) { + baseDeptIds = mpUserDataScopeMapper.selectList(Wrappers.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 allowedDeptIds = new LinkedHashSet<>(); + for (Long deptId : baseDeptIds) { + if (Func.isEmpty(deptId) || deptId <= 0) { + continue; + } + if (includeChildren) { + List 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 @Transactional(rollbackFor = Exception.class) public boolean submit(MpUser user) { @@ -223,7 +299,7 @@ public class MpUserServiceImpl extends BaseServiceImpl imp throw new ServiceException("该手机号已存在小程序用户"); } encryptPasswordIfPresent(user); - boolean saved = save(user); + boolean saved = save(user) && submitScopes(user); if (saved) { MpUserCache.clear(); } @@ -236,7 +312,18 @@ public class MpUserServiceImpl extends BaseServiceImpl imp if (user == null || Func.isEmpty(user.getId())) { throw new ServiceException("小程序用户主键不能为空"); } + MpUser dbUser = getById(user.getId()); + if (dbUser == null || Func.isEmpty(dbUser.getId())) { + throw new ServiceException("小程序用户不存在"); + } 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); MpUser existed = getByPhone(user.getTenantId(), user.getPhone()); if (existed != null && Func.isNotEmpty(existed.getId()) && !existed.getId().equals(user.getId())) { @@ -244,7 +331,7 @@ public class MpUserServiceImpl extends BaseServiceImpl imp } // 后台编辑不改密码,避免空串覆盖 user.setPassword(null); - boolean updated = updateById(user); + boolean updated = updateById(user) && submitScopes(user); if (updated) { MpUserCache.clear(); } @@ -268,6 +355,7 @@ public class MpUserServiceImpl extends BaseServiceImpl imp } boolean removed = deleteLogic(ids); if (removed) { + mpUserDataScopeMapper.delete(Wrappers.lambdaQuery().in(MpUserDataScope::getUserId, ids)); MpUserCache.clear(); } return removed; @@ -422,6 +510,15 @@ public class MpUserServiceImpl extends BaseServiceImpl imp user.setPostId(trimToNull(user.getPostId())); user.setRemark(trimToNull(user.getRemark())); 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())) { user.setAccount(user.getPhone()); } @@ -455,6 +552,9 @@ public class MpUserServiceImpl extends BaseServiceImpl imp if (Func.isBlank(user.getPhone())) { throw new ServiceException("手机号不能为空"); } + if (Func.isBlank(user.getDeptId())) { + throw new ServiceException("请选择所属组织"); + } if (Func.isBlank(user.getIdentities())) { throw new ServiceException("小程序身份不能为空"); } @@ -467,6 +567,42 @@ public class MpUserServiceImpl extends BaseServiceImpl imp 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.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.lambdaQuery() + .eq(MpUserDataScope::getUserId, user.getId())).stream().map(MpUserDataScope::getDeptId).toList()); + return user; } private String normalizeIdentities(String identities) { @@ -485,4 +621,18 @@ public class MpUserServiceImpl extends BaseServiceImpl imp 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("*"); + } + } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java index 7978f376..21d62c4a 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java @@ -935,7 +935,9 @@ public class OASyncServiceImpl implements IOASyncService { .ne(Dept::getOaId, "") .select(Dept::getId, Dept::getDeptCode, Dept::getDeptName, Dept::getFullName, 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 byOaId = new HashMap<>(); for (Dept dept : deptList) { if (StringUtils.isNotBlank(dept.getOaId())) { @@ -998,6 +1000,11 @@ public class OASyncServiceImpl implements IOASyncService { } else { 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())) { deptIdByCode.put(dept.getDeptCode(), dept.getId()); } @@ -1058,6 +1065,55 @@ public class OASyncServiceImpl implements IOASyncService { 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 一致则跳过更新;任一侧为空无法确认一致,执行更新以补齐字段。 * 组织编码已按新规则重算、父子挂载不一致、或名称字段与当前映射不一致时,即使 modified 相同也要更新。 diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/wrapper/MpUserWrapper.java b/blade-service/blade-system/src/main/java/org/springblade/system/wrapper/MpUserWrapper.java index 2022dcd7..1b50e295 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/wrapper/MpUserWrapper.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/wrapper/MpUserWrapper.java @@ -26,6 +26,7 @@ import org.springblade.core.mp.support.BaseEntityWrapper; import org.springblade.core.tool.utils.BeanUtil; import org.springblade.core.tool.utils.Func; import org.springblade.system.cache.DictCache; +import org.springblade.system.cache.SysCache; import org.springblade.system.cache.UserCache; import org.springblade.system.pojo.entity.MpUser; import org.springblade.system.pojo.enums.DictEnum; @@ -53,6 +54,9 @@ public class MpUserWrapper extends BaseEntityWrapper { // 密码哈希不下发前端 mpUserVO.setPassword(null); mpUserVO.setIdentitiesName(resolveIdentitiesName(mpUser.getIdentities())); + if (Func.isNotBlank(mpUser.getDeptId())) { + mpUserVO.setDeptName(Func.join(SysCache.getDeptNames(mpUser.getDeptId()))); + } if (mpUser.getSex() != null) { mpUserVO.setSexName(DictCache.getValue(DictEnum.SEX, mpUser.getSex())); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java index 01a53d18..3ec0e5fe 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java @@ -99,6 +99,8 @@ public class FormalSettlementController extends BladeController { excel.setContractNo(vo.getContractNo()); excel.setContractName(vo.getContractName()); excel.setSettlementAmount(formatMoney(vo.getSettlementAmount(), vo.getCurrency())); + excel.setBusinessModelName(vo.getBusinessModelName()); + excel.setTransportTypeName(vo.getTransportTypeName()); excel.setLocalSettlementAmount(formatMoney(vo.getLocalSettlementAmount(), vo.getLocalCurrency())); excel.setExchangeRate(vo.getExchangeRate() == null ? "" : vo.getExchangeRate().stripTrailingZeros().toPlainString()); excel.setInvoiceStatusName(invoiceStatusName(vo.getInvoiceStatus(), vo.getSettlementType())); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PreSettlementController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PreSettlementController.java index e337b9a1..385e8ded 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PreSettlementController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PreSettlementController.java @@ -250,6 +250,8 @@ public class PreSettlementController extends BladeController { excel.setContractNo(vo.getContractNo()); excel.setContractName(vo.getContractName()); excel.setSettlementAmount(formatMoney(vo.getSettlementAmount(), vo.getCurrency())); + excel.setBusinessModelName(vo.getBusinessModelName()); + excel.setTransportTypeName(vo.getTransportTypeName()); excel.setLocalSettlementAmount(formatMoney(vo.getLocalSettlementAmount(), vo.getLocalCurrency())); excel.setExchangeRate(vo.getExchangeRate() == null ? "" : vo.getExchangeRate().stripTrailingZeros().toPlainString()); excel.setAdvanceAppliedAmount(formatMoney(vo.getAdvanceAppliedAmount(), vo.getCurrency())); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/FormalSettlementExcel.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/FormalSettlementExcel.java index 56ca2039..99b72f1e 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/FormalSettlementExcel.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/FormalSettlementExcel.java @@ -39,6 +39,10 @@ public class FormalSettlementExcel implements Serializable { private String contractName; @ExcelProperty("原币结算金额") private String settlementAmount; + @ExcelProperty("业务模式") + private String businessModelName; + @ExcelProperty("运输方式") + private String transportTypeName; @ExcelProperty("本位币结算金额") private String localSettlementAmount; @ExcelProperty("结算汇率") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/PreSettlementExcel.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/PreSettlementExcel.java index d6d15c05..29d3a936 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/PreSettlementExcel.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/PreSettlementExcel.java @@ -50,6 +50,10 @@ public class PreSettlementExcel implements Serializable { private String contractName; @ExcelProperty("原币结算金额") private String settlementAmount; + @ExcelProperty("业务模式") + private String businessModelName; + @ExcelProperty("运输方式") + private String transportTypeName; @ExcelProperty("本位币结算金额") private String localSettlementAmount; @ExcelProperty("结算汇率") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/SettlementImportTemplateExporter.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/SettlementImportTemplateExporter.java index cee7d476..b703f1ce 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/SettlementImportTemplateExporter.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/SettlementImportTemplateExporter.java @@ -96,8 +96,7 @@ public final class SettlementImportTemplateExporter { private SettlementImportTemplateExporter() { } - public static void export(HttpServletResponse response, List transportTypeOptions, - List feeItemNames) { + public static void export(HttpServletResponse response, List transportTypeOptions) { List headers = buildHeaders(); // 运输方式由调用方传入业务字典 transport_type 的字典名称,禁止写死 List transportOptions = transportTypeOptions == null ? List.of() : transportTypeOptions.stream() @@ -106,7 +105,7 @@ public final class SettlementImportTemplateExporter { .distinct() .toList(); try (Workbook workbook = new XSSFWorkbook()) { - writeInstructionSheet(workbook, transportOptions, feeItemNames); + writeInstructionSheet(workbook); Sheet dataSheet = workbook.createSheet("导入结算单模板"); CellStyle headerStyle = createHeaderStyle(workbook); Row headerRow = dataSheet.createRow(0); @@ -137,8 +136,7 @@ public final class SettlementImportTemplateExporter { return headers; } - private static void writeInstructionSheet(Workbook workbook, List transportOptions, - List feeItemNames) { + private static void writeInstructionSheet(Workbook workbook) { Sheet sheet = workbook.createSheet("填写说明"); CellStyle titleStyle = workbook.createCellStyle(); Font titleFont = workbook.createFont(); @@ -147,24 +145,11 @@ public final class SettlementImportTemplateExporter { List lines = new ArrayList<>(); lines.add("导入结算单填写说明"); lines.add("1. 请不要修改「导入结算单模板」工作表的固定表头;带 * 号为必填项。"); - lines.add("2. 运输方式请从下拉项中选择,须与业务字典 transport_type 的字典名称一致。"); + lines.add("2. 运输方式请从下拉项中选择。"); lines.add("3. 日期请填写 yyyy-MM-dd(如 2026-01-01);Excel 日期单元格也可直接选择。"); lines.add("4. 里程(km) 可为空;填写时须为正数。"); lines.add("5. 「*运输费」之后、「*实际发货时间」之前可按需自行插入费用项列,列名须与系统已启用费用项名称完全一致;金额不为 0 时若不存在或已停用则无法导入。"); 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++) { Row row = sheet.createRow(index); Cell cell = row.createCell(0); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml index e6d9996c..2c04f248 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml @@ -33,13 +33,16 @@ + + + @@ -85,13 +88,16 @@ business_scope, network_freight_platform, business_term_type, + business_start_date, business_end_date, registered_capital, + paid_in_capital, principal, mnemonic_code, customer_level, max_credit_limit, apply_credit_limit, + used_credit_limit, remark, qualification_attachments, access_type, diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillLedgerServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillLedgerServiceImpl.java index a20a4e0e..6528a411 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillLedgerServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillLedgerServiceImpl.java @@ -107,7 +107,7 @@ public class BillLedgerServiceImpl extends BaseServiceImpl availableList(String keyword, Long deptId, Long selectedId, String payerName, String payeeName) { LocalDate today = LocalDate.now(); - // 未到期且有余额;收票且部门可用,或出票/收票单位与结算收付款方一致(开票、收票都可) + // 可选汇票:未到期且可用余额 > 0(已选中的可回显);不再按部门、开票/收票类型、结算收付款方过滤 return list(Wrappers.lambdaQuery() .and(Func.isNotEmpty(keyword), wrapper -> wrapper.like(BillLedger::getBillNo, keyword) .or().like(BillLedger::getIssuerName, keyword) @@ -119,35 +119,10 @@ public class BillLedgerServiceImpl extends BaseServiceImpl child.eq(BillLedger::getId, selectedId))) .orderByAsc(BillLedger::getMaturityDate) .orderByDesc(BillLedger::getCreateTime)).stream() - .filter(item -> selectedId != null && Objects.equals(item.getId(), selectedId) - || selectableBill(item, deptId, payerName, payeeName)) .map(item -> BillLedgerWrapper.build().entityVO(item)) .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 @Transactional(rollbackFor = Exception.class) public Long submit(BillLedgerSaveRequest request) { @@ -292,12 +267,6 @@ public class BillLedgerServiceImpl extends BaseServiceImpl 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) { if (Func.isEmpty(value)) return List.of(); try { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillPaymentServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillPaymentServiceImpl.java index ec93da0a..b2b3e6f9 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillPaymentServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillPaymentServiceImpl.java @@ -32,7 +32,6 @@ import lombok.RequiredArgsConstructor; import org.springblade.core.log.exception.ServiceException; import org.springblade.core.mp.base.BaseServiceImpl; import org.springblade.core.secure.utils.AuthUtil; -import org.springblade.core.tool.jackson.JsonUtil; import org.springblade.core.tool.utils.Func; import org.springblade.system.cache.SysCache; import org.springblade.transport.constant.TransportBusiConstant; @@ -112,7 +111,7 @@ public class BillPaymentServiceImpl extends BaseServiceImpllambdaQuery() .eq(BillLedgerUsage::getBillPaymentId, entity.getId()) .eq(BillLedgerUsage::getUsageStatus, TransportBusiConstant.APPROVED) @@ -245,13 +244,13 @@ public class BillPaymentServiceImpl extends BaseServiceImpl 0) { 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) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java index 9d36d811..aab31d8b 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java @@ -83,6 +83,8 @@ public class ContractManageServiceImpl extends BaseServiceImpl beforeData = buildContractChangeSnapshot(source); - applyContractNo(source, request.getContractNo(), false); - source.setContractName(request.getContractName()); source.setPartyB(request.getPartyB()); - source.setStartDate(request.getStartDate()); source.setEndDate(request.getEndDate()); source.setContractFormat(request.getContractFormat()); - 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.setElectronicSealFlag(request.getElectronicSealFlag()); - normalizeOptionalIntegerFields(source); - validateContractAmountRequired(source); - validateAdditionalFields(source); - source.setBillingPlanJson(request.getBillingPlanJson()); source.setSettlementRuleJson(request.getSettlementRuleJson()); source.setPreSettlementConfigJson(request.getPreSettlementConfigJson()); source.setFormalSettlementConfigJson(request.getFormalSettlementConfigJson()); source.setPaymentRatioJson(request.getPaymentRatioJson()); - source.setContractFileJson(request.getContractFileJson()); source.setAttachmentsJson(request.getAttachmentsJson()); - source.setChangeAttachmentsJson(TransportBusinessSupport.trimToNull(request.getChangeAttachmentsJson())); + if (settlementRuleOnly) { + applySettlementRuleChange(source, request); + } else { + applyContractInfoChange(source, request); + } Map 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, "合同信息变更", changeReason, STATUS_CHANGE_REVIEWING, "审核中", beforeData, afterData); + 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(); + BeanUtil.copyProperties(source, candidate); + candidate.setPartyB(TransportBusinessSupport.trimToNull(request.getPartyB())); + validateContractPartyCombinationUnique(candidate); + validatePaymentRatios(request.getPaymentRatioJson()); + applyContractNo(source, request.getContractNo(), false); + source.setContractName(request.getContractName()); + source.setPartyB(request.getPartyB()); + source.setStartDate(request.getStartDate()); + source.setEndDate(request.getEndDate()); + source.setContractFormat(request.getContractFormat()); + 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.setElectronicSealFlag(request.getElectronicSealFlag()); + normalizeOptionalIntegerFields(source); + validateContractAmountRequired(source); + validateAdditionalFields(source); + source.setBillingPlanJson(request.getBillingPlanJson()); + source.setSettlementRuleJson(request.getSettlementRuleJson()); + source.setPreSettlementConfigJson(request.getPreSettlementConfigJson()); + source.setFormalSettlementConfigJson(request.getFormalSettlementConfigJson()); + source.setPaymentRatioJson(request.getPaymentRatioJson()); + source.setContractFileJson(request.getContractFileJson()); + source.setAttachmentsJson(request.getAttachmentsJson()); + } + @Override @Transactional(rollbackFor = Exception.class) public boolean updateAttachments(ContractManage contractManage) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CreditScoreQuantificationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CreditScoreQuantificationServiceImpl.java index 25014d85..83035479 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CreditScoreQuantificationServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CreditScoreQuantificationServiceImpl.java @@ -458,6 +458,10 @@ public class CreditScoreQuantificationServiceImpl extends BaseServiceImpl queryWrapper = Wrappers.lambdaQuery() .eq(CreditScoreQuantification::getName, quantification.getName()) .eq(CreditScoreQuantification::getIsDeleted, 0); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java index a8444ebf..8293a856 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java @@ -280,6 +280,8 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl() : customer.getContacts()); + syncContactPhoneFromContacts(customer); applyFixedOwnerDept(customer); customer.setAccessType(Func.isEmpty(customer.getAccessType()) ? ACCESS_TEMPORARY : customer.getAccessType()); customer.setApprovalStatus(Func.isEmpty(customer.getApprovalStatus()) ? APPROVAL_DRAFT : customer.getApprovalStatus()); @@ -977,12 +987,33 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl() : customer.getContacts()); customer.setReceiptAccounts(customer.getReceiptAccounts() == null ? new ArrayList<>() : customer.getReceiptAccounts()); customer.setInvoices(customer.getInvoices() == null ? new ArrayList<>() : customer.getInvoices()); 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 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) { if (Func.isEmpty(customer.getFullName())) { throw new ServiceException("客商全称不能为空"); @@ -1009,10 +1040,8 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl 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) { List typeList = Func.toStrList(customerType.replace(",", ",")) .stream() @@ -1364,6 +1407,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl customerIds) { + // 逻辑删除保留历史评分明细;曾审核通过的旧评分在再次审核通过前仍通过主表等级/额度暂时生效 List scores = creditScoreMapper.selectList(Wrappers.lambdaQuery() .in(CustomerCreditScore::getCustomerId, customerIds) .and(wrapper -> wrapper.ne(CustomerCreditScore::getIsDeleted, 1).or().isNull(CustomerCreditScore::getIsDeleted))); @@ -1374,6 +1418,42 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl 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 loadContacts(Long customerId) { return contactMapper.selectList(Wrappers.lambdaQuery() .eq(CustomerContact::getCustomerId, customerId) @@ -2040,13 +2120,16 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl buildLinkmanEntries(CustomerArchiveVO customer) { + if (Func.isNotEmpty(customer.getContacts())) { + List 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())) { return List.of(); } BfiCustomerLinkmanEntryDTO entry = new BfiCustomerLinkmanEntryDTO(); - // 联系人:客商负责人 entry.setContactperson(customer.getPrincipal()); - // 电话:联系电话 - entry.setPhone(customer.getContactPhone()); - // 默认联系人 + entry.setPhone(this.resolveContactPhone(customer)); entry.setIsdefaultLinkman(Boolean.TRUE); 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 字典)—— * 值为 dictKey(即伙伴类型编码,1~17)时直接使用; @@ -248,35 +285,50 @@ public class CustomerBfiConverter { } /** - * 经营范围多选编码 → 字典文本「、」拼接,字典缺失时回退原编码 + * 经营范围:长文本直传;兼容历史多选编码 → 字典文本「、」拼接。 * - * @param businessScope 逗号拼接的经营范围编码 + * @param businessScope 经营范围长文本或历史逗号拼接编码 * @return 金蝶经营范围文本 */ public String buildBusinessScopeText(String businessScope) { if (Func.isEmpty(businessScope)) { return ""; } + String text = businessScope.trim(); + // 长文本(含中文描述)直接推送,不再按字典拆分 + if (text.length() > 40 || text.contains(";") || text.contains("。") || text.contains("\n")) { + return text; + } List dictList = DictBizCache.getList("scope_of_business"); Map scopeNames = dictList == null ? Map.of() : dictList.stream() .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)) .collect(Collectors.joining("、")); } /** - * 营业期限:长期传「长期」;固定期限传「至 yyyy-MM-dd」(TMS 未采集起始日) + * 营业期限:长期传「长期」;固定期限传「yyyy-MM-dd 至 yyyy-MM-dd」或「至 yyyy-MM-dd」。 * - * @param businessTermType 营业期限类型 - * @param businessEndDate 营业期限截止日 + * @param businessTermType 营业期限类型 + * @param businessStartDate 营业期限开始日 + * @param businessEndDate 营业期限截止日 * @return 金蝶营业期限文本 */ - public String buildBusinessTerm(String businessTermType, LocalDate businessEndDate) { + public String buildBusinessTerm(String businessTermType, LocalDate businessStartDate, LocalDate businessEndDate) { if (CustomerBfiConstants.BUSINESS_TERM_LONG.equals(businessTermType)) { - return CustomerBfiConstants.BUSINESS_TERM_LONG; + if (businessStartDate == null) { + return CustomerBfiConstants.BUSINESS_TERM_LONG; + } + return businessStartDate + " 至 " + CustomerBfiConstants.BUSINESS_TERM_LONG; } - return businessEndDate == null ? "" : "至 " + businessEndDate; + if (businessStartDate != null && businessEndDate != null) { + return businessStartDate + " 至 " + businessEndDate; + } + if (businessEndDate != null) { + return "至 " + businessEndDate; + } + return businessStartDate == null ? "" : String.valueOf(businessStartDate); } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ExceptionDisposalServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ExceptionDisposalServiceImpl.java index c5806856..4a6acf7e 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ExceptionDisposalServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ExceptionDisposalServiceImpl.java @@ -50,6 +50,7 @@ import org.springblade.transport.pojo.vo.ExceptionDisposalVO; import org.springblade.transport.service.IDriverWaybillService; import org.springblade.transport.service.IExceptionDisposalService; import org.springblade.transport.service.IWaybillService; +import org.springblade.transport.support.MpUserDataScopeSupport; import org.springblade.transport.support.TransportBusinessSupport; import org.springblade.transport.support.WaybillProcessSupport; import org.springframework.stereotype.Service; @@ -117,15 +118,18 @@ public class ExceptionDisposalServiceImpl private final IWaybillService waybillService; private final IDriverWaybillService driverWaybillService; private final LoadingManageMapper loadingManageMapper; + private final MpUserDataScopeSupport mpUserDataScopeSupport; public ExceptionDisposalServiceImpl(ExceptionDisposalFollowRecordMapper followRecordMapper, IWaybillService waybillService, IDriverWaybillService driverWaybillService, - LoadingManageMapper loadingManageMapper) { + LoadingManageMapper loadingManageMapper, + MpUserDataScopeSupport mpUserDataScopeSupport) { this.followRecordMapper = followRecordMapper; this.waybillService = waybillService; this.driverWaybillService = driverWaybillService; this.loadingManageMapper = loadingManageMapper; + this.mpUserDataScopeSupport = mpUserDataScopeSupport; } @Override @@ -929,6 +933,8 @@ public class ExceptionDisposalServiceImpl .like(Func.isNotEmpty(query.getCarrierName()), ExceptionDisposal::getCarrierName, query.getCarrierName()) .eq(Func.isNotEmpty(query.getExceptionType()), ExceptionDisposal::getExceptionType, query.getExceptionType()) .eq(Func.isNotEmpty(query.getDisposalStatus()), ExceptionDisposal::getDisposalStatus, query.getDisposalStatus()); + // 调度端列表按「权限组织 → 项目业务部门 → 项目」收口;司机端按运单查自己的异常时不套此过滤 + mpUserDataScopeSupport.applyProjectScopeIfMpDispatcher(wrapper, ExceptionDisposal::getProjectId); return wrapper.orderByDesc(ExceptionDisposal::getCreateTime); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java index 62e6a744..4b8bdfee 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java @@ -350,9 +350,14 @@ public class FormalSettlementServiceImpl extends BaseServiceImpllambdaQuery() - .eq(FormalSettlementChangeRecord::getFormalSettlementId, id) - .orderByDesc(FormalSettlementChangeRecord::getChangeTime))); + List changeRecords = changeRecordMapper.selectList( + Wrappers.lambdaQuery() + .eq(FormalSettlementChangeRecord::getFormalSettlementId, id) + .orderByDesc(FormalSettlementChangeRecord::getChangeTime)); + // 审批通过/驳回类不展示在变更记录中 + vo.setChangeRecords(changeRecords.stream() + .filter(record -> !isApprovalChangeContent(record.getChangeContent())) + .toList()); return vo; } @@ -558,6 +563,8 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl sourceFeeMap = receivablePayableCargoFeeMapper.selectBatchIds(sourceFeeIds).stream() .collect(Collectors.toMap(ReceivablePayableCargoFee::getId, Function.identity(), (left, right) -> left)); + // 历史数据可能把预结算费用行ID误写入 sourceFeeId,需再解析到应收应付费用行 + List unresolvedIds = sourceFeeIds.stream().filter(id -> !sourceFeeMap.containsKey(id)).toList(); + Map preFeeMap = unresolvedIds.isEmpty() ? Map.of() + : preDetailFeeMapper.selectBatchIds(unresolvedIds).stream() + .collect(Collectors.toMap(PreSettlementDetailFee::getId, Function.identity(), (left, right) -> left)); + List 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 -> { - 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) { fee.setBillingRulesJson(sourceFee.getBillingRulesJson()); } @@ -2029,7 +2058,11 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl beforeData, Map afterData) { + if (isApprovalChangeContent(content)) { + return; + } FormalSettlementChangeRecord record = new FormalSettlementChangeRecord(); record.setFormalSettlementId(settlementId); record.setChangeType(changeType); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java index ed88df34..268ddb9c 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java @@ -20,9 +20,11 @@ import org.springblade.system.pojo.entity.Dept; import org.springblade.transport.excel.LoadingManageExcel; import org.springblade.transport.mapper.LoadingManageMapper; 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.LoadingManage; 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.LoadingCarrierContractVO; import org.springblade.transport.pojo.vo.LoadingManageVO; @@ -74,6 +76,9 @@ public class LoadingManageServiceImpl extends BaseServiceImpl 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); // 草稿仅预占运单号,不把运单业务状态改成 draft;正式确认时再完整同步为待执行 if (STATUS_DRAFT.equals(businessStatus)) { syncDraftWaybillReservation(loadingManage, oldWaybillIds); + } else if (STATUS_RUNNING.equals(businessStatus)) { + // 进行中暂存:不重置原有子单状态/接单,仅同步任务信息与新增子单 + List waybills = loadWaybills(waybillIds(loadingManage.getWaybillIdsJson())); + writeWaybillsForRunningEdit(loadingManage, oldWaybillIds, waybills); } else { syncAssociatedWaybills(loadingManage, businessStatus, oldWaybillIds); } @@ -156,28 +177,103 @@ public class LoadingManageServiceImpl extends BaseServiceImpl oldWaybillIds = new ArrayList<>(); boolean ownChildrenAdmitted = false; + boolean runningEdit = false; + LoadingManage oldRecord = null; if (Func.isNotEmpty(loadingManage.getId())) { - LoadingManage oldRecord = loadEditable(loadingManage.getId(), true); + oldRecord = loadEditable(loadingManage.getId(), true); oldWaybillIds = waybillIds(oldRecord.getWaybillIdsJson()); // 草稿只预占运单号、未过判据,草稿转正式不得沿用本单豁免 ownChildrenAdmitted = !STATUS_DRAFT.equals(oldRecord.getBusinessStatus()); + runningEdit = STATUS_RUNNING.equals(oldRecord.getBusinessStatus()); loadingManage.setLoadingNo(oldRecord.getLoadingNo()); loadingManage.setDeptId(oldRecord.getDeptId()); loadingManage.setDeptName(oldRecord.getDeptName()); } prepareCreateOrUpdate(loadingManage); - // 确认提交:草稿/新建统一生成正式配载单(待执行) - loadingManage.setBusinessStatus(STATUS_PENDING); if (Func.isEmpty(loadingManage.getLoadingNo())) { loadingManage.setLoadingNo(nextCode()); } List waybills = validateWaybillsAvailable(loadingManage, waybillIds(loadingManage.getWaybillIdsJson()), ownChildrenAdmitted); 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); - writeWaybillsForDriver(loadingManage, oldWaybillIds, waybills); + // 进行中编辑:保留原有子单状态与接单事实,仅对新加入子单做派车同步 + if (runningEdit) { + writeWaybillsForRunningEdit(loadingManage, oldWaybillIds, waybills); + } else { + writeWaybillsForDriver(loadingManage, oldWaybillIds, waybills); + } return result; } + /** + * 进行中配载编辑校验:必填任务字段不可改;已装/卸打卡的子运单不可移除。 + */ + private void validateRunningEdit(LoadingManage oldRecord, LoadingManage incoming, + List waybills, List oldWaybillIds) { + validateRunningImmutableTaskFields(oldRecord, incoming); + Set 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 punches = waybillNodePunchMapper.selectList(Wrappers.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 @Transactional(rollbackFor = Exception.class) public void createFromImportedWaybills(String loadingNo, List waybills) { @@ -776,8 +872,8 @@ public class LoadingManageServiceImpl extends BaseServiceImpl "运输方式不是公路,不可配载"; - case NOT_PENDING -> "当前状态不是待执行,不可配载"; - case ON_OTHER_MANIFEST -> "已关联其他配载单"; + case NOT_LOADABLE_STATUS -> "当前状态不是待执行或进行中,不可配载"; + case ON_OTHER_MANIFEST -> "已关联其他有效配载单"; }; } @@ -910,6 +1006,108 @@ public class LoadingManageServiceImpl extends BaseServiceImpl oldWaybillIds, List waybills) { + List newWaybillIds = waybillIds(loadingManage.getWaybillIdsJson()); + Set oldIdSet = new HashSet<>(oldWaybillIds); + List removedIds = oldWaybillIds.stream() + .filter(id -> id != null && !newWaybillIds.contains(id)) + .toList(); + releaseAssociatedWaybills(removedIds, STATUS_PENDING); + List keptIds = newWaybillIds.stream() + .filter(oldIdSet::contains) + .toList(); + if (Func.isNotEmpty(keptIds)) { + updateAssociatedWaybillTaskFields(loadingManage, keptIds, null); + } + List addedIds = newWaybillIds.stream() + .filter(id -> id != null && !oldIdSet.contains(id)) + .toList(); + if (Func.isEmpty(addedIds)) { + return; + } + List addedWaybills = waybills.stream() + .filter(waybill -> waybill.getId() != null && addedIds.contains(waybill.getId())) + .toList(); + Map statusByWaybillId = deriveWaybillBusinessStatuses(addedWaybills); + Map> 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 waybillIds, String status) { + if (Func.isEmpty(waybillIds)) { + return; + } + var update = Wrappers.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 waybillIds) { + if (Func.isEmpty(waybillIds)) { + return; + } + waybillMapper.update(null, Wrappers.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)); + } + + /** + * 配载单业务状态按子运单所属项目的实时过程配置推导:需要接单 → 待执行,不需要 → 进行中。 + *

+ * 一张配载单的子运单同属一个项目,任一张需接单则整单待执行。 + */ + private String deriveLoadingBusinessStatus(List waybills) { + if (Func.isEmpty(waybills)) { + return STATUS_RUNNING; + } + Map 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; + } + /** * 子运单业务状态逐张按各自项目的实时过程配置推导:需要接单 → 待执行,不需要 → 进行中。 *

diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ManageWaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ManageWaybillServiceImpl.java index f50ae11f..c9f42670 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ManageWaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ManageWaybillServiceImpl.java @@ -33,6 +33,7 @@ import org.springblade.core.secure.utils.AuthUtil; import org.springblade.core.tool.utils.DateUtil; import org.springblade.core.tool.utils.Func; 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.ExceptionDisposal; 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.ITripQueryService; import org.springblade.transport.service.IWaybillService; +import org.springblade.transport.support.MpUserDataScopeSupport; import org.springblade.transport.support.TransportBusinessSupport; import org.springblade.transport.support.WaybillCargoSupport; import org.springblade.transport.support.WaybillProcessSupport; @@ -89,8 +91,8 @@ import java.util.stream.Collectors; * 列表与详情与司机端同粒度:取数、状态聚合、接单需求合并都调共享运次读服务, * 本类只做字段裁剪与调度端独有的筛选(组织类型 / 异常 / 创建时间 / 待重新派单)。 *

- * 小程序调度账号(如「小程序管理」)组织常与运单业务组织不一致,故不做 dept 过滤, - * 仅依赖租户隔离,口径接近后台 {@code /waybill-manage/list?allDept=1}。 + * 小程序调度端:所属组织 + 数据权限范围 + 数据层级范围 → 业务部门在范围内的项目 → 再取这些项目的运单; + * 异常/风险与运单同一项目口径。系统用户仍按「所属组织 / 超管不过滤」。 */ @Service @RequiredArgsConstructor @@ -139,13 +141,14 @@ public class ManageWaybillServiceImpl implements IManageWaybillService { private final IDriverWaybillService driverWaybillService; private final ITripQueryService tripQueryService; private final ILoadingManageService loadingManageService; + private final MpUserDataScopeSupport mpUserDataScopeSupport; @Override public AdminHomeStatsVO stats() { AdminHomeStatsVO vo = new AdminHomeStatsVO(); // 三档状态按运次计:有配载单的一趟车算一个,与列表和司机端同一份聚合 TripQueryVO query = TripQueryVO.forAdmin(); - query.setDeptIds(manageableDeptIds()); + applyAdminDataScope(query); TripTabCountsVO tripCounts = tripQueryService.tabCounts(query); vo.setPendingAccept(tripCounts.getPending()); vo.setTransporting(tripCounts.getDoing()); @@ -183,7 +186,7 @@ public class ManageWaybillServiceImpl implements IManageWaybillService { // 组织类型 / 异常 / 创建时间都是子单行级条件,与状态、关键字、分页一起交给运次读服务, // 一次取数取完整趟车:分组、分档、筛选、排序与分页都在它那边算完 TripQueryVO query = TripQueryVO.forAdmin(); - query.setDeptIds(manageableDeptIds()); + applyAdminDataScope(query); query.setStatus(toTripStatus(status)); query.setKeyword(keyword); query.setTransportOrgType(transportType); @@ -231,7 +234,7 @@ public class ManageWaybillServiceImpl implements IManageWaybillService { Set exceptionWaybillIds = loadIncompleteExceptionWaybillIds(); TripQueryVO query = TripQueryVO.forAdmin(); - query.setDeptIds(manageableDeptIds()); + applyAdminDataScope(query); query.setKeyword(keyword); // 待处理只要待接单与进行中两档;「未拒单」也是交给运次读服务按子单行筛 query.setStatuses(List.of(0, 1)); @@ -669,11 +672,8 @@ public class ManageWaybillServiceImpl implements IManageWaybillService { SQL_MAX_REJECT_TIME + " AS driver_reject_time", SQL_MAX_CREATE_TIME + " AS create_time"); wrapper.eq("driver_accept_status", ACCEPT_REJECTED); wrapper.in("business_status", STATUS_PENDING, STATUS_RUNNING); - // 这条查询不经运次读服务,组织收口得自己带上,否则待重派列表与首页重派角标会出别的组织的单 - List deptIds = manageableDeptIds(); - if (Func.isNotEmpty(deptIds)) { - wrapper.in("dept_id", deptIds); - } + // 这条查询不经运次读服务,数据权限收口得自己带上,否则待重派列表与首页重派角标会越权 + applyAdminDataScope(wrapper); wrapper.groupBy(TripAggregation.SQL_TRIP_KEY); return wrapper; } @@ -892,10 +892,50 @@ public class ManageWaybillServiceImpl implements IManageWaybillService { } /** - * 调度端可见组织:普通用户(含各级角色)只出自己所属组织,超级管理员不过滤。 - *

- * 小程序不校验「所属组织不能为空」:账号没有组织时不按组织收口,避免首页、待处理等接口直接报错。 - * 详情与重新派单本身不走这里。 + * 调度端数据权限落到运次查询条件:小程序按可见项目,系统用户按所属组织。 + */ + 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 wrapper) { + if (wrapper == null) { + return; + } + if (MpUserAuth.isMpUser()) { + List projectIds = manageableProjectIds(); + if (Func.isNotEmpty(projectIds)) { + wrapper.in("project_id", projectIds); + } + return; + } + List deptIds = manageableDeptIds(); + if (Func.isNotEmpty(deptIds)) { + wrapper.in("dept_id", deptIds); + } + } + + /** + * 小程序调度端可见项目:所属组织 + 数据权限范围 + 数据层级范围 → 业务部门在范围内的项目。 + * 空列表不过滤;[-1] 表示无权限。 + */ + private List manageableProjectIds() { + List projectIds = mpUserDataScopeSupport.resolveVisibleProjectIdsForQuery(); + return projectIds == null ? List.of() : projectIds; + } + + /** + * 系统用户可见组织:普通账号只出所属组织,超管不过滤;组织为空不拦截。 */ private List manageableDeptIds() { if (TransportBusinessSupport.isAdministrator()) { @@ -919,10 +959,12 @@ public class ManageWaybillServiceImpl implements IManageWaybillService { } private Set loadIncompleteExceptionWaybillIds() { - List list = exceptionDisposalService.list(Wrappers.lambdaQuery() + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() .select(ExceptionDisposal::getWaybillId) .in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING) - .isNotNull(ExceptionDisposal::getWaybillId)); + .isNotNull(ExceptionDisposal::getWaybillId); + applyExceptionDeptScope(wrapper); + List list = exceptionDisposalService.list(wrapper); Set ids = new HashSet<>(); for (ExceptionDisposal item : list) { if (item.getWaybillId() != null) { @@ -932,6 +974,38 @@ public class ManageWaybillServiceImpl implements IManageWaybillService { return ids; } + /** 异常与运单同一套项目口径(小程序);系统用户仍按组织 */ + private void applyExceptionDeptScope(LambdaQueryWrapper wrapper) { + if (MpUserAuth.isMpUser()) { + List projectIds = manageableProjectIds(); + if (Func.isNotEmpty(projectIds)) { + wrapper.in(ExceptionDisposal::getProjectId, projectIds); + } + return; + } + List deptIds = manageableDeptIds(); + if (Func.isNotEmpty(deptIds)) { + wrapper.in(ExceptionDisposal::getDeptId, deptIds); + } + } + + /** 风险无项目字段:小程序按关联运单的项目收口;系统用户按组织 */ + private void applyRiskDeptScope(LambdaQueryWrapper wrapper) { + if (MpUserAuth.isMpUser()) { + List 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 deptIds = manageableDeptIds(); + if (Func.isNotEmpty(deptIds)) { + wrapper.in(RiskDisposal::getDeptId, deptIds); + } + } + private AdminWaybillCardVO toCard(Waybill waybill, boolean hasException) { AdminWaybillCardVO card = new AdminWaybillCardVO(); card.setId(waybill.getId()); @@ -1062,13 +1136,17 @@ public class ManageWaybillServiceImpl implements IManageWaybillService { } private long countIncompleteExceptions() { - return exceptionDisposalService.count(Wrappers.lambdaQuery() - .in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING)); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING); + applyExceptionDeptScope(wrapper); + return exceptionDisposalService.count(wrapper); } private long countPendingRisks() { - return riskDisposalService.count(Wrappers.lambdaQuery() - .eq(RiskDisposal::getDisposalStatus, RISK_PENDING)); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .eq(RiskDisposal::getDisposalStatus, RISK_PENDING); + applyRiskDeptScope(wrapper); + return riskDisposalService.count(wrapper); } /** @@ -1110,10 +1188,12 @@ public class ManageWaybillServiceImpl implements IManageWaybillService { } private List buildExceptionFeed() { - List list = exceptionDisposalService.list(Wrappers.lambdaQuery() + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() .in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING) .orderByDesc(ExceptionDisposal::getReportTime) - .last("LIMIT " + FEED_LIMIT)); + .last("LIMIT " + FEED_LIMIT); + applyExceptionDeptScope(wrapper); + List list = exceptionDisposalService.list(wrapper); return list.stream() .map(disposal -> new TimedTodo(toTodoItem(disposal), exceptionTimeOf(disposal))) .collect(Collectors.toList()); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java index 9668c955..f9a28ea3 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java @@ -2131,21 +2131,18 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpllambdaQuery() .eq(PaymentApplicationSettlement::getPaymentApplicationId, entity.getId())); - boolean customAmounts = settlements.size() > 1 && lines != null && !lines.isEmpty(); + boolean customAmounts = this.useSpecifiedLineAmounts(lines); BigDecimal totalBase = settlements.stream() .map(item -> payableOfFormalSettlement(item.getId(), item.getSettlementAmount(), entity.getId())) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal remainingApplied = money(entity.getAppliedAmount()); BigDecimal remainingPaid = money(entity.getPaidAmount()); if (customAmounts) { - BigDecimal lineTotal = BigDecimal.ZERO; for (FormalSettlement settlement : settlements) { BigDecimal payable = payableOfFormalSettlement(settlement.getId(), settlement.getSettlementAmount(), entity.getId()); BigDecimal applied = this.specifiedAppliedAmount(lines, settlement.getId(), null); this.assertAppliedWithinPayable(settlement.getFormalSettlementNo(), applied, payable); - lineTotal = lineTotal.add(applied); } - this.assertLineTotalMatches(entity, lineTotal); } for (int index = 0; index < settlements.size(); index++) { FormalSettlement settlement = settlements.get(index); @@ -2243,20 +2240,18 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl 1 && lines != null && !lines.isEmpty(); + // 结算行付款金额独立于付款基本信息中的申请付款金额,有明细时按行保存 + boolean customAmounts = this.useSpecifiedLineAmounts(lines); BigDecimal totalBase = targetSettlements.stream() .map(item -> payableOfPreSettlement(item.getId(), item.getSettlementAmount(), entity.getId())) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal remainingApplied = money(entity.getAppliedAmount()); if (customAmounts) { - BigDecimal lineTotal = BigDecimal.ZERO; for (PreSettlement settlement : targetSettlements) { BigDecimal payable = payableOfPreSettlement(settlement.getId(), settlement.getSettlementAmount(), entity.getId()); BigDecimal applied = this.specifiedAppliedAmount(lines, null, settlement.getId()); this.assertAppliedWithinPayable(settlement.getPreSettlementNo(), applied, payable); - lineTotal = lineTotal.add(applied); } - this.assertLineTotalMatches(entity, lineTotal); } for (int index = 0; index < targetSettlements.size(); index++) { PreSettlement settlement = targetSettlements.get(index); @@ -2307,11 +2302,9 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl lines) { + return lines != null && !lines.isEmpty(); } private void validateQuota(PaymentApplication entity) { @@ -2563,44 +2556,11 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl 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) { @@ -2756,18 +2716,6 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl 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 public boolean isBillPayment(String paymentMethod) { if (Func.isEmpty(paymentMethod)) return false; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java index f828826f..e29b1aeb 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java @@ -203,13 +203,15 @@ public class PreSettlementServiceImpl extends BaseServiceImpllambdaQuery() - .eq(PreSettlementChangeRecord::getPreSettlementId, id) - .eq(PreSettlementChangeRecord::getIsDeleted, 0) - .and(wrapper -> wrapper.isNull(PreSettlementChangeRecord::getChangeContent) - .or() - .ne(PreSettlementChangeRecord::getChangeContent, "预结算审批通过")) - .orderByDesc(PreSettlementChangeRecord::getChangeTime))); + List changeRecords = changeRecordMapper.selectList( + Wrappers.lambdaQuery() + .eq(PreSettlementChangeRecord::getPreSettlementId, id) + .eq(PreSettlementChangeRecord::getIsDeleted, 0) + .orderByDesc(PreSettlementChangeRecord::getChangeTime)); + // 审批通过/驳回类不展示在变更记录中 + vo.setChangeRecords(changeRecords.stream() + .filter(record -> !isApprovalChangeContent(record.getChangeContent())) + .toList()); vo.setPrintTemplates(resolvePrintTemplates(settlement)); return vo; } @@ -349,6 +351,8 @@ public class PreSettlementServiceImpl extends BaseServiceImpl beforeData, Map afterData) { + if (isApprovalChangeContent(content)) { + return; + } PreSettlementChangeRecord record = new PreSettlementChangeRecord(); record.setPreSettlementId(settlementId); record.setChangeType(changeType); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java index 40ddbb6b..d72428c5 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java @@ -34,6 +34,7 @@ import org.springblade.core.tenant.annotation.TenantIgnore; import org.springblade.core.tool.jackson.JsonUtil; import org.springblade.core.tool.utils.BeanUtil; import org.springblade.core.tool.utils.Func; +import org.springblade.system.cache.SysCache; import org.springblade.system.cache.UserCache; import org.springblade.system.pojo.entity.Dept; import org.springblade.transport.excel.ProjectApplyExcel; @@ -488,8 +489,7 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl 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) { if (projectApply == null) { return; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java index 7a757025..c9865f90 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java @@ -672,8 +672,10 @@ public class ReceivablePayableDetailServiceImpl throw new ServiceException("费用调整试算数据不能为空"); } 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( Wrappers.lambdaQuery() diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/RiskDisposalServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/RiskDisposalServiceImpl.java index 869cdfc0..bc323eb4 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/RiskDisposalServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/RiskDisposalServiceImpl.java @@ -3,9 +3,9 @@ package org.springblade.transport.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; import org.springblade.core.log.exception.ServiceException; 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.Func; 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.RiskDisposalVO; import org.springblade.transport.service.IRiskDisposalService; +import org.springblade.transport.support.MpUserDataScopeSupport; import org.springblade.transport.support.TransportBusinessSupport; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -25,12 +26,15 @@ import java.util.List; import java.util.Objects; @Service +@RequiredArgsConstructor public class RiskDisposalServiceImpl extends BaseServiceImpl implements IRiskDisposalService { private static final String STATUS_PENDING = "pending"; private static final String STATUS_PROCESSED = "processed"; private static final String STATUS_IGNORED = "ignored"; + private final MpUserDataScopeSupport mpUserDataScopeSupport; + @Override public IPage selectPage(IPage page, RiskDisposalVO query) { IPage entityPage = page(page, buildQuery(query)); @@ -84,15 +88,16 @@ public class RiskDisposalServiceImpl extends BaseServiceImpl buildQuery(RiskDisposalVO query) { - return Wrappers.lambdaQuery() + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() .eq(RiskDisposal::getIsDeleted, 0) .like(Func.isNotEmpty(query.getRiskNo()), RiskDisposal::getRiskNo, query.getRiskNo()) .like(Func.isNotEmpty(query.getRuleName()), RiskDisposal::getRuleName, query.getRuleName()) .like(Func.isNotEmpty(query.getWaybillNo()), RiskDisposal::getWaybillNo, query.getWaybillNo()) .like(Func.isNotEmpty(query.getProjectName()), RiskDisposal::getProjectName, query.getProjectName()) .eq(Func.isNotEmpty(query.getRiskLevel()), RiskDisposal::getRiskLevel, query.getRiskLevel()) - .eq(Func.isNotEmpty(query.getDisposalStatus()), RiskDisposal::getDisposalStatus, query.getDisposalStatus()) - .orderByDesc(RiskDisposal::getCreateTime); + .eq(Func.isNotEmpty(query.getDisposalStatus()), RiskDisposal::getDisposalStatus, query.getDisposalStatus()); + mpUserDataScopeSupport.applyWaybillProjectScopeIfMpDispatcher(wrapper, RiskDisposal::getWaybillId); + return wrapper.orderByDesc(RiskDisposal::getCreateTime); } private RiskDisposalVO toVO(RiskDisposal entity) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementAdjustmentServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementAdjustmentServiceImpl.java index 0f5b680b..f7dd22e3 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementAdjustmentServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementAdjustmentServiceImpl.java @@ -119,8 +119,9 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl tailPaymentSettlementIds = tailPaymentSettlementIds(rows.stream() .map(FormalSettlement::getId).filter(Objects::nonNull).toList()); + // 仅排除已完成收/付款、已发起尾款付款的正式结算单。 + // 注意:sourceType=「预结算合并」是由预结算转正式的常规来源,不是尾款单,不能据此过滤。 rows = rows.stream().filter(row -> !isSettlementCompleted(row) - && !isTailSettlement(row) && !tailPaymentSettlementIds.contains(row.getId())).toList(); List> result = new ArrayList<>(); for (FormalSettlement row : rows) { @@ -467,9 +468,6 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl 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) { return formalSettlementId != null && tailPaymentSettlementIds(List.of(formalSettlementId)) .contains(formalSettlementId); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementImportBatchServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementImportBatchServiceImpl.java index 31f0a8b0..6ec5bcd0 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementImportBatchServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementImportBatchServiceImpl.java @@ -76,6 +76,7 @@ import org.springblade.transport.service.IReceivablePayableDetailService; import org.springblade.transport.service.ISettlementImportBatchService; import org.springblade.transport.service.IWaybillImportBatchService; import org.springblade.transport.service.IWaybillService; +import com.fasterxml.jackson.core.type.TypeReference; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -220,12 +221,14 @@ public class SettlementImportBatchServiceImpl rows = buildRowsFromWaybills(listBatchWaybills(batch)); } vo.setRows(rows); + // 费用列名优先取库字段;若历史数据解析失败或缺失,从明细 feeItems 回补,避免草稿再打开丢列 + vo.setFeeItemNames(mergeFeeItemNames(vo.getFeeItemNames(), rows)); return vo; } @Override public void exportTemplate(HttpServletResponse response) { - SettlementImportTemplateExporter.export(response, loadTransportTypeLabels(), loadEnabledFeeItemNames()); + SettlementImportTemplateExporter.export(response, loadTransportTypeLabels()); } @Override @@ -867,21 +870,56 @@ public class SettlementImportBatchServiceImpl } private List resolveFeeItemNames(SettlementImportBatchRequest request) { + LinkedHashSet names = new LinkedHashSet<>(); 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 names = new LinkedHashSet<>(); if (Func.isNotEmpty(request.getRows())) { for (Map row : request.getRows()) { - Object feeItems = row.get("feeItems"); - if (feeItems instanceof Map raw) { - raw.keySet().stream().filter(Objects::nonNull).map(String::valueOf).forEach(names::add); - } + collectFeeItemNamesFromRow(row, names); } } return new ArrayList<>(names); } + /** 合并请求/库中的费用列名与明细行 feeItems 的键,保持插入顺序。 */ + private List mergeFeeItemNames(List preferred, List> rows) { + LinkedHashSet 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 row : rows) { + collectFeeItemNamesFromRow(row, names); + } + } + return new ArrayList<>(names); + } + + private void collectFeeItemNamesFromRow(Map row, Set 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> buildFeeRows(List> rows, List waybills) { List> primaryRows = primaryRows(rows); List> feeRows = new ArrayList<>(); @@ -1061,12 +1099,35 @@ public class SettlementImportBatchServiceImpl vo.setSettlementTypeName(settlementTypeName(batch.getSettlementType())); vo.setSettlementBillTypeName(settlementBillTypeName(batch.getSettlementBillType())); vo.setWaybillModeName(waybillModeName(batch.getWaybillMode())); - if (Func.isNotEmpty(batch.getFeeItemNamesJson())) { - vo.setFeeItemNames(JsonUtil.parseArray(batch.getFeeItemNamesJson(), String.class)); - } + vo.setFeeItemNames(parseFeeItemNamesJson(batch.getFeeItemNamesJson())); return vo; } + /** + * 解析费用列名 JSON。 + * 注意:不能用 JsonUtil.parseArray(..., String.class),其内部按 List<Map> 解析,字符串数组会失败导致草稿再打开丢费用列。 + */ + private List parseFeeItemNamesJson(String feeItemNamesJson) { + if (Func.isEmpty(feeItemNamesJson)) { + return List.of(); + } + try { + List names = JsonUtil.parse(feeItemNamesJson, new TypeReference>() { + }); + 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) { if (STATUS_DRAFT.equals(status)) { return "草稿"; @@ -1350,20 +1411,6 @@ public class SettlementImportBatchServiceImpl return new ArrayList<>(labels); } - private List loadEnabledFeeItemNames() { - R> 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 时,按关联运单回填查看明细。 */ private List> buildRowsFromWaybills(List waybills) { if (Func.isEmpty(waybills)) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportPlanServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportPlanServiceImpl.java index 34d9d35f..606e0c41 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportPlanServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportPlanServiceImpl.java @@ -601,6 +601,13 @@ public class TransportPlanServiceImpl extends BaseServiceImpl internals = internalRows(id); List externals = externalRows(id); + internals.forEach(this::clearOptionalDecimalSentinels); + externals.forEach(this::normalizeExternalOptionalDecimals); vo.setInternalDetails(internals); vo.setExternalDetails(externals); vo.setFeeSummary(feeSummary(vo.getFormalSettlementId(), externals)); @@ -298,6 +300,7 @@ public class TransportReconciliationServiceImpl List validExternals = new ArrayList<>(); for (int index = 0; index < rows.size(); index++) { VehicleReconciliationExcel row = rows.get(index); + normalizeImportVehicleDecimals(row); List validationErrors = validateImportVehicle(row); if (Func.isNotEmpty(validationErrors)) { // 原因同时写回原行:异常携带的是原表 rows, @@ -342,6 +345,7 @@ public class TransportReconciliationServiceImpl external.setExternalLineNo(index + 1); external.setActualDepartureTime(parseTime(row.getActualDepartureTime(), "实际发货时间")); external.setActualCompletionTime(parseTimeNullable(row.getActualCompletionTime(), "实际完成时间")); + normalizeExternalOptionalDecimals(external); Map feeItems = row.getFeeItems(); if (feeItems == null || feeItems.isEmpty()) { feeItems = new HashMap<>(); @@ -427,6 +431,7 @@ public class TransportReconciliationServiceImpl List validExternals = new ArrayList<>(); for (int index = 0; index < rows.size(); index++) { CargoReconciliationExcel row = rows.get(index); + normalizeImportCargoDecimals(row); List validationErrors = validateImportCargo(row); if (Func.isNotEmpty(validationErrors)) { // 原因同时写回原行:异常携带的是原表 rows, @@ -471,6 +476,7 @@ public class TransportReconciliationServiceImpl external.setExternalLineNo(index + 1); external.setActualDepartureTime(parseTime(row.getActualDepartureTime(), "实际发货时间")); external.setActualCompletionTime(parseTimeNullable(row.getActualCompletionTime(), "实际完成时间")); + normalizeExternalOptionalDecimals(external); Map feeItems = row.getFeeItems(); if (feeItems == null || feeItems.isEmpty()) { feeItems = new LinkedHashMap<>(); @@ -729,10 +735,12 @@ public class TransportReconciliationServiceImpl target.setModel(source.getModel()); target.setTransportQuantity(nonNegative(source.getTransportQuantity(), "运输量")); target.setQuantityUnit(source.getQuantityUnit()); - target.setMileage(source.getMileage()); + target.setMileage(normalizeOptionalDecimal(source.getMileage())); target.setBatchNo(source.getBatchNo()); - target.setUnitPrice(source.getUnitPrice() == null ? null : nonNegative(source.getUnitPrice(), "运输单价")); - target.setFreightAmount(source.getFreightAmount() == null ? null : nonNegative(source.getFreightAmount(), "运输费")); + BigDecimal unitPrice = normalizeOptionalDecimal(source.getUnitPrice()); + target.setUnitPrice(unitPrice == null ? null : nonNegative(unitPrice, "运输单价")); + BigDecimal freightAmount = normalizeOptionalDecimal(source.getFreightAmount()); + target.setFreightAmount(freightAmount == null ? null : nonNegative(freightAmount, "运输费")); target.setFeeItemsJson(source.getFeeItemsJson()); target.setSettlementAmount(nonNegative(source.getSettlementAmount(), "结算费用合计")); target.setMatchedInternalId(source.getMatchedInternalId()); @@ -824,7 +832,6 @@ public class TransportReconciliationServiceImpl applyAmount(bill, internal, external); copyExternalToInternal(internal, external); internalMapper.updateById(internal); - updateWaybill(internal, external); } refreshFormalSummaryFees(bill.getFormalSettlementId()); recalculateSettlement(bill); @@ -867,50 +874,14 @@ public class TransportReconciliationServiceImpl internal.setModel(external.getModel()); internal.setTransportQuantity(external.getTransportQuantity()); internal.setQuantityUnit(external.getQuantityUnit()); - internal.setMileage(external.getMileage()); + internal.setMileage(normalizeOptionalDecimal(external.getMileage())); internal.setBatchNo(external.getBatchNo()); - internal.setUnitPrice(external.getUnitPrice()); - internal.setFreightAmount(external.getFreightAmount()); + internal.setUnitPrice(normalizeOptionalDecimal(external.getUnitPrice())); + internal.setFreightAmount(normalizeOptionalDecimal(external.getFreightAmount())); internal.setFeeItemsJson(external.getFeeItemsJson()); 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.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 @Transactional(rollbackFor = Exception.class) public void complete(Long id) { @@ -1309,7 +1280,8 @@ public class TransportReconciliationServiceImpl bill.setInternalBillCount(internals.size()); bill.setExternalBillCount(externals.size()); int internalUnmatched = (int) internals.stream().filter(item -> !MATCHED.equals(item.getMatchResult())).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.setInternalQuantity(sumInternalQuantity(internals)); bill.setExternalQuantity(sumExternalQuantity(externals)); 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 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 BigDecimal sumInternalQuantity(List rows) { return rows.stream().map(TransportReconciliationInternal::getTransportQuantity).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); } private BigDecimal sumExternalQuantity(List rows) { return rows.stream().map(TransportReconciliationExternal::getTransportQuantity).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TripQueryServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TripQueryServiceImpl.java index 92b3962c..abf0fc92 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TripQueryServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TripQueryServiceImpl.java @@ -248,6 +248,7 @@ public class TripQueryServiceImpl implements ITripQueryService { .or().ne(Waybill::getDriverAcceptStatus, WaybillProcessSupport.ACCEPT_REJECTED)); applyScope(wrapper, condition); applyDeptScope(wrapper, condition); + applyProjectScope(wrapper, condition); applyOrgTypeFilter(wrapper, condition); applyKeyword(wrapper, condition.getKeyword()); List rows = waybillService.list(wrapper); @@ -268,6 +269,18 @@ public class TripQueryServiceImpl implements ITripQueryService { wrapper.in(Waybill::getDeptId, condition.getDeptIds()); } + /** + * 项目收口:条件里给了项目 ID 集合时只出这些项目的运单行,空集合表示不过滤。 + *

+ * 小程序调度端先按用户数据权限解析可见项目(业务部门在范围内),再把项目 ID 传进来。 + */ + private void applyProjectScope(LambdaQueryWrapper wrapper, TripQueryVO condition) { + if (Func.isEmpty(condition.getProjectIds())) { + return; + } + wrapper.in(Waybill::getProjectId, condition.getProjectIds()); + } + /** * 运输组织类型按子单行筛:它在同一趟车里必然是同一个值(配载的每张子单带同一个配载单号, * 未配载的每张自成一趟车),所以这一层不会把一趟车拆开。 @@ -807,6 +820,7 @@ public class TripQueryServiceImpl implements ITripQueryService { copy.setDriverPhone(source.getDriverPhone()); copy.setAllDrivers(source.isAllDrivers()); copy.setDeptIds(source.getDeptIds()); + copy.setProjectIds(source.getProjectIds()); copy.setKeyword(source.getKeyword()); copy.setTransportOrgType(source.getTransportOrgType()); copy.setException(source.getException()); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VoucherManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VoucherManageServiceImpl.java index e01c3fc7..3553602e 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VoucherManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VoucherManageServiceImpl.java @@ -36,7 +36,10 @@ import org.springblade.transport.service.IWaybillService; import org.springblade.transport.wrapper.VoucherManageWrapper; import org.springframework.stereotype.Service; 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.support.TransactionTemplate; import io.minio.MinioClient; import io.minio.GetPresignedObjectUrlArgs; @@ -86,6 +89,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl minioClientProvider; + private final TransactionTemplate requiresNewTransactionTemplate; private final String minioBucketName; private final String minioRootDirectory; private final String minioTransformEndpoint; @@ -98,6 +102,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl minioClientProvider, + PlatformTransactionManager transactionManager, @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.transform-endpoint:${minio.transform-endpoint:}}") String minioTransformEndpoint, @@ -111,6 +116,9 @@ public class VoucherManageServiceImpl extends BaseServiceImpl relatedWaybillIds = new HashSet<>(); + assertParsableZipArchive(voucher.getFileName(), voucher.getFileUrl()); Path archivePath = downloadSourceFile(voucher.getFileUrl()); try { + assertZipMagic(archivePath); Charset archiveCharset = detectArchiveCharset(archivePath); log.info("[凭证处理] 已识别压缩包文件名编码 voucherId={}, charset={}", voucherId, archiveCharset.name()); try (InputStream source = Files.newInputStream(archivePath); @@ -1235,6 +1245,9 @@ public class VoucherManageServiceImpl extends BaseServiceImpl { VoucherManage update = new VoucherManage(); - update.setId(voucher.getId()); + update.setId(voucherId); update.setProcessStatus("处理失败"); 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 压缩包,无法解析"); + } } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java index a3ca5791..be049adc 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java @@ -1084,8 +1084,9 @@ public class WaybillServiceImpl extends BaseServiceImpl waybill.setProcessJson(oldRecord.getProcessJson()); } } else { + // 新建手工运单不应带配载号;总单/计划调度写入的 masterNo、relationNo 必须保留, + // 否则总单进度按 masterNo 汇总会一直显示「已调度 0」。 waybill.setLoadingNo(null); - waybill.setMasterNo(null); waybill.setMileageRemark(null); } // 新建若已带业务状态(如复制提交已完成)则在过程校正后回写,保证与原运单一致; @@ -1623,7 +1624,12 @@ public class WaybillServiceImpl extends BaseServiceImpl BusinessRemoveResultVO result = new BusinessRemoveResultVO(); for (Waybill waybill : listByIds(idList)) { try { - complete(waybill.getId()); + // 待回单确认走确认回单校验(打卡节点 + 未了结异常);其它状态仍走普通完成 + if (WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM.equals(waybill.getBusinessStatus())) { + confirmReturnReceipt(waybill.getId()); + } else { + complete(waybill.getId()); + } result.setSuccessCount(result.getSuccessCount() + 1); } catch (Exception exception) { log.error("批量完成运单失败,waybillId:{}, waybillNo:{}, failureReason:{}", diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/LoadableAdmissionSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/LoadableAdmissionSupport.java index eb7d9a43..ba6c7ddd 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/LoadableAdmissionSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/LoadableAdmissionSupport.java @@ -29,7 +29,7 @@ import java.util.List; import java.util.Locale; /** - * 可配载准入判据:公路 + 待执行 + 未关联其它配载单。 + * 可配载准入判据:公路 + 待执行/进行中 + 未关联其它配载单。 *

* 接单状态不进判据:拒单票照样可配载。配载提交会把它的接单事实清成待接单, * 否则新司机看不到这张票——司机端入口一律排除已拒绝的运单。 @@ -38,8 +38,11 @@ import java.util.Locale; */ public final class LoadableAdmissionSupport { - /** 可配载只放行待执行:已完成、已取消、草稿与调度中间态一律不可配载 */ - public static final String LOADABLE_BUSINESS_STATUS = WaybillProcessSupport.STATUS_PENDING; + /** 可配载业务状态:待执行、进行中;已完成、已取消、草稿与调度中间态一律不可配载 */ + public static final List LOADABLE_BUSINESS_STATUSES = List.of( + WaybillProcessSupport.STATUS_PENDING, + WaybillProcessSupport.STATUS_RUNNING + ); /** * 公路运输方式的同义写法,按「包含」比对,与前端运单管理页的识别口径一致。 @@ -56,7 +59,7 @@ public final class LoadableAdmissionSupport { } /** - * 不可配载的原因,按判定顺序排列:公路 → 待执行 → 未关联其它配载单。 + * 不可配载的原因,按判定顺序排列:公路 → 待执行/进行中 → 未关联其它配载单。 *

* 判据只产出原因,中文文案由调用方映射——面向用户的措辞不进这一层。 */ @@ -65,8 +68,8 @@ public final class LoadableAdmissionSupport { /** 运输方式不是公路 */ NOT_ROAD, - /** 业务状态不是待执行 */ - NOT_PENDING, + /** 业务状态不是待执行或进行中 */ + NOT_LOADABLE_STATUS, /** 已关联其它配载单 */ ON_OTHER_MANIFEST @@ -119,8 +122,8 @@ public final class LoadableAdmissionSupport { if (waybill == null || !isRoadTransport(waybill.getTransportType())) { return UnloadableReason.NOT_ROAD; } - if (!LOADABLE_BUSINESS_STATUS.equals(text(waybill.getBusinessStatus()))) { - return UnloadableReason.NOT_PENDING; + if (!LOADABLE_BUSINESS_STATUSES.contains(text(waybill.getBusinessStatus()))) { + return UnloadableReason.NOT_LOADABLE_STATUS; } String loadingNo = text(waybill.getLoadingNo()); if (loadingNo.isEmpty() || loadingNo.equals(text(ownLoadingNo))) { @@ -148,13 +151,13 @@ public final class LoadableAdmissionSupport { } /** - * 把整条可配载判据加到运单查询上:公路 + 待执行 + 未配载。 + * 把整条可配载判据加到运单查询上:公路 + 待执行/进行中 + 未配载。 * * @param wrapper 运单列表查询条件 */ public static void applyLoadableConditions(LambdaQueryWrapper 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, "")); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/MpUserDataScopeSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/MpUserDataScopeSupport.java new file mode 100644 index 00000000..8336292a --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/MpUserDataScopeSupport.java @@ -0,0 +1,184 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * 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; + +/** + * 小程序调度端数据权限。 + *

+ * 口径:所属组织 + 数据权限范围 + 数据层级范围 → 可见组织 → + * 业务部门落在可见组织内的项目 → 再按这些项目收口运单/异常/风险。 + * + * @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 result = mpUserClient.resolveDataScope(userId); + if (result == null || !result.isSuccess() || result.getData() == null) { + return MpUserDataScopeVO.empty(); + } + return result.getData(); + } + + /** + * 转为查询用的组织 ID 约定:空=不过滤;非空=in;无权限=[-1]。 + */ + public List 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(业务部门落在数据权限组织范围内)。 + *

    + *
  • 空列表 = 全部组织 / 不过滤
  • + *
  • 非空 = 仅这些项目
  • + *
  • 无权限或范围内无项目 = [-1]
  • + *
+ * 非小程序用户返回 null。 + */ + public List resolveVisibleProjectIdsForQuery() { + MpUserDataScopeVO scope = resolveCurrentMpUserScope(); + if (scope == null) { + return null; + } + return resolveProjectIds(scope); + } + + /** + * 按数据权限结果解析可见项目 ID。 + */ + public List resolveProjectIds(MpUserDataScopeVO scope) { + if (scope == null || scope.isAllDept()) { + return List.of(); + } + List deptIds = scope.getDeptIds(); + if (Func.isEmpty(deptIds)) { + return List.of(NONE_ID); + } + List projectList = projectApplyService.list(Wrappers.lambdaQuery() + .select(ProjectApply::getId) + .in(ProjectApply::getBusinessDeptId, deptIds)); + if (Func.isEmpty(projectList)) { + return List.of(NONE_ID); + } + List projectIds = projectList.stream() + .map(ProjectApply::getId) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + return Func.isEmpty(projectIds) ? List.of(NONE_ID) : projectIds; + } + + /** + * 当前为小程序调度用户时,按可见项目写入 project 列;非调度用户不改动。 + */ + public void applyProjectScopeIfMpDispatcher(LambdaQueryWrapper wrapper, SFunction projectColumn) { + if (!isMpDispatcher() || wrapper == null || projectColumn == null) { + return; + } + List projectIds = resolveVisibleProjectIdsForQuery(); + if (Func.isEmpty(projectIds)) { + return; + } + wrapper.in(projectColumn, projectIds); + } + + /** + * 风险等无 projectId 字段时:按关联运单的项目收口。 + */ + public void applyWaybillProjectScopeIfMpDispatcher(LambdaQueryWrapper wrapper, SFunction waybillIdColumn) { + if (!isMpDispatcher() || wrapper == null || waybillIdColumn == null) { + return; + } + List 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 + ")"); + } + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/WaybillProcessSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/WaybillProcessSupport.java index 3b9d920e..ea2d81ee 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/WaybillProcessSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/WaybillProcessSupport.java @@ -229,7 +229,7 @@ public final class WaybillProcessSupport { *

* 需要接单(接单节点 confirmMode=yes)且尚未接单(未响应 / 已拒绝)→ pending(待执行); * 已接单 → running(进行中);不需要接单 → running(仅当当前为空或 pending 时提升)。 - * draft / completed / cancelled 等终态或调度中间态不改动。 + * draft / completed / cancelled / pending_return 等终态或调度中间态不改动。 */ public static String normalizeBusinessStatus(String businessStatus, String processJson) { return normalizeBusinessStatus(businessStatus, processJson, null); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/FormalSettlementWrapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/FormalSettlementWrapper.java index 68142f7d..72c86191 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/FormalSettlementWrapper.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/FormalSettlementWrapper.java @@ -10,6 +10,9 @@ package org.springblade.transport.wrapper; import org.springblade.core.mp.support.BaseEntityWrapper; 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.transport.pojo.entity.FormalSettlement; import org.springblade.transport.pojo.vo.FormalSettlementVO; @@ -46,6 +49,28 @@ public class FormalSettlementWrapper extends BaseEntityWrapper entity.getApprovalStatus(); }); vo.setAdjustStatusName("adjusted".equals(entity.getAdjustStatus()) ? "已调整" : null); + vo.setBusinessModelName(dictBizName("business_model", entity.getBusinessModel())); + vo.setTransportTypeName(dictBizName("transport_type", entity.getTransportType())); 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; + } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/PreSettlementWrapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/PreSettlementWrapper.java index 6832c407..7defb71d 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/PreSettlementWrapper.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/PreSettlementWrapper.java @@ -10,6 +10,9 @@ package org.springblade.transport.wrapper; import org.springblade.core.mp.support.BaseEntityWrapper; 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.transport.pojo.entity.PreSettlement; import org.springblade.transport.pojo.vo.PreSettlementVO; @@ -34,6 +37,8 @@ public class PreSettlementWrapper extends BaseEntityWrapper(1, 10)); } diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ExceptionDisposalRejectRevokeTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ExceptionDisposalRejectRevokeTest.java index f4e063dc..23aad81c 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ExceptionDisposalRejectRevokeTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ExceptionDisposalRejectRevokeTest.java @@ -68,7 +68,7 @@ class ExceptionDisposalRejectRevokeTest { followRecordMapper = mock(ExceptionDisposalFollowRecordMapper.class); service = new ExceptionDisposalServiceImpl( followRecordMapper, mock(IWaybillService.class), mock(IDriverWaybillService.class), - mock(LoadingManageMapper.class)); + mock(LoadingManageMapper.class), mock(org.springblade.transport.support.MpUserDataScopeSupport.class)); inject("baseMapper", disposalMapper); when(disposalMapper.updateById(any(ExceptionDisposal.class))).thenReturn(1); when(followRecordMapper.insert(any(ExceptionDisposalFollowRecord.class))).thenReturn(1); diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ExceptionDisposalReportGateTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ExceptionDisposalReportGateTest.java index 1b13ec03..b0c07b35 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ExceptionDisposalReportGateTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ExceptionDisposalReportGateTest.java @@ -73,7 +73,8 @@ class ExceptionDisposalReportGateTest { driverWaybillService = mock(IDriverWaybillService.class); service = new ExceptionDisposalServiceImpl( mock(ExceptionDisposalFollowRecordMapper.class), waybillService, driverWaybillService, - mock(LoadingManageMapper.class)); + mock(LoadingManageMapper.class), + mock(org.springblade.transport.support.MpUserDataScopeSupport.class)); inject("baseMapper", disposalMapper); when(waybillService.getById(WAYBILL_ID)).thenReturn(waybill()); diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ExceptionDisposalRouteAdjustTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ExceptionDisposalRouteAdjustTest.java index 04b66e6d..eb658339 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ExceptionDisposalRouteAdjustTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ExceptionDisposalRouteAdjustTest.java @@ -87,7 +87,8 @@ class ExceptionDisposalRouteAdjustTest { waybillService = mock(IWaybillService.class); loadingManageMapper = mock(LoadingManageMapper.class); service = new ExceptionDisposalServiceImpl( - followRecordMapper, waybillService, mock(IDriverWaybillService.class), loadingManageMapper); + followRecordMapper, waybillService, mock(IDriverWaybillService.class), loadingManageMapper, + mock(org.springblade.transport.support.MpUserDataScopeSupport.class)); inject("baseMapper", disposalMapper); when(disposalMapper.updateById(any(ExceptionDisposal.class))).thenReturn(1); when(followRecordMapper.insert(any(ExceptionDisposalFollowRecord.class))).thenReturn(1); diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/LoadingManageChildStatusWriteTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/LoadingManageChildStatusWriteTest.java index 52ab19f9..6de2e6e7 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/LoadingManageChildStatusWriteTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/LoadingManageChildStatusWriteTest.java @@ -139,13 +139,14 @@ class LoadingManageChildStatusWriteTest { } @Test - @DisplayName("不需接单的项目:提交时子单直接落进行中,不留待执行等读路径去提升") + @DisplayName("不需接单的项目:提交时配载单与子单均为进行中") void submitWritesRunningForProjectWithoutAcceptNode() { stubWaybills(waybill(NO_ACCEPT_WAYBILL_ID, NO_ACCEPT_PROJECT_ID)); stubConfigs(config(NO_ACCEPT_PROJECT_ID, NO_ACCEPT_JSON)); assertTrue(service.submit(loadingForm(NO_ACCEPT_WAYBILL_ID))); + assertEquals("running", capturedLoadingStatus()); CapturedUpdate statusUpdate = statusSyncUpdates().get(0); assertEquals("running", statusUpdate.setValue("business_status")); assertEquals(List.of(NO_ACCEPT_WAYBILL_ID), statusUpdate.whereIds()); @@ -153,13 +154,14 @@ class LoadingManageChildStatusWriteTest { } @Test - @DisplayName("需接单的项目:提交时子单落待执行,等新司机接单") + @DisplayName("需接单的项目:提交时配载单与子单均为待执行,等新司机接单") void submitWritesPendingForProjectWithAcceptNode() { stubWaybills(waybill(NEED_ACCEPT_WAYBILL_ID, NEED_ACCEPT_PROJECT_ID)); stubConfigs(config(NEED_ACCEPT_PROJECT_ID, NEED_ACCEPT_JSON)); assertTrue(service.submit(loadingForm(NEED_ACCEPT_WAYBILL_ID))); + assertEquals("pending", capturedLoadingStatus()); assertEquals("pending", statusSyncUpdates().get(0).setValue("business_status")); } @@ -367,12 +369,18 @@ class LoadingManageChildStatusWriteTest { waybill.setProjectId(projectId); waybill.setProcessJson(processJson); waybill.setDeptId(DEPT_ID_LONG); - // 可配载只放行公路 + 待执行 + 未配载,夹具的票都是这种票 + // 可配载放行公路 + 待执行/进行中 + 未配载,夹具默认待执行 waybill.setTransportType("公路运输"); waybill.setBusinessStatus("pending"); return waybill; } + private String capturedLoadingStatus() { + ArgumentCaptor captor = ArgumentCaptor.forClass(LoadingManage.class); + verify(service, atLeastOnce()).saveOrUpdate(captor.capture()); + return captor.getValue().getBusinessStatus(); + } + private void stubWaybills(Waybill... waybills) { when(waybillMapper.selectList(ArgumentMatchers.>any())).thenReturn(List.of(waybills)); } diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageAdminHomeFeedTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageAdminHomeFeedTest.java index 3b37513b..a0228fcd 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageAdminHomeFeedTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageAdminHomeFeedTest.java @@ -84,7 +84,8 @@ class ManageAdminHomeFeedTest { mock(IRiskDisposalService.class), mock(IDriverWaybillService.class), mock(ITripQueryService.class), - mock(ILoadingManageService.class)); + mock(ILoadingManageService.class), + mock(org.springblade.transport.support.MpUserDataScopeSupport.class)); when(exceptionService.list(ArgumentMatchers.>any())) .thenReturn(new ArrayList<>()); when(waybillService.list(ArgumentMatchers.>any())) diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageTripListDetailTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageTripListDetailTest.java index b5771621..8f3f26d6 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageTripListDetailTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageTripListDetailTest.java @@ -101,7 +101,8 @@ class ManageTripListDetailTest { mock(IRiskDisposalService.class), driverWaybillService, tripQueryService, - loadingManageService); + loadingManageService, + mock(org.springblade.transport.support.MpUserDataScopeSupport.class)); when(exceptionDisposalService.list(ArgumentMatchers.>any())) .thenReturn(new ArrayList<>()); when(waybillService.list(ArgumentMatchers.>any())).thenReturn(new ArrayList<>()); diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageWaybillBusinessStatusTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageWaybillBusinessStatusTest.java index ad43545b..5a0efd11 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageWaybillBusinessStatusTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageWaybillBusinessStatusTest.java @@ -65,7 +65,8 @@ class ManageWaybillBusinessStatusTest { mock(IRiskDisposalService.class), mock(IDriverWaybillService.class), mock(ITripQueryService.class), - mock(ILoadingManageService.class)); + mock(ILoadingManageService.class), + mock(org.springblade.transport.support.MpUserDataScopeSupport.class)); when(exceptionDisposalService.list(anyWrapper())).thenReturn(new ArrayList<>()); } diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageWaybillOrgScopeTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageWaybillOrgScopeTest.java index 95346246..ad8d8b48 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageWaybillOrgScopeTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageWaybillOrgScopeTest.java @@ -29,6 +29,7 @@ import org.springblade.transport.service.ILoadingManageService; import org.springblade.transport.service.IRiskDisposalService; import org.springblade.transport.service.ITripQueryService; import org.springblade.transport.service.IWaybillService; +import org.springblade.transport.support.MpUserDataScopeSupport; import java.util.ArrayList; import java.util.HashSet; @@ -80,6 +81,8 @@ class ManageWaybillOrgScopeTest { waybillService = mock(IWaybillService.class); tripQueryService = mock(ITripQueryService.class); IExceptionDisposalService exceptionDisposalService = mock(IExceptionDisposalService.class); + MpUserDataScopeSupport mpUserDataScopeSupport = mock(MpUserDataScopeSupport.class); + when(mpUserDataScopeSupport.resolveVisibleProjectIdsForQuery()).thenReturn(null); service = new ManageWaybillServiceImpl( waybillService, mock(IDriverService.class), @@ -87,7 +90,8 @@ class ManageWaybillOrgScopeTest { mock(IRiskDisposalService.class), mock(IDriverWaybillService.class), tripQueryService, - mock(ILoadingManageService.class)); + mock(ILoadingManageService.class), + mpUserDataScopeSupport); when(exceptionDisposalService.list(ArgumentMatchers.>any())) .thenReturn(new ArrayList<>()); when(waybillService.list(ArgumentMatchers.>any())).thenReturn(new ArrayList<>()); diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/WaybillLoadableQueryTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/WaybillLoadableQueryTest.java index 54e6eacf..9a7b6813 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/WaybillLoadableQueryTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/WaybillLoadableQueryTest.java @@ -68,7 +68,7 @@ class WaybillLoadableQueryTest { } @Test - @DisplayName("开关开启:公路词表、待执行、未配载一次全部进 wrapper,接单状态不参与") + @DisplayName("开关开启:公路词表、待执行/进行中、未配载一次全部进 wrapper,接单状态不参与") void loadableSwitchAppliesWholePredicate() { LambdaQueryWrapper wrapper = capturedQuery(loadableQuery()); String sql = String.valueOf(wrapper.getTargetSql()); @@ -79,10 +79,8 @@ class WaybillLoadableQueryTest { assertTrue(params.contains("%" + keyword + "%"), "公路词表要整份进条件,只写 road 会漏掉中文取值的运单:" + params); } - assertTrue(sql.contains("business_status =") && params.contains("pending"), - "只放行待执行:" + sql + " " + params); - assertFalse(params.contains("running"), - "进行中一律不可配载,不能沿用「待执行与进行中都可配载」的旧口径:" + params); + assertTrue(sql.contains("business_status") && params.contains("pending") && params.contains("running"), + "放行待执行与进行中:" + sql + " " + params); assertTrue(sql.contains("loading_no IS NULL") && sql.contains("loading_no ="), "未配载要同时覆盖 NULL 与空串:" + sql); assertFalse(sql.contains("driver_accept_status"), diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/support/LoadableAdmissionSupportTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/support/LoadableAdmissionSupportTest.java index 9b5c72ad..cb6cb597 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/support/LoadableAdmissionSupportTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/support/LoadableAdmissionSupportTest.java @@ -50,10 +50,12 @@ class LoadableAdmissionSupportTest { // ------------------------------------------------------------ 单张运单准入 @Test - @DisplayName("公路 + 待执行 + 未配载才可配载") + @DisplayName("公路 + 待执行/进行中 + 未配载才可配载") void loadableOnlyWhenAllConditionsHold() { assertTrue(LoadableAdmissionSupport.isLoadable(waybill("road", "pending", null, "pending"), null)); assertTrue(LoadableAdmissionSupport.isLoadable(waybill("公路运输", "pending", "", null), null)); + assertTrue(LoadableAdmissionSupport.isLoadable(waybill("road", "running", null, null), null), + "进行中也可配载"); } @Test @@ -64,12 +66,10 @@ class LoadableAdmissionSupportTest { } @Test - @DisplayName("非公路、非待执行、已关联配载单各自单独即可拒绝") + @DisplayName("非公路、非待执行/进行中、已关联配载单各自单独即可拒绝") void eachConditionRejectsOnItsOwn() { 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", "completed", null, null), null)); assertFalse(LoadableAdmissionSupport.isLoadable(waybill("road", "cancelled", null, null), null)); @@ -79,18 +79,19 @@ class LoadableAdmissionSupportTest { } @Test - @DisplayName("不可配载原因按 公路 → 待执行 → 未关联其它配载单 取第一条") + @DisplayName("不可配载原因按 公路 → 待执行/进行中 → 未关联其它配载单 取第一条") void unloadableReasonFollowsPredicateOrder() { assertEquals(LoadableAdmissionSupport.UnloadableReason.NOT_ROAD, LoadableAdmissionSupport.unloadableReason( - waybill("railway", "running", "PZ20260101-002", null), "PZ20260101-001")); - assertEquals(LoadableAdmissionSupport.UnloadableReason.NOT_PENDING, + waybill("railway", "completed", "PZ20260101-002", null), "PZ20260101-001")); + assertEquals(LoadableAdmissionSupport.UnloadableReason.NOT_LOADABLE_STATUS, LoadableAdmissionSupport.unloadableReason( - waybill("road", "running", "PZ20260101-002", null), "PZ20260101-001")); + waybill("road", "completed", "PZ20260101-002", null), "PZ20260101-001")); assertEquals(LoadableAdmissionSupport.UnloadableReason.ON_OTHER_MANIFEST, LoadableAdmissionSupport.unloadableReason( waybill("road", "pending", "PZ20260101-002", null), "PZ20260101-001")); assertNull(LoadableAdmissionSupport.unloadableReason(waybill("road", "pending", null, null), null)); + assertNull(LoadableAdmissionSupport.unloadableReason(waybill("road", "running", null, null), null)); assertNull(LoadableAdmissionSupport.unloadableReason( waybill("road", "pending", "PZ20260101-001", null), "PZ20260101-001")); } diff --git a/doc/sql/bladex/bladex.mysql.all.create.sql b/doc/sql/bladex/bladex.mysql.all.create.sql index fed3b1b5..0d8125ed 100644 --- a/doc/sql/bladex/bladex.mysql.all.create.sql +++ b/doc/sql/bladex/bladex.mysql.all.create.sql @@ -311,6 +311,7 @@ CREATE TABLE `blade_dept` ( `mnemonic_code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '助记码', `carrier_customer_id` bigint NULL DEFAULT NULL COMMENT '承运商客商档案主键', `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_sup_sub_com_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'OA上级主键', `sort` int NULL DEFAULT NULL COMMENT '排序', diff --git a/doc/sql/transport/blade_credit_score_quantification.sql b/doc/sql/transport/blade_credit_score_quantification.sql index ed9a8bdd..4bc6df40 100644 --- a/doc/sql/transport/blade_credit_score_quantification.sql +++ b/doc/sql/transport/blade_credit_score_quantification.sql @@ -8,6 +8,7 @@ CREATE TABLE `blade_credit_score_quantification` ( `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 '备注', `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_dept` bigint NULL DEFAULT NULL COMMENT '创建部门', `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', diff --git a/doc/sql/transport/blade_credit_score_quantification_export_insert.sql b/doc/sql/transport/blade_credit_score_quantification_export_insert.sql new file mode 100644 index 00000000..d15ae98d --- /dev/null +++ b/doc/sql/transport/blade_credit_score_quantification_export_insert.sql @@ -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; diff --git a/doc/sql/transport/blade_credit_score_quantification_max_minus_score_patch_20261023.sql b/doc/sql/transport/blade_credit_score_quantification_max_minus_score_patch_20261023.sql new file mode 100644 index 00000000..9425b13a --- /dev/null +++ b/doc/sql/transport/blade_credit_score_quantification_max_minus_score_patch_20261023.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`; diff --git a/doc/sql/transport/blade_customer_archive.sql b/doc/sql/transport/blade_customer_archive.sql index b1f584b8..0cec14d9 100644 --- a/doc/sql/transport/blade_customer_archive.sql +++ b/doc/sql/transport/blade_customer_archive.sql @@ -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_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 '法人/负责人', - `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_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 '所属组织', `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是', `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 '营业期限截止日', `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 '客商负责人', `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 '客户等级', `max_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 '备注', `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正式', diff --git a/doc/sql/transport/blade_customer_archive_paid_in_used_credit_20261022.sql b/doc/sql/transport/blade_customer_archive_paid_in_used_credit_20261022.sql new file mode 100644 index 00000000..08557073 --- /dev/null +++ b/doc/sql/transport/blade_customer_archive_paid_in_used_credit_20261022.sql @@ -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 '联系电话'; diff --git a/doc/sql/transport/blade_dept_is_business_dept_20261022.sql b/doc/sql/transport/blade_dept_is_business_dept_20261022.sql new file mode 100644 index 00000000..da6793f8 --- /dev/null +++ b/doc/sql/transport/blade_dept_is_business_dept_20261022.sql @@ -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`; diff --git a/doc/sql/transport/blade_formal_settlement_20260818.sql b/doc/sql/transport/blade_formal_settlement_20260818.sql index 240eafcb..f3777be3 100644 --- a/doc/sql/transport/blade_formal_settlement_20260818.sql +++ b/doc/sql/transport/blade_formal_settlement_20260818.sql @@ -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, `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', + `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, `applied_payment_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '申请付款金额含预付', `paid_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '已收已付合计含预付', diff --git a/doc/sql/transport/blade_mp_user_20261012.sql b/doc/sql/transport/blade_mp_user_20261012.sql index 9d34d49c..32e2d94a 100644 --- a/doc/sql/transport/blade_mp_user_20261012.sql +++ b/doc/sql/transport/blade_mp_user_20261012.sql @@ -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', `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', + `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', `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', `create_user` bigint NULL DEFAULT NULL COMMENT '创建人', diff --git a/doc/sql/transport/blade_mp_user_data_scope_20261021.sql b/doc/sql/transport/blade_mp_user_data_scope_20261021.sql new file mode 100644 index 00000000..5eba53c1 --- /dev/null +++ b/doc/sql/transport/blade_mp_user_data_scope_20261021.sql @@ -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='小程序用户组织数据权限'; diff --git a/doc/sql/transport/blade_pre_settlement_20260818.sql b/doc/sql/transport/blade_pre_settlement_20260818.sql index 6f20b639..bcbfb22e 100644 --- a/doc/sql/transport/blade_pre_settlement_20260818.sql +++ b/doc/sql/transport/blade_pre_settlement_20260818.sql @@ -24,6 +24,8 @@ CREATE TABLE IF NOT EXISTS `blade_pre_settlement` ( `payee_name` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '收款方', `currency` varchar(20) COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'RMB' 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_settlement_amount` decimal(18,2) DEFAULT NULL COMMENT '本位币合计', `exchange_rate_date` date DEFAULT NULL COMMENT '汇率日期', diff --git a/doc/sql/transport/blade_settlement_business_model_transport_type_20261021.sql b/doc/sql/transport/blade_settlement_business_model_transport_type_20261021.sql new file mode 100644 index 00000000..376a90c8 --- /dev/null +++ b/doc/sql/transport/blade_settlement_business_model_transport_type_20261021.sql @@ -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`; diff --git a/doc/sql/transport/stat_account_module_create_count.sql b/doc/sql/transport/stat_account_module_create_count.sql index 6cc9a5b2..a0233b35 100644 --- a/doc/sql/transport/stat_account_module_create_count.sql +++ b/doc/sql/transport/stat_account_module_create_count.sql @@ -7,8 +7,12 @@ -- 3. 只统计各功能列表对应的主表。随主单保存的明细、联系人、附件、变更流水不重复计数。 -- 4. 含已逻辑删除的数据,因为这些记录确实被创建过。 -- 5. 客商档案菜单挂在「车/船务管理」下,这里按统计口径单独计入「客商管理」。 --- 6. 未纳入:结算管理、首付款管理、在途管理,以及系统管理下的开票项目。 +-- 6. 未纳入:在途管理,以及系统管理下的开票项目。 -- 7. 导入运单产生的运单计入「业务模块-运单管理」,不另计导入批次。 +-- 导入结算单产生的正式结算单计入「结算管理」的付款结算单或收款结算单,不另计导入批次。 +-- 8. 付款结算单、收款结算单共用 blade_formal_settlement,按 settlement_type 区分(payable/receivable)。 +-- 应收、应付共用 blade_receivable_payable_detail,同样按 settlement_type 区分。 +-- 9. 菜单名称为「收付款管理」,统计模块名与菜单一致。 SET NAMES utf8mb4; @@ -396,7 +400,124 @@ SELECT t.create_user, 8, '运力模块', 4, '车辆调度', 'blade_transport_veh FROM blade_transport_vehicle_dispatch 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; +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 为空表示库中没有该账号。 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(d.cnt), 0) AS `合计` FROM tmp_stat_account a LEFT JOIN tmp_stat_user u ON u.account = a.account