🔀 合并主干 master 到 dev

- 状态命名统一用 dev 的 pending_return_confirm:master 侧 pending_return 常量、字面量与文案分支一并改名,避免两套状态并存
- 汇票可选按 master 放开:去掉部门/收付款方限制,删除随之失效的 selectableBill 判定
- 结算导入模板按 master 重写:不预置费用项列、不再有结算金额列,导出签名收为两参
- 合入 master 的待确认回单批量完成、变更记录审批过滤、小程序数据权限(MpUserDataScope)与评分量化/客商档案改动
- 受影响的测试构造器补上 MpUserDataScopeSupport 依赖
This commit is contained in:
2026-10-24 16:37:36 +08:00
86 changed files with 2379 additions and 357 deletions
@@ -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主键,仅同步记录,不对外展示
*/
@@ -26,6 +26,10 @@ public class FormalSettlementSaveRequest implements Serializable {
private List<Long> sourcePreSettlementIds;
private List<Long> sourceDetailIds;
private List<DetailAdjustment> detailAdjustments;
/** 业务模式(业务字典 business_model) */
private String businessModel;
/** 运输方式(业务字典 transport_type) */
private String transportType;
private LocalDate exchangeRateDate;
private BigDecimal exchangeRate;
private String attachmentsJson;
@@ -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;
@@ -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;
@@ -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;
}
@@ -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;
@@ -88,6 +88,14 @@ public class FormalSettlement extends TenantEntity {
* 结算金额(结算币种,含全部合计费用)
*/
private BigDecimal settlementAmount;
/**
* 业务模式(业务字典 business_model)
*/
private String businessModel;
/**
* 运输方式(业务字典 transport_type)
*/
private String transportType;
/**
* 本位币,默认 RMB
*/
@@ -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;
@@ -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<CreditRatingStandardVO> standards = new ArrayList<>();
/**
* 来自评分量化表的减分项目最高扣分限制(非落库字段,供前端计分使用)
*/
@TableField(exist = false)
@Schema(description = "减分项目最高扣分限制")
private BigDecimal maxMinusScore;
}
@@ -43,6 +43,8 @@ public class FormalSettlementVO extends FormalSettlement implements KingdeeUnloc
@TableField(exist = false) private String ids;
@TableField(exist = false) private String 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;
@@ -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<PreSettlementDetail> details;
@@ -56,6 +56,9 @@ public class TripQueryVO implements Serializable {
@Schema(description = "可见组织 ID 集合:非空时只出这些组织的运单行,空集合表示不按组织过滤")
private List<Long> deptIds;
@Schema(description = "可见项目 ID 集合:非空时只出这些项目的运单行,空集合表示不按项目过滤(小程序调度端优先用此口径)")
private List<Long> projectIds;
@Schema(description = "状态:0待接单/1进行中/2已完成/3已取消,不传为全部(不含已取消)")
private Integer status;
@@ -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<MpUser> userById(@RequestParam("userId") Long userId);
/**
* 解析小程序用户可见组织(所属组织 + 数据权限范围 + 数据层级范围)
*/
@GetMapping(RESOLVE_DATA_SCOPE)
R<MpUserDataScopeVO> resolveDataScope(@RequestParam("userId") Long userId);
/**
* 按租户 + 手机号查询
*/
@@ -25,6 +25,7 @@
*/
package org.springblade.system.pojo.entity;
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<Long> dataScopeDeptIds;
/**
* 岗位id
*/
@@ -0,0 +1,44 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.system.pojo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 小程序用户组织数据权限
*
* @author Chill
*/
@Data
@TableName("blade_mp_user_data_scope")
@Schema(description = "小程序用户组织数据权限")
public class MpUserDataScope implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "主键")
private Long id;
@Schema(description = "小程序用户主键")
private Long userId;
@Schema(description = "组织主键")
private Long deptId;
}
@@ -0,0 +1,91 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.system.pojo.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* 小程序用户数据权限解析结果
* <p>
* allDept=true 表示全部组织(查询不过滤);否则按 deptIds 收口,空列表表示无可见组织。
*
* @author Chill
*/
@Data
@Schema(description = "小程序用户数据权限")
public class MpUserDataScopeVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 是否全部组织(不过滤)
*/
@Schema(description = "是否全部组织")
private boolean allDept;
/**
* 可见组织 ID;allDept=false 时生效,空表示无权限
*/
@Schema(description = "可见组织ID")
private List<Long> deptIds = new ArrayList<>();
/**
* 全部组织
*/
public static MpUserDataScopeVO all() {
MpUserDataScopeVO scope = new MpUserDataScopeVO();
scope.setAllDept(true);
return scope;
}
/**
* 无可见组织
*/
public static MpUserDataScopeVO empty() {
MpUserDataScopeVO scope = new MpUserDataScopeVO();
scope.setAllDept(false);
scope.setDeptIds(new ArrayList<>());
return scope;
}
/**
* 指定可见组织
*/
public static MpUserDataScopeVO of(List<Long> deptIds) {
MpUserDataScopeVO scope = new MpUserDataScopeVO();
scope.setAllDept(false);
scope.setDeptIds(deptIds == null ? new ArrayList<>() : new ArrayList<>(deptIds));
return scope;
}
}
@@ -42,6 +42,13 @@ public class MpUserVO extends MpUser {
@Serial
private static final long serialVersionUID = 1L;
/**
* 所属组织名称(逗号分隔)
*/
@TableField(exist = false)
@Schema(description = "所属组织名称")
private String deptName;
/**
* 身份名称(逗号分隔中文)
*/
@@ -260,6 +260,17 @@ public class DeptController extends BladeController {
return R.data(deptService.listPlatformCompany());
}
/**
* 业务部门下拉(是否业务部门=是)
*/
@PreAuth(AuthConstant.PERMIT_ALL)
@GetMapping("/business-dept-select")
@ApiOperationSupport(order = 12)
@Operation(summary = "业务部门下拉", description = "返回是否业务部门=是的部门列表")
public R<List<Dept>> businessDeptSelect() {
return R.data(deptService.listBusinessDept());
}
/**
* 获取部门的主管信息
*/
@@ -71,7 +71,8 @@ public class MpUserController extends BladeController {
@ApiOperationSupport(order = 1)
@Operation(summary = "详情", description = "传入id")
public R<MpUserVO> detail(MpUser mpUser) {
MpUser detail = mpUserService.getOne(Condition.getQueryWrapper(mpUser));
Long id = mpUser == null ? null : mpUser.getId();
MpUser detail = id == null ? null : mpUserService.detail(id);
if (detail == null) {
return R.fail("小程序用户不存在");
}
@@ -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<MpUserDataScopeVO> resolveDataScope(@RequestParam("userId") Long userId) {
return R.data(mpUserService.resolveDataScope(userId));
}
@Override
@GetMapping(USER_BY_PHONE)
public R<MpUser> userByPhone(String tenantId, String phone) {
@@ -20,6 +20,7 @@
<result column="mnemonic_code" property="mnemonicCode"/>
<result column="carrier_customer_id" property="carrierCustomerId"/>
<result column="is_platform_company" property="isPlatformCompany"/>
<result column="is_business_dept" property="isBusinessDept"/>
<result column="sort" property="sort"/>
<result column="remark" property="remark"/>
<result column="status" property="status"/>
@@ -43,6 +44,7 @@
<result column="mnemonic_code" property="mnemonicCode"/>
<result column="carrier_customer_id" property="carrierCustomerId"/>
<result column="is_platform_company" property="isPlatformCompany"/>
<result column="is_business_dept" property="isBusinessDept"/>
<result column="sort" property="sort"/>
<result column="remark" property="remark"/>
<result column="status" property="status"/>
@@ -0,0 +1,21 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springblade.system.pojo.entity.MpUserDataScope;
/**
* 小程序用户组织数据权限 Mapper
*
* @author Chill
*/
public interface MpUserDataScopeMapper extends BaseMapper<MpUserDataScope> {
}
@@ -106,6 +106,13 @@ public interface IDeptService extends IService<Dept> {
*/
List<Dept> listPlatformCompany();
/**
* 业务部门下拉(是否业务部门=是)
*
* @return 业务部门列表
*/
List<Dept> listBusinessDept();
/**
* 根据部门名称精确匹配获取部门ID集合
*
@@ -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<MpUser> {
*/
IPage<MpUser> selectMpUserPage(IPage<MpUser> page, MpUser query);
/**
* 详情(含自定义数据权限组织)
*/
MpUser detail(Long id);
/**
* 解析小程序用户可见组织(所属组织 + 数据权限范围 + 数据层级范围)
* <p>
* allDept=true 表示全部组织;否则 deptIds 为可见组织,空列表表示无权限。
*/
MpUserDataScopeVO resolveDataScope(Long userId);
/**
* 保存小程序用户
*/
@@ -337,6 +337,18 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
return list(queryWrapper);
}
@Override
public List<Dept> listBusinessDept() {
LambdaQueryWrapper<Dept> queryWrapper = Wrappers.<Dept>lambdaQuery()
.eq(Dept::getIsBusinessDept, 1)
.orderByAsc(Dept::getSort)
.orderByAsc(Dept::getId);
if (!AuthUtil.isAdministrator()) {
queryWrapper.eq(Dept::getTenantId, AuthUtil.getTenantId());
}
return list(queryWrapper);
}
@Override
public String getDeptIds(String tenantId, String deptNames) {
List<Dept> deptList = baseMapper.selectList(Wrappers.<Dept>query().lambda().eq(Dept::getTenantId, tenantId).in(Dept::getDeptName, Func.toStrList(deptNames)));
@@ -443,6 +455,7 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> 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<DeptMapper, Dept> 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<DeptMapper, Dept> 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<DeptMapper, Dept> 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<DeptMapper, Dept> 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<DeptMapper, Dept> 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) {
@@ -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<MpUserMapper, MpUser> 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<MpUserMapper, MpUser> 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<Long> baseDeptIds;
if (Objects.equals(dataScopeRange, DATA_SCOPE_CUSTOM)) {
baseDeptIds = mpUserDataScopeMapper.selectList(Wrappers.<MpUserDataScope>lambdaQuery()
.eq(MpUserDataScope::getUserId, userId))
.stream()
.map(MpUserDataScope::getDeptId)
.filter(Objects::nonNull)
.distinct()
.toList();
} else {
// 仅所属组织
baseDeptIds = Func.toLongList(user.getDeptId());
}
if (Func.isEmpty(baseDeptIds)) {
return MpUserDataScopeVO.empty();
}
boolean includeChildren = !Objects.equals(dataLevelRange, DATA_LEVEL_CURRENT_ONLY);
LinkedHashSet<Long> allowedDeptIds = new LinkedHashSet<>();
for (Long deptId : baseDeptIds) {
if (Func.isEmpty(deptId) || deptId <= 0) {
continue;
}
if (includeChildren) {
List<Long> childIds = SysCache.getDeptChildIds(deptId);
if (Func.isNotEmpty(childIds)) {
allowedDeptIds.addAll(childIds);
} else {
allowedDeptIds.add(deptId);
}
} else {
allowedDeptIds.add(deptId);
}
}
return MpUserDataScopeVO.of(List.copyOf(allowedDeptIds));
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean submit(MpUser user) {
@@ -223,7 +299,7 @@ public class MpUserServiceImpl extends BaseServiceImpl<MpUserMapper, MpUser> 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<MpUserMapper, MpUser> 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<MpUserMapper, MpUser> 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<MpUserMapper, MpUser> imp
}
boolean removed = deleteLogic(ids);
if (removed) {
mpUserDataScopeMapper.delete(Wrappers.<MpUserDataScope>lambdaQuery().in(MpUserDataScope::getUserId, ids));
MpUserCache.clear();
}
return removed;
@@ -422,6 +510,15 @@ public class MpUserServiceImpl extends BaseServiceImpl<MpUserMapper, MpUser> 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<MpUserMapper, MpUser> 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<MpUserMapper, MpUser> 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.<MpUserDataScope>lambdaQuery().eq(MpUserDataScope::getUserId, user.getId()));
if (Objects.equals(user.getDataScopeRange(), 3) && Func.isNotEmpty(user.getDataScopeDeptIds())) {
user.getDataScopeDeptIds().stream().filter(Objects::nonNull).distinct().forEach(deptId -> {
MpUserDataScope scope = new MpUserDataScope();
scope.setUserId(user.getId());
scope.setDeptId(deptId);
mpUserDataScopeMapper.insert(scope);
});
}
return true;
}
private MpUser fillScopes(MpUser user) {
if (user == null || Func.isEmpty(user.getId())) {
return user;
}
user.setDataScopeDeptIds(mpUserDataScopeMapper.selectList(Wrappers.<MpUserDataScope>lambdaQuery()
.eq(MpUserDataScope::getUserId, user.getId())).stream().map(MpUserDataScope::getDeptId).toList());
return user;
}
private String normalizeIdentities(String identities) {
@@ -485,4 +621,18 @@ public class MpUserServiceImpl extends BaseServiceImpl<MpUserMapper, MpUser> 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("*");
}
}
@@ -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<String, Dept> 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 相同也要更新。
@@ -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<MpUser, MpUserVO> {
// 密码哈希不下发前端
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()));
}
@@ -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()));
@@ -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()));
@@ -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("结算汇率")
@@ -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("结算汇率")
@@ -96,8 +96,7 @@ public final class SettlementImportTemplateExporter {
private SettlementImportTemplateExporter() {
}
public static void export(HttpServletResponse response, List<String> transportTypeOptions,
List<String> feeItemNames) {
public static void export(HttpServletResponse response, List<String> transportTypeOptions) {
List<String> headers = buildHeaders();
// 运输方式由调用方传入业务字典 transport_type 的字典名称,禁止写死
List<String> 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<String> transportOptions,
List<String> 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<String> 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);
@@ -33,13 +33,16 @@
<result column="business_scope" property="businessScope"/>
<result column="network_freight_platform" property="networkFreightPlatform"/>
<result column="business_term_type" property="businessTermType"/>
<result column="business_start_date" property="businessStartDate"/>
<result column="business_end_date" property="businessEndDate"/>
<result column="registered_capital" property="registeredCapital"/>
<result column="paid_in_capital" property="paidInCapital"/>
<result column="principal" property="principal"/>
<result column="mnemonic_code" property="mnemonicCode"/>
<result column="customer_level" property="customerLevel"/>
<result column="max_credit_limit" property="maxCreditLimit"/>
<result column="apply_credit_limit" property="applyCreditLimit"/>
<result column="used_credit_limit" property="usedCreditLimit"/>
<result column="remark" property="remark"/>
<result column="qualification_attachments" property="qualificationAttachments"/>
<result column="access_type" property="accessType"/>
@@ -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,
@@ -107,7 +107,7 @@ public class BillLedgerServiceImpl extends BaseServiceImpl<BillLedgerMapper, Bil
private List<BillLedgerVO> availableList(String keyword, Long deptId, Long selectedId, String payerName,
String payeeName) {
LocalDate today = LocalDate.now();
// 未到期且有余额;收票且部门可用,或出票/收票单位与结算收付款方一致(开票、收票都可)
// 可选汇票:未到期且可用余额 > 0(已选中的可回显);不再按部门、开票/收票类型、结算收付款方过滤
return list(Wrappers.<BillLedger>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<BillLedgerMapper, Bil
.or(selectedId != null, child -> 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<BillLedgerMapper, Bil
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
private boolean departmentAvailable(BillLedger ledger, Long deptId) {
List<?> values = parseArray(ledger.getAvailableDeptIdsJson(), "可用部门");
if (values.stream().anyMatch(value -> "all".equals(String.valueOf(value)))) return true;
return deptId == null || values.stream().anyMatch(value -> String.valueOf(deptId).equals(String.valueOf(value)));
}
private List<?> parseArray(String value, String name) {
if (Func.isEmpty(value)) return List.of();
try {
@@ -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 BaseServiceImpl<BillPaymentMapper, B
String deptName = required(SysCache.getDeptName(deptId), "使用部门", 100);
BillLedger ledger = lockedBill(request.getBillLedgerId());
BigDecimal usedAmount = positive(request.getUsedAmount(), "本次使用");
validateLedgerAmount(ledger, usedAmount, deptId);
validateLedgerAmount(ledger, usedAmount);
if (entity.getId() == null) {
entity.setPaymentNo(nextNo());
entity.setApprovalStatus(TransportBusiConstant.DRAFT);
@@ -165,7 +164,7 @@ public class BillPaymentServiceImpl extends BaseServiceImpl<BillPaymentMapper, B
throw new ServiceException("仅审批中的汇票付款允许审核");
}
BillLedger ledger = lockedBill(entity.getBillLedgerId());
validateLedgerAmount(ledger, entity.getUsedAmount(), entity.getDeptId());
validateLedgerAmount(ledger, entity.getUsedAmount());
BillLedgerUsage exists = usageMapper.selectOne(Wrappers.<BillLedgerUsage>lambdaQuery()
.eq(BillLedgerUsage::getBillPaymentId, entity.getId())
.eq(BillLedgerUsage::getUsageStatus, TransportBusiConstant.APPROVED)
@@ -245,13 +244,13 @@ public class BillPaymentServiceImpl extends BaseServiceImpl<BillPaymentMapper, B
private void validateStoredAmount(BillPayment entity) {
BillLedger ledger = lockedBill(entity.getBillLedgerId());
validateLedgerAmount(ledger, entity.getUsedAmount(), entity.getDeptId());
validateLedgerAmount(ledger, entity.getUsedAmount());
entity.setBillNo(ledger.getBillNo());
entity.setFaceAmount(ledger.getFaceAmount());
entity.setAvailableBalance(ledger.getAvailableBalance());
}
private void validateLedgerAmount(BillLedger ledger, BigDecimal usedAmount, Long deptId) {
private void validateLedgerAmount(BillLedger ledger, BigDecimal usedAmount) {
if (usedAmount == null || usedAmount.compareTo(BigDecimal.ZERO) <= 0) {
throw new ServiceException("本次使用必须大于0");
}
@@ -261,21 +260,6 @@ public class BillPaymentServiceImpl extends BaseServiceImpl<BillPaymentMapper, B
if (usedAmount.compareTo(money(ledger.getAvailableBalance())) > 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) {
@@ -83,6 +83,8 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
private static final String ARCHIVE_STATUS_PENDING = "未归档";
private static final String ARCHIVE_STATUS_ARCHIVED = "已归档";
private static final String FILE_TYPE_SEAL_ARCHIVE = "双章归档文件";
private static final String CHANGE_TYPE_INFO = "合同信息变更";
private static final String CHANGE_TYPE_SETTLEMENT = "结算规则变更";
private static final long TEMPORARY_VALIDITY_DAYS = 30L;
private static final BigDecimal TAX_RATE_MIN = BigDecimal.ZERO;
private static final BigDecimal TAX_RATE_MAX = BigDecimal.valueOf(100);
@@ -285,45 +287,99 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|| !List.of(STATUS_APPROVED, STATUS_CHANGE_APPROVED, STATUS_CHANGE_REJECTED).contains(source.getApprovalStatus())) {
throw new ServiceException("当前合同状态不允许发起变更");
}
TransportBusinessSupport.validateRequired(request.getSettlementCurrency(), "请选择结算币种");
String changeType = resolveChangeType(request.getChangeType());
boolean settlementRuleOnly = Objects.equals(changeType, CHANGE_TYPE_SETTLEMENT);
String changeReason = TransportBusinessSupport.trimToNull(request.getChangeReason());
if (Func.isEmpty(changeReason) && Objects.equals(source.getApprovalStatus(), STATUS_CHANGE_REJECTED)) {
changeReason = resolveResubmitChangeReason(source);
}
TransportBusinessSupport.validateLength(changeReason, 500, "变更原因不能超过500字");
ContractManage candidate = new ContractManage();
BeanUtil.copyProperties(source, candidate);
candidate.setPartyB(TransportBusinessSupport.trimToNull(request.getPartyB()));
validateContractPartyCombinationUnique(candidate);
validateBillingPlans(request.getBillingPlanJson());
validatePaymentRatios(request.getPaymentRatioJson());
Map<String, Object> 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<String, Object> afterData = buildContractChangeSnapshot(source);
retainChangedSnapshotFields(beforeData, afterData);
source.setApprovalStatus(STATUS_CHANGE_REVIEWING);
source.setChangeContent(TransportBusinessSupport.trimToNull(request.getChangeContent()));
source.setChangeReason(changeReason);
source.setChangeAttachmentsJson(TransportBusinessSupport.trimToNull(request.getChangeAttachmentsJson()));
source.setCurrentNode("合同变更审批");
source.setCurrentProcessor("待处理");
appendChangeRecord(source, "合同信息变更", 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) {
@@ -458,6 +458,10 @@ public class CreditScoreQuantificationServiceImpl extends BaseServiceImpl<Credit
quantification.setName(trimToEmpty(quantification.getName()));
quantification.setRemark(trimToNull(quantification.getRemark()));
quantification.setStandardDescription(trimToNull(quantification.getStandardDescription()));
if (quantification.getMaxMinusScore() != null
&& quantification.getMaxMinusScore().compareTo(BigDecimal.ZERO) < 0) {
throw new ServiceException("减分项目最高扣分限制不能小于0");
}
if (Func.isEmpty(quantification.getStatus())) {
quantification.setStatus(STATUS_DRAFT);
}
@@ -472,6 +476,10 @@ public class CreditScoreQuantificationServiceImpl extends BaseServiceImpl<Credit
validateLength(quantification.getName(), NAME_MAX_LENGTH, "评定表名称最多30字符");
validateLength(quantification.getRemark(), REMARK_MAX_LENGTH, "备注最多500字符");
validateLength(quantification.getStandardDescription(), REMARK_MAX_LENGTH, "标准说明最多500字符");
if (quantification.getMaxMinusScore() != null
&& quantification.getMaxMinusScore().compareTo(BigDecimal.ZERO) < 0) {
throw new ServiceException("减分项目最高扣分限制不能小于0");
}
LambdaQueryWrapper<CreditScoreQuantification> queryWrapper = Wrappers.<CreditScoreQuantification>lambdaQuery()
.eq(CreditScoreQuantification::getName, quantification.getName())
.eq(CreditScoreQuantification::getIsDeleted, 0);
@@ -280,6 +280,8 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
customer.setCustomerCode(nextCustomerCode());
}
CustomerArchive entity = Objects.requireNonNull(BeanUtil.copyProperties(customer, CustomerArchive.class));
// 曾审核通过后的评分替换:保存/再提交时仍沿用原生效等级与额度,待再次审核通过后再切换
this.keepEffectiveCreditUntilReapproved(before, entity);
boolean result = saveOrUpdate(entity);
customer.setId(entity.getId());
replaceDetail(entity.getId(), customer);
@@ -420,6 +422,8 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
after.setCurrentNode("审核通过");
after.setCurrentProcessor(Func.isEmpty(processorName) ? "系统" : processorName.trim());
after.setApprovedTime(LocalDateTime.now());
// 审核通过后按当前未删除评分刷新生效等级/额度(替换旧评分场景在此切换生效)
this.applyCreditFromActiveScore(after);
addChangeRecord(id, "客商准入审核通过", before, after);
boolean updated = updateById(after);
log.info("流程结束审核通过 id={} processor={} result={}", id, after.getCurrentProcessor(), updated);
@@ -484,6 +488,8 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
after.setCurrentNode("审核通过");
after.setCurrentProcessor(AuthUtil.getUserName());
after.setApprovedTime(LocalDateTime.now());
// 审核通过后按当前未删除评分刷新生效等级/额度(替换旧评分场景在此切换生效)
this.applyCreditFromActiveScore(after);
addChangeRecord(id, "客商准入审核通过", before, after);
return updateById(after);
}
@@ -943,6 +949,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
score.setScoreDate(LocalDate.now());
score.setSelfStatus("未完成");
score.setReviewStatus("未完成");
score.setMaxMinusScore(quantification.getMaxMinusScore());
score.setDetails(loadScoreTemplateDetails(quantification.getId()));
return score;
}
@@ -963,13 +970,16 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
customer.setBusinessScope(trimToNull(customer.getBusinessScope()));
customer.setInvoiceTaxRate(normalizeSentinelAmount(customer.getInvoiceTaxRate()));
customer.setRegisteredCapital(normalizeSentinelAmount(customer.getRegisteredCapital()));
customer.setPaidInCapital(normalizeSentinelAmount(customer.getPaidInCapital()));
customer.setMaxCreditLimit(normalizeSentinelAmount(customer.getMaxCreditLimit()));
customer.setApplyCreditLimit(normalizeSentinelAmount(customer.getApplyCreditLimit()));
customer.setUsedCreditLimit(normalizeSentinelAmount(customer.getUsedCreditLimit()));
customer.setRegisteredRegionName(trimToNull(customer.getRegisteredRegionName()));
customer.setRegisteredDetailAddress(trimToNull(customer.getRegisteredDetailAddress()));
customer.setRegisteredAddress(buildRegisteredAddress(customer));
customer.setLegalPerson(trimToEmpty(customer.getLegalPerson()));
customer.setContactPhone(trimToEmpty(customer.getContactPhone()));
customer.setContacts(customer.getContacts() == null ? new ArrayList<>() : 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<CustomerArchiveM
if (Func.isEmpty(customer.getStatus())) {
customer.setStatus(STATUS_ENABLED);
}
customer.setContacts(customer.getContacts() == null ? new ArrayList<>() : 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<CustomerContactVO> contacts = customer.getContacts();
if (Func.isEmpty(contacts)) {
customer.setContactPhone(null);
return;
}
String phone = contacts.stream()
.map(CustomerContactVO::getContactPhone)
.filter(Func::isNotEmpty)
.map(String::trim)
.findFirst()
.orElse(null);
customer.setContactPhone(phone);
}
private void validateBase(CustomerArchiveVO customer) {
if (Func.isEmpty(customer.getFullName())) {
throw new ServiceException("客商全称不能为空");
@@ -1009,10 +1040,8 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
if (Func.isEmpty(customer.getLegalPerson())) {
throw new ServiceException("法人/负责人不能为空");
}
if (Func.isEmpty(customer.getContactPhone())) {
throw new ServiceException("联系电话不能为空");
}
if (!customer.getContactPhone().matches("^1\\d{10}$")) {
validateContactsRequired(customer);
if (Func.isNotEmpty(customer.getContactPhone()) && !customer.getContactPhone().matches("^1\\d{10}$")) {
throw new ServiceException("联系电话必须为11位手机号");
}
if (Func.isEmpty(customer.getDeptName())) {
@@ -1028,16 +1057,30 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
validateLength(customer.getRegisteredRegionName(), 100, "省市区最多100个字符");
validateLength(customer.getRegisteredDetailAddress(), 255, "详细地址最多255个字符");
validateLength(customer.getRegisteredAddress(), 400, "地址最多400个字符");
validateLength(customer.getBusinessScope(), 500, "经营范围最多500个字符");
validateLength(customer.getBusinessScope(), 2000, "经营范围最多2000个字符");
validateLength(customer.getRemark(), 500, "备注最多500个字符");
validateNonNegative(customer.getInvoiceTaxRate(), "开票税点不能小于0");
validateMax(customer.getInvoiceTaxRate(), new BigDecimal("13"), "开票税点必须在0到13之间");
validateNonNegative(customer.getRegisteredCapital(), "注册资金不能小于0");
validateNonNegative(customer.getPaidInCapital(), "实缴资本不能小于0");
validateNonNegative(customer.getMaxCreditLimit(), "最大资金使用额度不能小于0");
validateNonNegative(customer.getApplyCreditLimit(), "申请总资金使用额度不能小于0");
validateNonNegative(customer.getUsedCreditLimit(), "已使用额度不能小于0");
validateUnique(customer);
}
/**
* 提交客商档案时必须至少维护一条联系人。
*/
private void validateContactsRequired(CustomerArchiveVO customer) {
List<CustomerContactVO> contacts = customer.getContacts() == null ? List.of() : customer.getContacts();
boolean hasContact = contacts.stream()
.anyMatch(item -> Func.isNotEmpty(item.getContactName()) || Func.isNotEmpty(item.getContactPhone()));
if (!hasContact) {
throw new ServiceException("请至少维护一条联系人");
}
}
private void validateCustomerType(String customerType) {
List<String> typeList = Func.toStrList(customerType.replace(",", ","))
.stream()
@@ -1364,6 +1407,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
}
private void deleteScores(List<Long> customerIds) {
// 逻辑删除保留历史评分明细;曾审核通过的旧评分在再次审核通过前仍通过主表等级/额度暂时生效
List<CustomerCreditScore> scores = creditScoreMapper.selectList(Wrappers.<CustomerCreditScore>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<CustomerArchiveM
}
}
/**
* 客商曾审核通过后,保存或再次提审时不覆盖主表已生效的等级/额度。
*/
private void keepEffectiveCreditUntilReapproved(CustomerArchive before, CustomerArchive entity) {
if (before == null || before.getApprovedTime() == null || entity == null) {
return;
}
entity.setCustomerLevel(before.getCustomerLevel());
entity.setMaxCreditLimit(before.getMaxCreditLimit());
entity.setApplyCreditLimit(before.getApplyCreditLimit());
}
/**
* 按当前未删除评分刷新客商生效等级与额度;优先取复评已完成的评分。
*/
private void applyCreditFromActiveScore(CustomerArchive customer) {
if (customer == null || customer.getId() == null) {
return;
}
List<CustomerCreditScoreVO> scores = loadScores(customer.getId());
if (Func.isEmpty(scores)) {
return;
}
CustomerCreditScoreVO activeScore = scores.stream()
.filter(score -> "已完成".equals(score.getReviewStatus()))
.findFirst()
.orElse(scores.get(0));
if (Func.isNotEmpty(activeScore.getCreditLevel())) {
customer.setCustomerLevel(activeScore.getCreditLevel());
}
if (activeScore.getMaxCreditLimit() != null) {
customer.setMaxCreditLimit(activeScore.getMaxCreditLimit());
customer.setApplyCreditLimit(activeScore.getMaxCreditLimit());
}
}
private List<CustomerContactVO> loadContacts(Long customerId) {
return contactMapper.selectList(Wrappers.<CustomerContact>lambdaQuery()
.eq(CustomerContact::getCustomerId, customerId)
@@ -2040,13 +2120,16 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
snapshot.put("开票税点", customer.getInvoiceTaxRate());
snapshot.put("经营范围", customer.getBusinessScope());
snapshot.put("营业期限类型", customer.getBusinessTermType());
snapshot.put("营业期限开始日", customer.getBusinessStartDate());
snapshot.put("营业期限截止日", customer.getBusinessEndDate());
snapshot.put("注册资金(万元)", customer.getRegisteredCapital());
snapshot.put("实缴资本(万元)", customer.getPaidInCapital());
snapshot.put("客商负责人", customer.getPrincipal());
snapshot.put("助记码", customer.getMnemonicCode());
snapshot.put("客户等级", customer.getCustomerLevel());
snapshot.put("最大资金使用额度(万元)", customer.getMaxCreditLimit());
snapshot.put("申请总资金使用额度(万元)", customer.getApplyCreditLimit());
snapshot.put("已使用额度(万元)", customer.getUsedCreditLimit());
snapshot.put("备注", customer.getRemark());
snapshot.put("客商材料", customer.getQualificationAttachments());
snapshot.put("准入类型", customer.getAccessType());
@@ -40,11 +40,14 @@ import org.springblade.thirdparty.bfi.pojo.dto.BfiCustomerSaveDTO;
import org.springblade.transport.config.BfiProperties;
import org.springblade.transport.constant.CustomerBfiConstants;
import org.springblade.transport.pojo.vo.CustomerArchiveVO;
import org.springblade.transport.pojo.vo.CustomerContactVO;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
@@ -86,12 +89,13 @@ public class CustomerBfiConverter {
param.setType(this.resolvePartnerType(customer.getCustomerNature()));
// 详细地址:注册地址(省市区 + 详细地址拼接文本)
param.setBizpartnerAddress(customer.getRegisteredAddress());
// 经营范围:多选编码 → 字典文本「、」拼接
// 经营范围:长文本直传;兼容历史多选编码→字典文本
param.setBusinessscope(this.buildBusinessScopeText(customer.getBusinessScope()));
// 营业期限:长期传「长期」,固定期限传「至 yyyy-MM-dd」(TMS 未采集起始日)
param.setBusinessterm(this.buildBusinessTerm(customer.getBusinessTermType(), customer.getBusinessEndDate()));
// 联系电话:客商联系电话
param.setBizpartnerPhone(customer.getContactPhone());
// 营业期限:长期传「长期」,固定期限传「yyyy-MM-dd 至 yyyy-MM-dd」
param.setBusinessterm(this.buildBusinessTerm(
customer.getBusinessTermType(), customer.getBusinessStartDate(), customer.getBusinessEndDate()));
// 联系电话:优先主表,空时回退首条联系人
param.setBizpartnerPhone(this.resolveContactPhone(customer));
// 备注:客商备注,空时不传
param.setFcusbeizhu(customer.getRemark());
// 业务职能:固定全选(1销售,2结算,3付款,4收货)
@@ -107,7 +111,7 @@ public class CustomerBfiConverter {
param.setEntryGroupstandard(List.of(groupEntry));
// 银行信息分录:收款账户明细(账户名称传开户行,币种默认 RMB)
param.setEntryBank(this.buildBankEntries(customer));
// 联系人信息分录:页面填写的客商负责人与联系电话
// 联系人信息分录:客商联系人明细(兼容历史负责人字段)
param.setEntryLinkman(this.buildLinkmanEntries(customer));
return param;
}
@@ -175,25 +179,58 @@ public class CustomerBfiConverter {
}
/**
* 组装 A902-2 联系人信息分录:页面填写的客商负责人与联系电话,未填负责人时返回空分录
* 组装 A902-2 联系人信息分录:优先推送客商联系人明细;无明细时兼容历史「负责人+联系电话」。
*
* @param customer 客商档案
* @return 联系人信息分录
*/
private List<BfiCustomerLinkmanEntryDTO> buildLinkmanEntries(CustomerArchiveVO customer) {
if (Func.isNotEmpty(customer.getContacts())) {
List<BfiCustomerLinkmanEntryDTO> entries = new ArrayList<>();
boolean defaultAssigned = false;
for (CustomerContactVO contact : customer.getContacts()) {
if (Func.isBlank(contact.getContactName()) && Func.isBlank(contact.getContactPhone())) {
continue;
}
BfiCustomerLinkmanEntryDTO entry = new BfiCustomerLinkmanEntryDTO();
entry.setContactperson(contact.getContactName());
entry.setPhone(contact.getContactPhone());
boolean isDefault = !defaultAssigned && (Objects.equals(contact.getIsDefault(), 1)
|| Func.isNotBlank(contact.getContactName()));
entry.setIsdefaultLinkman(isDefault);
if (isDefault) {
defaultAssigned = true;
}
entries.add(entry);
}
if (!entries.isEmpty()) {
return entries;
}
}
if (Func.isBlank(customer.getPrincipal())) {
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<DictBiz> dictList = DictBizCache.getList("scope_of_business");
Map<String, String> 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);
}
}
@@ -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);
}
@@ -350,9 +350,14 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
.eq(SettlementAdjustment::getFormalSettlementId, id)
.orderByDesc(SettlementAdjustment::getCreateTime)).stream()
.map(this::toAdjustmentVO).toList());
vo.setChangeRecords(changeRecordMapper.selectList(Wrappers.<FormalSettlementChangeRecord>lambdaQuery()
.eq(FormalSettlementChangeRecord::getFormalSettlementId, id)
.orderByDesc(FormalSettlementChangeRecord::getChangeTime)));
List<FormalSettlementChangeRecord> changeRecords = changeRecordMapper.selectList(
Wrappers.<FormalSettlementChangeRecord>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<FormalSettlemen
settlement.setKingdeeSyncStatus("unsynced");
}
if (first == null) copyHeader(contract, settlementType, directDetails.get(0), settlement); else copyHeader(first, settlement);
settlement.setBusinessModel(limit(request.getBusinessModel(), 64));
settlement.setTransportType(limit(request.getTransportType(), 64));
settlement.setExchangeRateDate(request.getExchangeRateDate());
settlement.setExchangeRate(request.getExchangeRate() == null ? BigDecimal.ONE : positive(request.getExchangeRate(), "结算汇率"));
BigDecimal sourceAmount = sources.stream().map(PreSettlement::getSettlementAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add);
@@ -631,7 +638,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
settlement.setCurrentProcessor(AuthUtil.getUserName());
settlement.setApprovedTime(LocalDateTime.now());
updateById(settlement);
saveChange(settlement.getId(), "结算单基本信息", null, "调整", "正式结算审批通过", request.getReason());
// 审批通过不落变更记录
// 审批事务提交后自动推送金蝶(AFTER_COMMIT 监听器消费,推送失败不影响审批结果)
eventPublisher.publishEvent(new KingdeeBillApprovedEvent(KingdeeBillApprovedEvent.BIZ_FORMAL_SETTLEMENT, settlement.getId()));
}
@@ -1268,8 +1275,30 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
}
Map<Long, ReceivablePayableCargoFee> sourceFeeMap = receivablePayableCargoFeeMapper.selectBatchIds(sourceFeeIds).stream()
.collect(Collectors.toMap(ReceivablePayableCargoFee::getId, Function.identity(), (left, right) -> left));
// 历史数据可能把预结算费用行ID误写入 sourceFeeId,需再解析到应收应付费用行
List<Long> unresolvedIds = sourceFeeIds.stream().filter(id -> !sourceFeeMap.containsKey(id)).toList();
Map<Long, PreSettlementDetailFee> preFeeMap = unresolvedIds.isEmpty() ? Map.of()
: preDetailFeeMapper.selectBatchIds(unresolvedIds).stream()
.collect(Collectors.toMap(PreSettlementDetailFee::getId, Function.identity(), (left, right) -> left));
List<Long> receivableFeeIds = preFeeMap.values().stream().map(PreSettlementDetailFee::getSourceFeeId)
.filter(Objects::nonNull).distinct().toList();
if (!receivableFeeIds.isEmpty()) {
receivablePayableCargoFeeMapper.selectBatchIds(receivableFeeIds).forEach(item ->
sourceFeeMap.putIfAbsent(item.getId(), item));
}
fees.forEach(fee -> {
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<FormalSettlemen
for (PreSettlementDetailFee item : fees) {
FormalSettlementDetailFee fee = Objects.requireNonNull(BeanUtil.copyProperties(item, FormalSettlementDetailFee.class));
fee.setTransportQuantity(money(fee.getTransportQuantity()));
fee.setId(null); fee.setFormalSettlementDetailId(target.getId()); fee.setSourceFeeId(item.getId()); detailFeeMapper.insert(fee);
fee.setId(null);
fee.setFormalSettlementDetailId(target.getId());
// 优先保留应收应付费用行ID,便于正式结算调整时按合同计费方案试算
fee.setSourceFeeId(item.getSourceFeeId() != null ? item.getSourceFeeId() : item.getId());
detailFeeMapper.insert(fee);
}
if (fees.isEmpty()) createSingleFee(target);
}
@@ -2334,7 +2367,10 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
settlement.setApprovalStatus(target); settlement.setCurrentNode(node); settlement.setCurrentProcessor(AuthUtil.getUserName());
if (TransportBusiConstant.RETURNED.equals(target)) settlement.setVoidReason(limit(reason, 200));
updateById(settlement);
saveChange(settlement.getId(), "结算单基本信息", null, "调整", node, reason);
// 审批驳回不落变更记录;提交等其它状态流转仍记录
if (!TransportBusiConstant.RETURNED.equals(target)) {
saveChange(settlement.getId(), "结算单基本信息", null, "调整", node, reason);
}
}
private void saveManualSummaryFeeChange(Long settlementId, boolean creating, String beforeFeeType,
@@ -2393,6 +2429,14 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
}
}
/** 审批通过、驳回等审批流转不落变更记录。 */
private boolean isApprovalChangeContent(String content) {
if (Func.isEmpty(content)) {
return false;
}
return content.contains("审批通过") || content.contains("审批驳回") || "已驳回".equals(content);
}
private void saveChange(Long settlementId, String changeType, Integer lineNo, String operationType,
String content, String reason) {
saveChange(settlementId, changeType, lineNo, operationType, content, reason, null, null);
@@ -2400,6 +2444,9 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
private void saveChange(Long settlementId, String changeType, Integer lineNo, String operationType,
String content, String reason, Map<String, Object> beforeData, Map<String, Object> afterData) {
if (isApprovalChangeContent(content)) {
return;
}
FormalSettlementChangeRecord record = new FormalSettlementChangeRecord();
record.setFormalSettlementId(settlementId);
record.setChangeType(changeType);
@@ -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<LoadingManageMappe
@Resource
private WaybillMapper waybillMapper;
@Resource
private WaybillNodePunchMapper waybillNodePunchMapper;
@Resource
private IContractManageService contractManageService;
@@ -139,10 +144,26 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
loadingManage.setLoadingNo(nextCode());
}
validateCarrierContract(loadingManage, false);
if (STATUS_RUNNING.equals(businessStatus) && Func.isNotEmpty(loadingManage.getId())) {
LoadingManage oldRecord = getById(loadingManage.getId());
if (oldRecord != null) {
validateRunningImmutableTaskFields(oldRecord, loadingManage);
Set<Long> nextIds = new HashSet<>(waybillIds(loadingManage.getWaybillIdsJson()));
for (Long oldId : oldWaybillIds) {
if (oldId != null && !nextIds.contains(oldId) && hasLoadOrUnloadPunch(oldId)) {
throw new ServiceException("已打卡的子运单不可从进行中配载单移除");
}
}
}
}
boolean result = saveOrUpdate(loadingManage);
// 草稿仅预占运单号,不把运单业务状态改成 draft;正式确认时再完整同步为待执行
if (STATUS_DRAFT.equals(businessStatus)) {
syncDraftWaybillReservation(loadingManage, oldWaybillIds);
} else if (STATUS_RUNNING.equals(businessStatus)) {
// 进行中暂存:不重置原有子单状态/接单,仅同步任务信息与新增子单
List<Waybill> waybills = loadWaybills(waybillIds(loadingManage.getWaybillIdsJson()));
writeWaybillsForRunningEdit(loadingManage, oldWaybillIds, waybills);
} else {
syncAssociatedWaybills(loadingManage, businessStatus, oldWaybillIds);
}
@@ -156,28 +177,103 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
validate(loadingManage, false);
List<Long> 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<Waybill> 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<Waybill> waybills, List<Long> oldWaybillIds) {
validateRunningImmutableTaskFields(oldRecord, incoming);
Set<Long> nextIds = waybills.stream().map(Waybill::getId).filter(Objects::nonNull).collect(Collectors.toSet());
for (Long oldId : oldWaybillIds) {
if (oldId == null || nextIds.contains(oldId)) {
continue;
}
if (hasLoadOrUnloadPunch(oldId)) {
throw new ServiceException("已打卡的子运单不可从进行中配载单移除");
}
}
}
private void validateRunningImmutableTaskFields(LoadingManage oldRecord, LoadingManage incoming) {
if (!Objects.equals(trimText(oldRecord.getCarrierType()), trimText(incoming.getCarrierType()))) {
throw new ServiceException("进行中配载单不允许修改承运类型");
}
if (Objects.equals(oldRecord.getCarrierType(), "承运商")
&& !Objects.equals(oldRecord.getCarrierContractId(), incoming.getCarrierContractId())) {
throw new ServiceException("进行中配载单不允许修改承运商");
}
if (!Objects.equals(trimText(oldRecord.getVehicleNo()), trimText(incoming.getVehicleNo()))) {
throw new ServiceException("进行中配载单不允许修改车牌号");
}
if (Objects.equals(oldRecord.getCarrierType(), CARRIER_SELF)) {
if (!Objects.equals(trimText(oldRecord.getDriverName()), trimText(incoming.getDriverName()))) {
throw new ServiceException("进行中配载单不允许修改司机");
}
if (!Objects.equals(trimText(oldRecord.getDriverPhone()), trimText(incoming.getDriverPhone()))) {
throw new ServiceException("进行中配载单不允许修改手机号");
}
}
}
private boolean hasLoadOrUnloadPunch(Long waybillId) {
if (waybillId == null) {
return false;
}
List<WaybillNodePunch> punches = waybillNodePunchMapper.selectList(Wrappers.<WaybillNodePunch>lambdaQuery()
.eq(WaybillNodePunch::getWaybillId, waybillId)
.eq(WaybillNodePunch::getIsDeleted, 0));
return punches.stream().anyMatch(this::isLoadOrUnloadPunch);
}
private boolean isLoadOrUnloadPunch(WaybillNodePunch punch) {
if (punch == null) {
return false;
}
String code = Func.toStr(punch.getNodeCode(), "").trim().toLowerCase();
String name = Func.toStr(punch.getNodeName(), "").trim();
return "load".equals(code) || name.contains("装货")
|| "unload".equals(code) || name.contains("卸货")
|| "arrival".equals(code) || name.contains("到货")
|| "sign".equals(code) || name.contains("签收");
}
private String trimText(String value) {
return Func.toStr(value, "").trim();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void createFromImportedWaybills(String loadingNo, List<Waybill> waybills) {
@@ -776,8 +872,8 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
String prefix = "运单" + waybill.getWaybillNo();
return prefix + switch (reason) {
case NOT_ROAD -> "运输方式不是公路,不可配载";
case NOT_PENDING -> "当前状态不是待执行,不可配载";
case ON_OTHER_MANIFEST -> "已关联其他配载单";
case NOT_LOADABLE_STATUS -> "当前状态不是待执行或进行中,不可配载";
case ON_OTHER_MANIFEST -> "已关联其他有效配载单";
};
}
@@ -910,6 +1006,108 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
clearAssociatedWaybillAcceptRecords(loadingManage);
}
/**
* 进行中配载编辑同步:原有子单只更新任务可选信息,保留业务状态与接单事实;
* 新加入子单按正常派车逻辑推导状态并清接单;被移除且未打卡的子单释放回待执行。
*/
private void writeWaybillsForRunningEdit(LoadingManage loadingManage, List<Long> oldWaybillIds, List<Waybill> waybills) {
List<Long> newWaybillIds = waybillIds(loadingManage.getWaybillIdsJson());
Set<Long> oldIdSet = new HashSet<>(oldWaybillIds);
List<Long> removedIds = oldWaybillIds.stream()
.filter(id -> id != null && !newWaybillIds.contains(id))
.toList();
releaseAssociatedWaybills(removedIds, STATUS_PENDING);
List<Long> keptIds = newWaybillIds.stream()
.filter(oldIdSet::contains)
.toList();
if (Func.isNotEmpty(keptIds)) {
updateAssociatedWaybillTaskFields(loadingManage, keptIds, null);
}
List<Long> addedIds = newWaybillIds.stream()
.filter(id -> id != null && !oldIdSet.contains(id))
.toList();
if (Func.isEmpty(addedIds)) {
return;
}
List<Waybill> addedWaybills = waybills.stream()
.filter(waybill -> waybill.getId() != null && addedIds.contains(waybill.getId()))
.toList();
Map<Long, String> statusByWaybillId = deriveWaybillBusinessStatuses(addedWaybills);
Map<String, List<Long>> waybillIdsByStatus = new LinkedHashMap<>();
for (Long waybillId : addedIds) {
waybillIdsByStatus
.computeIfAbsent(statusByWaybillId.getOrDefault(waybillId, STATUS_PENDING), key -> new ArrayList<>())
.add(waybillId);
}
waybillIdsByStatus.forEach((status, ids) -> updateAssociatedWaybillFields(loadingManage, ids, status));
clearWaybillAcceptRecords(addedIds);
}
private void updateAssociatedWaybillTaskFields(LoadingManage loadingManage, List<Long> waybillIds, String status) {
if (Func.isEmpty(waybillIds)) {
return;
}
var update = Wrappers.<Waybill>lambdaUpdate()
.in(Waybill::getId, waybillIds)
.set(Waybill::getLoadingNo, loadingManage.getLoadingNo())
.set(Waybill::getCarrierType, loadingManage.getCarrierType())
.set(Waybill::getCarrierContractId, loadingManage.getCarrierContractId())
.set(Waybill::getCarrierName, loadingManage.getCarrierName())
.set(Waybill::getDriverName, loadingManage.getDriverName())
.set(Waybill::getDriverPhone, loadingManage.getDriverPhone())
.set(Waybill::getVehicleNo, loadingManage.getVehicleNo())
.set(Waybill::getTrailerVehicleNo, loadingManage.getTrailerVehicleNo())
.set(Waybill::getEscortName, loadingManage.getEscortName())
.set(Waybill::getEscortPhone, loadingManage.getEscortPhone())
.set(Waybill::getMileage, loadingManage.getMileage())
.set(Waybill::getEstimatedStartTime, toDateTime(loadingManage.getEstimatedStartDate()))
.set(Waybill::getEstimatedEndTime, toDateTime(loadingManage.getEstimatedEndDate()))
.set(Waybill::getTaskRemark, loadingManage.getTaskRemark())
.set(Waybill::getTaskInfoJson, loadingManage.getTaskInfoJson())
.set(Waybill::getCurrentProcessNode, loadingManage.getCurrentProcessNode());
if (Func.isNotEmpty(status)) {
update.set(Waybill::getBusinessStatus, status);
if (STATUS_COMPLETED.equals(status)) {
update.set(Waybill::getEndDate, LocalDate.now());
}
}
waybillMapper.update(null, update);
}
private void clearWaybillAcceptRecords(List<Long> waybillIds) {
if (Func.isEmpty(waybillIds)) {
return;
}
waybillMapper.update(null, Wrappers.<Waybill>lambdaUpdate()
.in(Waybill::getId, waybillIds)
.set(Waybill::getDriverAcceptStatus, WaybillProcessSupport.ACCEPT_PENDING)
.set(Waybill::getDriverAcceptTime, null)
.set(Waybill::getDriverAcceptDriverId, null)
.set(Waybill::getDriverRejectTime, null)
.set(Waybill::getDriverRejectReason, null));
}
/**
* 配载单业务状态按子运单所属项目的实时过程配置推导:需要接单 → 待执行,不需要 → 进行中。
* <p>
* 一张配载单的子运单同属一个项目,任一张需接单则整单待执行。
*/
private String deriveLoadingBusinessStatus(List<Waybill> waybills) {
if (Func.isEmpty(waybills)) {
return STATUS_RUNNING;
}
Map<Long, String> liveConfigByProject = LiveProcessConfigSupport.byProjects(processConfigService,
waybills.stream().map(Waybill::getProjectId).toList());
for (Waybill waybill : waybills) {
String processJson = LiveProcessConfigSupport.resolve(
liveConfigByProject.get(waybill.getProjectId()), waybill.getProcessJson());
if (WaybillProcessSupport.requiresDriverAcceptConfirmation(processJson)) {
return STATUS_PENDING;
}
}
return STATUS_RUNNING;
}
/**
* 子运单业务状态逐张按各自项目的实时过程配置推导:需要接单 → 待执行,不需要 → 进行中。
* <p>
@@ -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;
* 列表与详情与司机端同粒度:取数、状态聚合、接单需求合并都调共享运次读服务,
* 本类只做字段裁剪与调度端独有的筛选(组织类型 / 异常 / 创建时间 / 待重新派单)。
* <p>
* 小程序调度账号(如「小程序管理」)组织常与运单业务组织不一致,故不做 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<Long> 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<Long> 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 {
}
/**
* 调度端可见组织:普通用户(含各级角色)只出自己所属组织,超级管理员不过滤。
* <p>
* 小程序不校验「所属组织不能为空」:账号没有组织时不按组织收口,避免首页、待处理等接口直接报错。
* 详情与重新派单本身不走这里。
* 调度端数据权限落到运次查询条件:小程序按可见项目,系统用户按所属组织。
*/
private void applyAdminDataScope(TripQueryVO query) {
if (query == null) {
return;
}
if (MpUserAuth.isMpUser()) {
query.setProjectIds(manageableProjectIds());
return;
}
query.setDeptIds(manageableDeptIds());
}
/**
* 待重派等直查运单表时的数据权限:小程序按 project_id,系统用户按 dept_id。
*/
private void applyAdminDataScope(QueryWrapper<Waybill> wrapper) {
if (wrapper == null) {
return;
}
if (MpUserAuth.isMpUser()) {
List<Long> projectIds = manageableProjectIds();
if (Func.isNotEmpty(projectIds)) {
wrapper.in("project_id", projectIds);
}
return;
}
List<Long> deptIds = manageableDeptIds();
if (Func.isNotEmpty(deptIds)) {
wrapper.in("dept_id", deptIds);
}
}
/**
* 小程序调度端可见项目:所属组织 + 数据权限范围 + 数据层级范围 → 业务部门在范围内的项目。
* 空列表不过滤;[-1] 表示无权限。
*/
private List<Long> manageableProjectIds() {
List<Long> projectIds = mpUserDataScopeSupport.resolveVisibleProjectIdsForQuery();
return projectIds == null ? List.of() : projectIds;
}
/**
* 系统用户可见组织:普通账号只出所属组织,超管不过滤;组织为空不拦截。
*/
private List<Long> manageableDeptIds() {
if (TransportBusinessSupport.isAdministrator()) {
@@ -919,10 +959,12 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
}
private Set<Long> loadIncompleteExceptionWaybillIds() {
List<ExceptionDisposal> list = exceptionDisposalService.list(Wrappers.<ExceptionDisposal>lambdaQuery()
LambdaQueryWrapper<ExceptionDisposal> wrapper = Wrappers.<ExceptionDisposal>lambdaQuery()
.select(ExceptionDisposal::getWaybillId)
.in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING)
.isNotNull(ExceptionDisposal::getWaybillId));
.isNotNull(ExceptionDisposal::getWaybillId);
applyExceptionDeptScope(wrapper);
List<ExceptionDisposal> list = exceptionDisposalService.list(wrapper);
Set<Long> ids = new HashSet<>();
for (ExceptionDisposal item : list) {
if (item.getWaybillId() != null) {
@@ -932,6 +974,38 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
return ids;
}
/** 异常与运单同一套项目口径(小程序);系统用户仍按组织 */
private void applyExceptionDeptScope(LambdaQueryWrapper<ExceptionDisposal> wrapper) {
if (MpUserAuth.isMpUser()) {
List<Long> projectIds = manageableProjectIds();
if (Func.isNotEmpty(projectIds)) {
wrapper.in(ExceptionDisposal::getProjectId, projectIds);
}
return;
}
List<Long> deptIds = manageableDeptIds();
if (Func.isNotEmpty(deptIds)) {
wrapper.in(ExceptionDisposal::getDeptId, deptIds);
}
}
/** 风险无项目字段:小程序按关联运单的项目收口;系统用户按组织 */
private void applyRiskDeptScope(LambdaQueryWrapper<RiskDisposal> wrapper) {
if (MpUserAuth.isMpUser()) {
List<Long> projectIds = manageableProjectIds();
if (Func.isNotEmpty(projectIds)) {
String inList = projectIds.stream().map(String::valueOf).collect(Collectors.joining(","));
wrapper.inSql(RiskDisposal::getWaybillId,
"SELECT id FROM blade_waybill WHERE is_deleted = 0 AND project_id IN (" + inList + ")");
}
return;
}
List<Long> deptIds = manageableDeptIds();
if (Func.isNotEmpty(deptIds)) {
wrapper.in(RiskDisposal::getDeptId, deptIds);
}
}
private AdminWaybillCardVO toCard(Waybill waybill, boolean hasException) {
AdminWaybillCardVO card = new AdminWaybillCardVO();
card.setId(waybill.getId());
@@ -1062,13 +1136,17 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
}
private long countIncompleteExceptions() {
return exceptionDisposalService.count(Wrappers.<ExceptionDisposal>lambdaQuery()
.in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING));
LambdaQueryWrapper<ExceptionDisposal> wrapper = Wrappers.<ExceptionDisposal>lambdaQuery()
.in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING);
applyExceptionDeptScope(wrapper);
return exceptionDisposalService.count(wrapper);
}
private long countPendingRisks() {
return riskDisposalService.count(Wrappers.<RiskDisposal>lambdaQuery()
.eq(RiskDisposal::getDisposalStatus, RISK_PENDING));
LambdaQueryWrapper<RiskDisposal> wrapper = Wrappers.<RiskDisposal>lambdaQuery()
.eq(RiskDisposal::getDisposalStatus, RISK_PENDING);
applyRiskDeptScope(wrapper);
return riskDisposalService.count(wrapper);
}
/**
@@ -1110,10 +1188,12 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
}
private List<TimedTodo> buildExceptionFeed() {
List<ExceptionDisposal> list = exceptionDisposalService.list(Wrappers.<ExceptionDisposal>lambdaQuery()
LambdaQueryWrapper<ExceptionDisposal> wrapper = Wrappers.<ExceptionDisposal>lambdaQuery()
.in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING)
.orderByDesc(ExceptionDisposal::getReportTime)
.last("LIMIT " + FEED_LIMIT));
.last("LIMIT " + FEED_LIMIT);
applyExceptionDeptScope(wrapper);
List<ExceptionDisposal> list = exceptionDisposalService.list(wrapper);
return list.stream()
.map(disposal -> new TimedTodo(toTodoItem(disposal), exceptionTimeOf(disposal)))
.collect(Collectors.toList());
@@ -2131,21 +2131,18 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
if (settlements.isEmpty()) return;
settlementRelationMapper.delete(Wrappers.<PaymentApplicationSettlement>lambdaQuery()
.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<PaymentApplic
return;
}
// 单选/多选均落关联行,生成即占用可付款额度(删除草稿或审核不通过后释放)
boolean customAmounts = targetSettlements.size() > 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<PaymentApplic
}
}
private void assertLineTotalMatches(PaymentApplication entity, BigDecimal lineTotal) {
BigDecimal appliedAmount = money(entity.getAppliedAmount()).setScale(2, RoundingMode.HALF_UP);
if (lineTotal.setScale(2, RoundingMode.HALF_UP).compareTo(appliedAmount) != 0) {
throw new ServiceException("申请付款金额须等于各结算单付款金额合计");
}
/** 前端传入结算行付款金额时,按行落库,不再要求合计等于申请付款金额。 */
private boolean useSpecifiedLineAmounts(List<PaymentApplicationSettlementLineRequest> lines) {
return lines != null && !lines.isEmpty();
}
private void validateQuota(PaymentApplication entity) {
@@ -2563,44 +2556,11 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
private void validateBill(BillLedger ledger, Long deptId, BigDecimal amount, boolean checkMaturity,
Long paymentApplicationId, Long settlementId, Long preSettlementId) {
if (ledger == null || Objects.equals(ledger.getIsDeleted(), 1)) throw new ServiceException("所选汇票台账不存在");
boolean partyMatched = settlementPartiesMatch(ledger, settlementId, preSettlementId);
if (!"received".equals(ledger.getBillType()) && !partyMatched) {
throw new ServiceException("汇票付款仅可选择收票类型的汇票,或出票单位与收票单位均为当前结算收付款方的汇票");
}
if (checkMaturity && (ledger.getMaturityDate() == null || ledger.getMaturityDate().isBefore(LocalDate.now()))) {
throw new ServiceException("所选汇票已到期");
}
BigDecimal available = money(ledger.getAvailableBalance()).add(occupiedAmount(paymentApplicationId, ledger.getId()));
if (money(amount).compareTo(available) > 0) throw new ServiceException("申请付款金额超过汇票可用余额");
if (!partyMatched && !departmentAvailable(ledger, deptId)) throw new ServiceException("当前使用部门不在汇票可用部门范围内");
}
/** 出票单位、收票单位与结算付款方、收款方一致(顺序不限) */
private boolean settlementPartiesMatch(BillLedger ledger, Long settlementId, Long preSettlementId) {
String payerName = null;
String payeeName = null;
if (settlementId != null) {
FormalSettlement settlement = formalSettlementMapper.selectById(settlementId);
if (settlement != null && !Objects.equals(settlement.getIsDeleted(), 1)) {
payerName = settlement.getPayerName();
payeeName = settlement.getPayeeName();
}
}
if ((payerName == null || payeeName == null) && preSettlementId != null) {
PreSettlement settlement = preSettlementMapper.selectById(preSettlementId);
if (settlement != null && !Objects.equals(settlement.getIsDeleted(), 1)) {
payerName = settlement.getPayerName();
payeeName = settlement.getPayeeName();
}
}
String issuer = ledger.getIssuerName() == null ? "" : ledger.getIssuerName().trim();
String receiver = ledger.getReceiverName() == null ? "" : ledger.getReceiverName().trim();
String payer = payerName == null ? "" : payerName.trim();
String payee = payeeName == null ? "" : payeeName.trim();
if (issuer.isEmpty() || receiver.isEmpty() || payer.isEmpty() || payee.isEmpty()) {
return false;
}
return (issuer.equals(payer) && receiver.equals(payee)) || (issuer.equals(payee) && receiver.equals(payer));
}
private void useBillBalance(PaymentApplication entity) {
@@ -2756,18 +2716,6 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
return entity;
}
private boolean departmentAvailable(BillLedger ledger, Long deptId) {
if (Func.isEmpty(ledger.getAvailableDeptIdsJson())) return false;
try {
Object parsed = JsonUtil.parse(ledger.getAvailableDeptIdsJson(), List.class);
if (!(parsed instanceof List<?> values)) return false;
if (values.stream().anyMatch(value -> "all".equals(String.valueOf(value)))) return true;
return deptId != null && values.stream().anyMatch(value -> String.valueOf(deptId).equals(String.valueOf(value)));
} catch (Exception exception) {
throw new ServiceException("汇票可用部门配置不正确");
}
}
@Override
public boolean isBillPayment(String paymentMethod) {
if (Func.isEmpty(paymentMethod)) return false;
@@ -203,13 +203,15 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
vo.setAdvances(advances);
vo.setProjectAdvanceWriteOffs(projectAdvanceWriteOffService.listBySettlement(
TransportBusiConstant.SETTLEMENT_BILL_PRE, id));
vo.setChangeRecords(changeRecordMapper.selectList(Wrappers.<PreSettlementChangeRecord>lambdaQuery()
.eq(PreSettlementChangeRecord::getPreSettlementId, id)
.eq(PreSettlementChangeRecord::getIsDeleted, 0)
.and(wrapper -> wrapper.isNull(PreSettlementChangeRecord::getChangeContent)
.or()
.ne(PreSettlementChangeRecord::getChangeContent, "预结算审批通过"))
.orderByDesc(PreSettlementChangeRecord::getChangeTime)));
List<PreSettlementChangeRecord> changeRecords = changeRecordMapper.selectList(
Wrappers.<PreSettlementChangeRecord>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<PreSettlementMappe
settlement.setLocalCurrency(LOCAL_CURRENCY);
}
fillContract(settlement, contract);
settlement.setBusinessModel(limitText(request.getBusinessModel(), 64, "业务模式"));
settlement.setTransportType(limitText(request.getTransportType(), 64, "运输方式"));
settlement.setExchangeRateDate(request.getExchangeRateDate());
settlement.setExchangeRate(request.getExchangeRate());
settlement.setAttachmentsJson(request.getAttachmentsJson());
@@ -441,7 +445,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
settlement.setCurrentNode("已驳回");
settlement.setCurrentProcessor(AuthUtil.getUserName());
updateById(settlement);
saveChange(settlement.getId(), "结算单基本信息", null, "调整", "预结算审批驳回", reason);
// 审批驳回不落变更记录
}
@Override
@@ -1614,6 +1618,14 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
return source + " " + name;
}
/** 审批通过、驳回等审批流转不落变更记录。 */
private boolean isApprovalChangeContent(String content) {
if (Func.isEmpty(content)) {
return false;
}
return content.contains("审批通过") || content.contains("审批驳回") || "已驳回".equals(content);
}
private void saveChange(Long settlementId, String changeType, Integer lineNo, String operationType,
String content, String reason) {
saveChange(settlementId, changeType, lineNo, operationType, content, reason, null, null);
@@ -1621,6 +1633,9 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
private void saveChange(Long settlementId, String changeType, Integer lineNo, String operationType,
String content, String reason, Map<String, Object> beforeData, Map<String, Object> afterData) {
if (isApprovalChangeContent(content)) {
return;
}
PreSettlementChangeRecord record = new PreSettlementChangeRecord();
record.setPreSettlementId(settlementId);
record.setChangeType(changeType);
@@ -34,6 +34,7 @@ import org.springblade.core.tenant.annotation.TenantIgnore;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.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<ProjectApplyMapper,
projectApply.setUndertakeDeptName(dept.getDeptName());
}
if (Func.isEmpty(projectApply.getBusinessDeptId())) {
projectApply.setBusinessDeptId(firstUndertakeDeptId(projectApply.getUndertakeDeptId()));
projectApply.setBusinessDeptName(firstUndertakeDeptName(projectApply.getUndertakeDeptName()));
fillDefaultBusinessDept(projectApply);
}
if (Func.isEmpty(projectApply.getHandlerUserId())) {
projectApply.setHandlerUserId(AuthUtil.getUserId());
@@ -748,7 +748,7 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
private void validateSubmit(ProjectApply projectApply) {
validateDraft(projectApply);
TransportBusinessSupport.validateRequired(projectApply.getProjectShortName(), "请输入项目简称");
TransportBusinessSupport.validateRequired(projectApply.getBusinessDeptName(), "请选择业务部门");
validateBusinessDept(projectApply);
TransportBusinessSupport.validateRequired(projectApply.getUndertakeDeptName(), "请选择平台公司");
TransportBusinessSupport.validateRequired(projectApply.getBusinessMode(), "请选择业务模式");
TransportBusinessSupport.validateRequired(projectApply.getCustomerNames(), "请选择客户");
@@ -1066,6 +1066,48 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
return exception;
}
/**
* 新增时默认业务部门:取当前用户所属部门中第一个「是否业务部门=是」的组织。
*/
private void fillDefaultBusinessDept(ProjectApply projectApply) {
List<Long> deptIds = Func.toLongList(AuthUtil.getDeptId());
if (Func.isEmpty(deptIds)) {
return;
}
for (Long deptId : deptIds) {
if (Func.isEmpty(deptId) || deptId <= 0) {
continue;
}
Dept dept = SysCache.getDept(deptId);
if (dept != null && Objects.equals(dept.getIsBusinessDept(), 1)) {
projectApply.setBusinessDeptId(dept.getId());
projectApply.setBusinessDeptName(dept.getDeptName());
return;
}
}
}
/**
* 校验业务部门必须是组织中标记为「是否业务部门=是」的部门。
*/
private void validateBusinessDept(ProjectApply projectApply) {
Long businessDeptId = projectApply.getBusinessDeptId();
if (Func.isEmpty(businessDeptId)) {
throw new ServiceException("请选择业务部门");
}
Dept dept = SysCache.getDept(businessDeptId);
if (dept == null || Objects.equals(dept.getIsDeleted(), 1)) {
throw new ServiceException("业务部门不存在");
}
if (!Objects.equals(dept.getIsBusinessDept(), 1)) {
throw new ServiceException("请选择组织中标记为业务部门的部门");
}
if (Func.isNotEmpty(dept.getDeptName())) {
projectApply.setBusinessDeptName(dept.getDeptName());
}
TransportBusinessSupport.validateRequired(projectApply.getBusinessDeptName(), "请选择业务部门");
}
private void normalizeUndertakeDept(ProjectApply projectApply) {
if (projectApply == null) {
return;
@@ -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.<ReceivablePayableCargoFee>lambdaQuery()
@@ -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<RiskDisposalMapper, RiskDisposal> 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<RiskDisposalVO> selectPage(IPage<RiskDisposal> page, RiskDisposalVO query) {
IPage<RiskDisposal> entityPage = page(page, buildQuery(query));
@@ -84,15 +88,16 @@ public class RiskDisposalServiceImpl extends BaseServiceImpl<RiskDisposalMapper,
}
private LambdaQueryWrapper<RiskDisposal> buildQuery(RiskDisposalVO query) {
return Wrappers.<RiskDisposal>lambdaQuery()
LambdaQueryWrapper<RiskDisposal> wrapper = Wrappers.<RiskDisposal>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) {
@@ -119,8 +119,9 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
.orderByDesc(FormalSettlement::getCreateTime));
Set<Long> 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<Map<String, Object>> result = new ArrayList<>();
for (FormalSettlement row : rows) {
@@ -467,9 +468,6 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
if (isSettlementCompleted(formal)) {
throw new ServiceException("该正式结算单已完成收/付款,不能进行结算调整");
}
if (isTailSettlement(formal)) {
throw new ServiceException("尾款结算正式结算单不能进行结算调整");
}
if (hasTailPayment(formal.getId())) {
throw new ServiceException("该正式结算单已发起尾款结算,不能进行结算调整");
}
@@ -480,9 +478,6 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
|| (money(formal.getSettlementAmount()).signum() > 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);
@@ -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<String> resolveFeeItemNames(SettlementImportBatchRequest request) {
LinkedHashSet<String> 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<String> names = new LinkedHashSet<>();
if (Func.isNotEmpty(request.getRows())) {
for (Map<String, Object> 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<String> mergeFeeItemNames(List<String> preferred, List<Map<String, Object>> rows) {
LinkedHashSet<String> names = new LinkedHashSet<>();
if (Func.isNotEmpty(preferred)) {
preferred.stream()
.filter(Func::isNotEmpty)
.map(String::trim)
.filter(Func::isNotEmpty)
.forEach(names::add);
}
if (Func.isNotEmpty(rows)) {
for (Map<String, Object> row : rows) {
collectFeeItemNamesFromRow(row, names);
}
}
return new ArrayList<>(names);
}
private void collectFeeItemNamesFromRow(Map<String, Object> row, Set<String> names) {
if (row == null || names == null) {
return;
}
Object feeItems = row.get("feeItems");
if (!(feeItems instanceof Map<?, ?> raw)) {
return;
}
raw.keySet().stream()
.filter(Objects::nonNull)
.map(String::valueOf)
.map(String::trim)
.filter(Func::isNotEmpty)
.forEach(names::add);
}
private List<Map<String, Object>> buildFeeRows(List<Map<String, Object>> rows, List<Waybill> waybills) {
List<Map<String, Object>> primaryRows = primaryRows(rows);
List<Map<String, Object>> 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&lt;Map&gt; 解析,字符串数组会失败导致草稿再打开丢费用列。
*/
private List<String> parseFeeItemNamesJson(String feeItemNamesJson) {
if (Func.isEmpty(feeItemNamesJson)) {
return List.of();
}
try {
List<String> names = JsonUtil.parse(feeItemNamesJson, new TypeReference<List<String>>() {
});
if (names == null || names.isEmpty()) {
return List.of();
}
return names.stream()
.filter(Func::isNotEmpty)
.map(String::trim)
.filter(Func::isNotEmpty)
.distinct()
.toList();
} catch (Exception exception) {
return List.of();
}
}
private String importStatusName(String status) {
if (STATUS_DRAFT.equals(status)) {
return "草稿";
@@ -1350,20 +1411,6 @@ public class SettlementImportBatchServiceImpl
return new ArrayList<>(labels);
}
private List<String> loadEnabledFeeItemNames() {
R<List<FeeItem>> response = sysClient.getFeeItems(false);
if (response == null || !response.isSuccess() || response.getData() == null) {
return List.of();
}
return response.getData().stream()
.filter(item -> item != null && (item.getStatus() == null || Objects.equals(item.getStatus(), 1)))
.map(FeeItem::getName)
.filter(Func::isNotEmpty)
.map(String::trim)
.distinct()
.toList();
}
/** 历史已完成批次无 rows_json 时,按关联运单回填查看明细。 */
private List<Map<String, Object>> buildRowsFromWaybills(List<Waybill> waybills) {
if (Func.isEmpty(waybills)) {
@@ -601,6 +601,13 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
waybill.setContractId(plan.getContractId());
waybill.setContractName(plan.getContractName());
waybill.setCustomerName(plan.getCustomerName());
// 计划单若来自总单,调度运单须继承总单号与段号,供总单进度汇总
if (Func.isEmpty(waybill.getMasterNo()) && Func.isNotEmpty(plan.getMasterNo())) {
waybill.setMasterNo(plan.getMasterNo());
}
if (Func.isEmpty(waybill.getRelationNo()) && Func.isNotEmpty(plan.getRelationNo())) {
waybill.setRelationNo(plan.getRelationNo());
}
if (Func.isEmpty(waybill.getDepartureAddress())) {
waybill.setDepartureAddressId(plan.getDepartureAddressId());
waybill.setDepartureName(plan.getDepartureName());
@@ -205,6 +205,8 @@ public class TransportReconciliationServiceImpl
}
List<TransportReconciliationInternal> internals = internalRows(id);
List<TransportReconciliationExternal> 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<TransportReconciliationExternal> validExternals = new ArrayList<>();
for (int index = 0; index < rows.size(); index++) {
VehicleReconciliationExcel row = rows.get(index);
normalizeImportVehicleDecimals(row);
List<String> 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<String, BigDecimal> feeItems = row.getFeeItems();
if (feeItems == null || feeItems.isEmpty()) {
feeItems = new HashMap<>();
@@ -427,6 +431,7 @@ public class TransportReconciliationServiceImpl
List<TransportReconciliationExternal> validExternals = new ArrayList<>();
for (int index = 0; index < rows.size(); index++) {
CargoReconciliationExcel row = rows.get(index);
normalizeImportCargoDecimals(row);
List<String> 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<String, BigDecimal> 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.<Waybill>lambdaQuery()
.eq(Waybill::getWaybillNo, internal.getWaybillNo()));
}
if (waybill == null) return;
waybill.setVehicleNo(external.getVehicleNo());
waybill.setDepartureAddress(external.getDepartureAddress());
waybill.setArrivalAddress(external.getArrivalAddress());
waybill.setStartDate(external.getActualDepartureTime() == null ? null : external.getActualDepartureTime().toLocalDate());
waybill.setEndDate(external.getActualCompletionTime() == null ? null : external.getActualCompletionTime().toLocalDate());
waybill.setTransportType(external.getTransportType());
waybill.setCargoName(external.getCargoName());
waybill.setCargoType(external.getCargoType());
waybill.setSpecification(external.getSpecification());
waybill.setModel(external.getModel());
waybill.setQuantity(external.getTransportQuantity());
waybill.setQuantityUnit(external.getQuantityUnit());
waybill.setMileage(external.getMileage());
waybill.setBatchNo(external.getBatchNo());
waybill.setUnitPrice(external.getUnitPrice());
waybill.setFreightJson(external.getFeeItemsJson());
waybill.setOtherFeeTotal(money(external.getSettlementAmount()).subtract(money(external.getFreightAmount())).max(BigDecimal.ZERO));
waybillMapper.updateById(waybill);
}
@Override
@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<TransportReconciliationInternal> rows) { return rows.stream().map(TransportReconciliationInternal::getTransportQuantity).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); }
private BigDecimal sumExternalQuantity(List<TransportReconciliationExternal> rows) { return rows.stream().map(TransportReconciliationExternal::getTransportQuantity).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); }
@@ -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<Waybill> rows = waybillService.list(wrapper);
@@ -268,6 +269,18 @@ public class TripQueryServiceImpl implements ITripQueryService {
wrapper.in(Waybill::getDeptId, condition.getDeptIds());
}
/**
* 项目收口:条件里给了项目 ID 集合时只出这些项目的运单行,空集合表示不过滤。
* <p>
* 小程序调度端先按用户数据权限解析可见项目(业务部门在范围内),再把项目 ID 传进来。
*/
private void applyProjectScope(LambdaQueryWrapper<Waybill> wrapper, TripQueryVO condition) {
if (Func.isEmpty(condition.getProjectIds())) {
return;
}
wrapper.in(Waybill::getProjectId, condition.getProjectIds());
}
/**
* 运输组织类型按子单行筛:它在同一趟车里必然是同一个值(配载的每张子单带同一个配载单号,
* 未配载的每张自成一趟车),所以这一层不会把一趟车拆开。
@@ -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());
@@ -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<VoucherManageMappe
private final LoadingManageMapper loadingManageMapper;
private final ApplicationEventPublisher eventPublisher;
private final ObjectProvider<MinioClient> 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<VoucherManageMappe
VoucherImageMapper voucherImageMapper, VoucherFileMapper voucherFileMapper,
LoadingManageMapper loadingManageMapper,
ApplicationEventPublisher eventPublisher, ObjectProvider<MinioClient> 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<VoucherManageMappe
this.loadingManageMapper = loadingManageMapper;
this.eventPublisher = eventPublisher;
this.minioClientProvider = minioClientProvider;
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
this.requiresNewTransactionTemplate = transactionTemplate;
this.minioBucketName = minioBucketName;
this.minioRootDirectory = minioRootDirectory;
this.minioTransformEndpoint = minioTransformEndpoint;
@@ -1156,8 +1164,10 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
int imageCount = 0;
int matchedImageCount = 0;
Set<Long> 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<VoucherManageMappe
}
}
}
if (fileCount == 0) {
throw new ServiceException("压缩包无法解析或内没有找到有效文件,请检查后重新上传");
}
log.info("[凭证处理] 进度 90%:文件解压上传完成 voucherId={}, fileCount={}, imageCount={}, matchedImageCount={}",
voucherId, fileCount, imageCount, matchedImageCount);
VoucherManage update = new VoucherManage();
@@ -1256,12 +1269,53 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
deleteTempArchive(archivePath);
}
} catch (Exception exception) {
// 独立事务落「处理失败」,避免本方法事务回滚把失败状态冲掉后一直卡在「处理中」。
markProcessFailed(voucher.getId());
log.error("凭证压缩包处理失败 voucherId:{}", voucherId, exception);
// 不向上抛:解析失败属业务终态,MQ 消费确认后不再重试。
}
}
/**
* 在新事务中写入处理失败,确保即使当前解析事务回滚,列表也能看到失败态并允许重新上传。
*/
private void markProcessFailed(Long voucherId) {
if (voucherId == null) {
return;
}
requiresNewTransactionTemplate.executeWithoutResult(status -> {
VoucherManage update = new VoucherManage();
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 压缩包,无法解析");
}
}
}
@@ -1084,8 +1084,9 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
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<WaybillMapper, Waybill>
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:{}",
@@ -29,7 +29,7 @@ import java.util.List;
import java.util.Locale;
/**
* 可配载准入判据:公路 + 待执行 + 未关联其它配载单。
* 可配载准入判据:公路 + 待执行/进行中 + 未关联其它配载单。
* <p>
* 接单状态不进判据:拒单票照样可配载。配载提交会把它的接单事实清成待接单,
* 否则新司机看不到这张票——司机端入口一律排除已拒绝的运单。
@@ -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<String> LOADABLE_BUSINESS_STATUSES = List.of(
WaybillProcessSupport.STATUS_PENDING,
WaybillProcessSupport.STATUS_RUNNING
);
/**
* 公路运输方式的同义写法,按「包含」比对,与前端运单管理页的识别口径一致。
@@ -56,7 +59,7 @@ public final class LoadableAdmissionSupport {
}
/**
* 不可配载的原因,按判定顺序排列:公路 → 待执行 → 未关联其它配载单。
* 不可配载的原因,按判定顺序排列:公路 → 待执行/进行中 → 未关联其它配载单。
* <p>
* 判据只产出原因,中文文案由调用方映射——面向用户的措辞不进这一层。
*/
@@ -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<Waybill> 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, ""));
}
@@ -0,0 +1,184 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.support;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import lombok.RequiredArgsConstructor;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.feign.IMpUserClient;
import org.springblade.system.pojo.enums.MpUserIdentity;
import org.springblade.system.pojo.vo.MpUserDataScopeVO;
import org.springblade.system.utils.MpUserAuth;
import org.springblade.transport.pojo.entity.ProjectApply;
import org.springblade.transport.service.IProjectApplyService;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 小程序调度端数据权限。
* <p>
* 口径:所属组织 + 数据权限范围 + 数据层级范围 → 可见组织 →
* 业务部门落在可见组织内的项目 → 再按这些项目收口运单/异常/风险。
*
* @author Chill
*/
@Component
@RequiredArgsConstructor
public class MpUserDataScopeSupport {
/**
* 无权限时的占位 ID:查询 in(-1) 保证查不到数据(空列表在运次查询里表示不过滤)。
*/
private static final Long NONE_ID = -1L;
private final IMpUserClient mpUserClient;
private final IProjectApplyService projectApplyService;
/**
* 当前登录是否为小程序调度端用户(含 admin 身份)
*/
public boolean isMpDispatcher() {
return MpUserAuth.isMpUser()
&& MpUserIdentity.has(AuthUtil.getUserRole(), MpUserIdentity.ADMIN);
}
/**
* 解析当前小程序用户可见组织;非小程序用户返回 null(调用方走原逻辑)。
*/
public MpUserDataScopeVO resolveCurrentMpUserScope() {
if (!MpUserAuth.isMpUser()) {
return null;
}
return resolveByUserId(AuthUtil.getUserId());
}
/**
* 解析指定小程序用户可见组织;Feign 失败时按无权限处理。
*/
public MpUserDataScopeVO resolveByUserId(Long userId) {
if (Func.isEmpty(userId) || userId <= 0) {
return MpUserDataScopeVO.empty();
}
R<MpUserDataScopeVO> result = mpUserClient.resolveDataScope(userId);
if (result == null || !result.isSuccess() || result.getData() == null) {
return MpUserDataScopeVO.empty();
}
return result.getData();
}
/**
* 转为查询用的组织 ID 约定:空=不过滤;非空=in;无权限=[-1]。
*/
public List<Long> toQueryDeptIds(MpUserDataScopeVO scope) {
if (scope == null || scope.isAllDept()) {
return List.of();
}
if (Func.isEmpty(scope.getDeptIds())) {
return List.of(NONE_ID);
}
return scope.getDeptIds();
}
/**
* 解析当前小程序用户可见项目 ID(业务部门落在数据权限组织范围内)。
* <ul>
* <li>空列表 = 全部组织 / 不过滤</li>
* <li>非空 = 仅这些项目</li>
* <li>无权限或范围内无项目 = [-1]</li>
* </ul>
* 非小程序用户返回 null。
*/
public List<Long> resolveVisibleProjectIdsForQuery() {
MpUserDataScopeVO scope = resolveCurrentMpUserScope();
if (scope == null) {
return null;
}
return resolveProjectIds(scope);
}
/**
* 按数据权限结果解析可见项目 ID。
*/
public List<Long> resolveProjectIds(MpUserDataScopeVO scope) {
if (scope == null || scope.isAllDept()) {
return List.of();
}
List<Long> deptIds = scope.getDeptIds();
if (Func.isEmpty(deptIds)) {
return List.of(NONE_ID);
}
List<ProjectApply> projectList = projectApplyService.list(Wrappers.<ProjectApply>lambdaQuery()
.select(ProjectApply::getId)
.in(ProjectApply::getBusinessDeptId, deptIds));
if (Func.isEmpty(projectList)) {
return List.of(NONE_ID);
}
List<Long> projectIds = projectList.stream()
.map(ProjectApply::getId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
return Func.isEmpty(projectIds) ? List.of(NONE_ID) : projectIds;
}
/**
* 当前为小程序调度用户时,按可见项目写入 project 列;非调度用户不改动。
*/
public <T> void applyProjectScopeIfMpDispatcher(LambdaQueryWrapper<T> wrapper, SFunction<T, ?> projectColumn) {
if (!isMpDispatcher() || wrapper == null || projectColumn == null) {
return;
}
List<Long> projectIds = resolveVisibleProjectIdsForQuery();
if (Func.isEmpty(projectIds)) {
return;
}
wrapper.in(projectColumn, projectIds);
}
/**
* 风险等无 projectId 字段时:按关联运单的项目收口。
*/
public <T> void applyWaybillProjectScopeIfMpDispatcher(LambdaQueryWrapper<T> wrapper, SFunction<T, ?> waybillIdColumn) {
if (!isMpDispatcher() || wrapper == null || waybillIdColumn == null) {
return;
}
List<Long> projectIds = resolveVisibleProjectIdsForQuery();
if (Func.isEmpty(projectIds)) {
return;
}
String inList = projectIds.stream().map(String::valueOf).collect(Collectors.joining(","));
wrapper.inSql(waybillIdColumn,
"SELECT id FROM blade_waybill WHERE is_deleted = 0 AND project_id IN (" + inList + ")");
}
}
@@ -229,7 +229,7 @@ public final class WaybillProcessSupport {
* <p>
* 需要接单(接单节点 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);
@@ -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<FormalSettlement,
default -> 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;
}
}
@@ -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<PreSettlement, PreSe
vo.setUpdateUserName(UserCache.getUserRealName(entity.getUpdateUser()));
vo.setApprovalStatusName(approvalStatusName(entity.getApprovalStatus()));
vo.setSettlementTypeName("receivable".equals(entity.getSettlementType()) ? "应收" : "应付");
vo.setBusinessModelName(dictBizName("business_model", entity.getBusinessModel()));
vo.setTransportTypeName(dictBizName("transport_type", entity.getTransportType()));
return vo;
}
@@ -48,4 +53,24 @@ public class PreSettlementWrapper extends BaseEntityWrapper<PreSettlement, PreSe
};
}
private String dictBizName(String code, String value) {
if (Func.isEmpty(value)) {
return value;
}
String name = "";
try {
name = DictBizCache.getValue(code, value);
} catch (Exception exception) {
name = "";
}
if (Func.isEmpty(name)) {
try {
name = DictCache.getValue(code, value);
} catch (Exception exception) {
name = "";
}
}
return Func.isEmpty(name) ? value : name;
}
}
@@ -85,7 +85,8 @@ class ExceptionDisposalDetailPayloadTest {
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);
}
@@ -66,11 +66,14 @@ class ExceptionDisposalListQueryTest {
disposalMapper = mock(ExceptionDisposalMapper.class);
waybillService = mock(IWaybillService.class);
loadingManageMapper = mock(LoadingManageMapper.class);
org.springblade.transport.support.MpUserDataScopeSupport scopeSupport =
mock(org.springblade.transport.support.MpUserDataScopeSupport.class);
service = new ExceptionDisposalServiceImpl(
mock(ExceptionDisposalFollowRecordMapper.class),
waybillService,
mock(IDriverWaybillService.class),
loadingManageMapper);
loadingManageMapper,
scopeSupport);
inject("baseMapper", disposalMapper);
when(disposalMapper.selectPage(any(), any())).thenReturn(new Page<ExceptionDisposal>(1, 10));
}
@@ -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);
@@ -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());
@@ -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);
@@ -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<LoadingManage> captor = ArgumentCaptor.forClass(LoadingManage.class);
verify(service, atLeastOnce()).saveOrUpdate(captor.capture());
return captor.getValue().getBusinessStatus();
}
private void stubWaybills(Waybill... waybills) {
when(waybillMapper.selectList(ArgumentMatchers.<Wrapper<Waybill>>any())).thenReturn(List.of(waybills));
}
@@ -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.<Wrapper<ExceptionDisposal>>any()))
.thenReturn(new ArrayList<>());
when(waybillService.list(ArgumentMatchers.<Wrapper<Waybill>>any()))
@@ -101,7 +101,8 @@ class ManageTripListDetailTest {
mock(IRiskDisposalService.class),
driverWaybillService,
tripQueryService,
loadingManageService);
loadingManageService,
mock(org.springblade.transport.support.MpUserDataScopeSupport.class));
when(exceptionDisposalService.list(ArgumentMatchers.<Wrapper<ExceptionDisposal>>any()))
.thenReturn(new ArrayList<>());
when(waybillService.list(ArgumentMatchers.<Wrapper<Waybill>>any())).thenReturn(new ArrayList<>());
@@ -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<>());
}
@@ -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.<Wrapper<ExceptionDisposal>>any()))
.thenReturn(new ArrayList<>());
when(waybillService.list(ArgumentMatchers.<Wrapper<Waybill>>any())).thenReturn(new ArrayList<>());
@@ -68,7 +68,7 @@ class WaybillLoadableQueryTest {
}
@Test
@DisplayName("开关开启:公路词表、待执行、未配载一次全部进 wrapper,接单状态不参与")
@DisplayName("开关开启:公路词表、待执行/进行中、未配载一次全部进 wrapper,接单状态不参与")
void loadableSwitchAppliesWholePredicate() {
LambdaQueryWrapper<Waybill> 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"),
@@ -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"));
}
@@ -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 '排序',
@@ -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 '创建时间',
@@ -0,0 +1,421 @@
-- ============================================================================
-- 评分量化表跨环境复制:按 ID 生成 INSERT 脚本
-- ============================================================================
-- 用法(在 DEV 库执行):
-- 1. 修改下方 @quantification_id 为要复制的评分量化表主键
-- 2. 整段执行,结果集中每一行是一条可直接在 UAT/PROD 执行的 SQL
-- 3. 先复制「清理」语句,再复制各表 INSERT,按结果顺序在目标库执行
--
-- 覆盖表(按外键依赖顺序):
-- blade_credit_score_quantification
-- blade_credit_score_category
-- blade_credit_score_item
-- blade_credit_score_item_option
-- blade_credit_rating_standard
--
-- 说明:
-- - 保留原主键与关联 ID,便于 UAT/PROD 与 DEV 配置一致
-- - 仅导出 is_deleted = 0 的有效数据
-- - 目标库若已存在同名评定表(tenant_id + name + is_deleted 唯一),请先处理冲突
-- ============================================================================
-- >>> 请修改为要导出的评分量化表 ID <<<
SET @quantification_id := 0;
-- ----------------------------------------------------------------------------
-- 0. 导出前校验:确认主表存在及子表数量
-- ----------------------------------------------------------------------------
SELECT
q.id,
q.tenant_id,
q.name,
q.status,
q.max_minus_score,
(SELECT COUNT(1) FROM blade_credit_score_category c
WHERE c.quantification_id = q.id AND c.is_deleted = 0) AS category_count,
(SELECT COUNT(1) FROM blade_credit_score_item i
WHERE i.quantification_id = q.id AND i.is_deleted = 0) AS item_count,
(SELECT COUNT(1) FROM blade_credit_score_item_option o
WHERE o.quantification_id = q.id AND o.is_deleted = 0) AS option_count,
(SELECT COUNT(1) FROM blade_credit_rating_standard s
WHERE s.quantification_id = q.id AND s.is_deleted = 0) AS standard_count
FROM blade_credit_score_quantification q
WHERE q.id = @quantification_id
AND q.is_deleted = 0;
-- ----------------------------------------------------------------------------
-- 1. 目标库清理语句(先删子表再删主表;结果复制到 UAT/PROD 优先执行)
-- ----------------------------------------------------------------------------
SELECT CONCAT(
'-- ========== 目标库清理(quantification_id=', @quantification_id, ') ==========\n',
'DELETE FROM `blade_credit_score_item_option` WHERE `quantification_id` = ', @quantification_id, ';\n',
'DELETE FROM `blade_credit_score_item` WHERE `quantification_id` = ', @quantification_id, ';\n',
'DELETE FROM `blade_credit_score_category` WHERE `quantification_id` = ', @quantification_id, ';\n',
'DELETE FROM `blade_credit_rating_standard` WHERE `quantification_id` = ', @quantification_id, ';\n',
'DELETE FROM `blade_credit_score_quantification` WHERE `id` = ', @quantification_id, ';\n',
'-- 若目标库存在同名不同 ID 的评定表,可按名称清理(按需取消注释):\n',
'-- DELETE o FROM `blade_credit_score_item_option` o\n',
'-- INNER JOIN `blade_credit_score_quantification` q ON q.id = o.quantification_id\n',
'-- WHERE q.tenant_id = ', IFNULL(QUOTE((SELECT tenant_id FROM blade_credit_score_quantification WHERE id = @quantification_id LIMIT 1)), 'NULL'),
' AND q.name = ', IFNULL(QUOTE((SELECT name FROM blade_credit_score_quantification WHERE id = @quantification_id LIMIT 1)), 'NULL'),
' AND q.is_deleted = 0;\n',
'-- DELETE i FROM `blade_credit_score_item` i\n',
'-- INNER JOIN `blade_credit_score_quantification` q ON q.id = i.quantification_id\n',
'-- WHERE q.tenant_id = ', IFNULL(QUOTE((SELECT tenant_id FROM blade_credit_score_quantification WHERE id = @quantification_id LIMIT 1)), 'NULL'),
' AND q.name = ', IFNULL(QUOTE((SELECT name FROM blade_credit_score_quantification WHERE id = @quantification_id LIMIT 1)), 'NULL'),
' AND q.is_deleted = 0;\n',
'-- DELETE c FROM `blade_credit_score_category` c\n',
'-- INNER JOIN `blade_credit_score_quantification` q ON q.id = c.quantification_id\n',
'-- WHERE q.tenant_id = ', IFNULL(QUOTE((SELECT tenant_id FROM blade_credit_score_quantification WHERE id = @quantification_id LIMIT 1)), 'NULL'),
' AND q.name = ', IFNULL(QUOTE((SELECT name FROM blade_credit_score_quantification WHERE id = @quantification_id LIMIT 1)), 'NULL'),
' AND q.is_deleted = 0;\n',
'-- DELETE s FROM `blade_credit_rating_standard` s\n',
'-- INNER JOIN `blade_credit_score_quantification` q ON q.id = s.quantification_id\n',
'-- WHERE q.tenant_id = ', IFNULL(QUOTE((SELECT tenant_id FROM blade_credit_score_quantification WHERE id = @quantification_id LIMIT 1)), 'NULL'),
' AND q.name = ', IFNULL(QUOTE((SELECT name FROM blade_credit_score_quantification WHERE id = @quantification_id LIMIT 1)), 'NULL'),
' AND q.is_deleted = 0;\n',
'-- DELETE FROM `blade_credit_score_quantification`\n',
'-- WHERE tenant_id = ', IFNULL(QUOTE((SELECT tenant_id FROM blade_credit_score_quantification WHERE id = @quantification_id LIMIT 1)), 'NULL'),
' AND name = ', IFNULL(QUOTE((SELECT name FROM blade_credit_score_quantification WHERE id = @quantification_id LIMIT 1)), 'NULL'),
' AND is_deleted = 0;'
) AS sql_text;
-- ----------------------------------------------------------------------------
-- 2. 主表 blade_credit_score_quantification
-- ----------------------------------------------------------------------------
SELECT CONCAT(
'INSERT INTO `blade_credit_score_quantification` (',
'`id`, `tenant_id`, `name`, `remark`, `standard_description`, `max_minus_score`, ',
'`create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`, `is_deleted`',
') VALUES (',
id, ', ',
IFNULL(QUOTE(tenant_id), 'NULL'), ', ',
IFNULL(QUOTE(name), 'NULL'), ', ',
IFNULL(QUOTE(remark), 'NULL'), ', ',
IFNULL(QUOTE(standard_description), 'NULL'), ', ',
IFNULL(CAST(max_minus_score AS CHAR), 'NULL'), ', ',
IFNULL(CAST(create_user AS CHAR), 'NULL'), ', ',
IFNULL(CAST(create_dept AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(update_user AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(update_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(status AS CHAR), 'NULL'), ', ',
IFNULL(CAST(is_deleted AS CHAR), '0'),
');'
) AS sql_text
FROM blade_credit_score_quantification
WHERE id = @quantification_id
AND is_deleted = 0;
-- ----------------------------------------------------------------------------
-- 3. 分类 blade_credit_score_category
-- ----------------------------------------------------------------------------
SELECT CONCAT(
'INSERT INTO `blade_credit_score_category` (',
'`id`, `tenant_id`, `quantification_id`, `category_code`, `category_name`, `sort`, ',
'`create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`, `is_deleted`',
') VALUES (',
id, ', ',
IFNULL(QUOTE(tenant_id), 'NULL'), ', ',
quantification_id, ', ',
IFNULL(QUOTE(category_code), 'NULL'), ', ',
IFNULL(QUOTE(category_name), 'NULL'), ', ',
IFNULL(CAST(sort AS CHAR), '0'), ', ',
IFNULL(CAST(create_user AS CHAR), 'NULL'), ', ',
IFNULL(CAST(create_dept AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(update_user AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(update_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(status AS CHAR), 'NULL'), ', ',
IFNULL(CAST(is_deleted AS CHAR), '0'),
');'
) AS sql_text
FROM blade_credit_score_category
WHERE quantification_id = @quantification_id
AND is_deleted = 0
ORDER BY sort, id;
-- ----------------------------------------------------------------------------
-- 4. 评分项目 blade_credit_score_item
-- ----------------------------------------------------------------------------
SELECT CONCAT(
'INSERT INTO `blade_credit_score_item` (',
'`id`, `tenant_id`, `quantification_id`, `category_id`, `category_code`, `item_name`, ',
'`option_type`, `base_value`, `score`, `score_description`, `option_description`, `sort`, ',
'`create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`, `is_deleted`',
') VALUES (',
id, ', ',
IFNULL(QUOTE(tenant_id), 'NULL'), ', ',
quantification_id, ', ',
category_id, ', ',
IFNULL(QUOTE(category_code), 'NULL'), ', ',
IFNULL(QUOTE(item_name), 'NULL'), ', ',
IFNULL(QUOTE(option_type), 'NULL'), ', ',
IFNULL(CAST(base_value AS CHAR), 'NULL'), ', ',
IFNULL(CAST(score AS CHAR), '0.00'), ', ',
IFNULL(QUOTE(score_description), 'NULL'), ', ',
IFNULL(QUOTE(option_description), 'NULL'), ', ',
IFNULL(CAST(sort AS CHAR), '0'), ', ',
IFNULL(CAST(create_user AS CHAR), 'NULL'), ', ',
IFNULL(CAST(create_dept AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(update_user AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(update_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(status AS CHAR), 'NULL'), ', ',
IFNULL(CAST(is_deleted AS CHAR), '0'),
');'
) AS sql_text
FROM blade_credit_score_item
WHERE quantification_id = @quantification_id
AND is_deleted = 0
ORDER BY sort, id;
-- ----------------------------------------------------------------------------
-- 5. 评分项目选项 blade_credit_score_item_option
-- ----------------------------------------------------------------------------
SELECT CONCAT(
'INSERT INTO `blade_credit_score_item_option` (',
'`id`, `tenant_id`, `quantification_id`, `item_id`, `option_name`, ',
'`change_type`, `change_value`, `change_unit`, `score_type`, `score`, `sort`, ',
'`create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`, `is_deleted`',
') VALUES (',
id, ', ',
IFNULL(QUOTE(tenant_id), 'NULL'), ', ',
quantification_id, ', ',
item_id, ', ',
IFNULL(QUOTE(option_name), 'NULL'), ', ',
IFNULL(QUOTE(change_type), 'NULL'), ', ',
IFNULL(CAST(change_value AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(change_unit), 'NULL'), ', ',
IFNULL(QUOTE(score_type), 'NULL'), ', ',
IFNULL(CAST(score AS CHAR), '0.00'), ', ',
IFNULL(CAST(sort AS CHAR), '0'), ', ',
IFNULL(CAST(create_user AS CHAR), 'NULL'), ', ',
IFNULL(CAST(create_dept AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(update_user AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(update_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(status AS CHAR), 'NULL'), ', ',
IFNULL(CAST(is_deleted AS CHAR), '0'),
');'
) AS sql_text
FROM blade_credit_score_item_option
WHERE quantification_id = @quantification_id
AND is_deleted = 0
ORDER BY sort, id;
-- ----------------------------------------------------------------------------
-- 6. 信用等级评估标准 blade_credit_rating_standard
-- ----------------------------------------------------------------------------
SELECT CONCAT(
'INSERT INTO `blade_credit_rating_standard` (',
'`id`, `tenant_id`, `quantification_id`, `credit_level`, ',
'`score_rate_lower`, `score_rate_upper`, `score_rate_increase`, ',
'`credit_limit_lower`, `credit_limit_upper`, `credit_limit_increase`, ',
'`standard_description`, `sort`, ',
'`create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`, `is_deleted`',
') VALUES (',
id, ', ',
IFNULL(QUOTE(tenant_id), 'NULL'), ', ',
quantification_id, ', ',
IFNULL(QUOTE(credit_level), 'NULL'), ', ',
IFNULL(CAST(score_rate_lower AS CHAR), 'NULL'), ', ',
IFNULL(CAST(score_rate_upper AS CHAR), 'NULL'), ', ',
IFNULL(CAST(score_rate_increase AS CHAR), '1'), ', ',
IFNULL(CAST(credit_limit_lower AS CHAR), '0.00'), ', ',
IFNULL(CAST(credit_limit_upper AS CHAR), '0.00'), ', ',
IFNULL(CAST(credit_limit_increase AS CHAR), '0.00'), ', ',
IFNULL(QUOTE(standard_description), 'NULL'), ', ',
IFNULL(CAST(sort AS CHAR), '0'), ', ',
IFNULL(CAST(create_user AS CHAR), 'NULL'), ', ',
IFNULL(CAST(create_dept AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(update_user AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(update_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(status AS CHAR), 'NULL'), ', ',
IFNULL(CAST(is_deleted AS CHAR), '0'),
');'
) AS sql_text
FROM blade_credit_rating_standard
WHERE quantification_id = @quantification_id
AND is_deleted = 0
ORDER BY sort, id;
-- ----------------------------------------------------------------------------
-- 7. 汇总:一次性输出全部 SQL(便于整段复制;MySQL 客户端需允许 GROUP_CONCAT 长度)
-- ----------------------------------------------------------------------------
SET SESSION group_concat_max_len = 1024 * 1024 * 16;
SELECT CONCAT_WS(
'\n',
-- 清理
CONCAT(
'-- ========== 目标库清理(quantification_id=', @quantification_id, ') ==========\n',
'DELETE FROM `blade_credit_score_item_option` WHERE `quantification_id` = ', @quantification_id, ';\n',
'DELETE FROM `blade_credit_score_item` WHERE `quantification_id` = ', @quantification_id, ';\n',
'DELETE FROM `blade_credit_score_category` WHERE `quantification_id` = ', @quantification_id, ';\n',
'DELETE FROM `blade_credit_rating_standard` WHERE `quantification_id` = ', @quantification_id, ';\n',
'DELETE FROM `blade_credit_score_quantification` WHERE `id` = ', @quantification_id, ';'
),
'',
'-- ========== 主表 ==========',
(SELECT GROUP_CONCAT(
CONCAT(
'INSERT INTO `blade_credit_score_quantification` (',
'`id`, `tenant_id`, `name`, `remark`, `standard_description`, `max_minus_score`, ',
'`create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`, `is_deleted`',
') VALUES (',
id, ', ',
IFNULL(QUOTE(tenant_id), 'NULL'), ', ',
IFNULL(QUOTE(name), 'NULL'), ', ',
IFNULL(QUOTE(remark), 'NULL'), ', ',
IFNULL(QUOTE(standard_description), 'NULL'), ', ',
IFNULL(CAST(max_minus_score AS CHAR), 'NULL'), ', ',
IFNULL(CAST(create_user AS CHAR), 'NULL'), ', ',
IFNULL(CAST(create_dept AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(update_user AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(update_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(status AS CHAR), 'NULL'), ', ',
IFNULL(CAST(is_deleted AS CHAR), '0'),
');'
)
SEPARATOR '\n'
)
FROM blade_credit_score_quantification
WHERE id = @quantification_id AND is_deleted = 0),
'',
'-- ========== 分类 ==========',
IFNULL((SELECT GROUP_CONCAT(
CONCAT(
'INSERT INTO `blade_credit_score_category` (',
'`id`, `tenant_id`, `quantification_id`, `category_code`, `category_name`, `sort`, ',
'`create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`, `is_deleted`',
') VALUES (',
id, ', ',
IFNULL(QUOTE(tenant_id), 'NULL'), ', ',
quantification_id, ', ',
IFNULL(QUOTE(category_code), 'NULL'), ', ',
IFNULL(QUOTE(category_name), 'NULL'), ', ',
IFNULL(CAST(sort AS CHAR), '0'), ', ',
IFNULL(CAST(create_user AS CHAR), 'NULL'), ', ',
IFNULL(CAST(create_dept AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(update_user AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(update_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(status AS CHAR), 'NULL'), ', ',
IFNULL(CAST(is_deleted AS CHAR), '0'),
');'
)
ORDER BY sort, id
SEPARATOR '\n'
)
FROM blade_credit_score_category
WHERE quantification_id = @quantification_id AND is_deleted = 0), ''),
'',
'-- ========== 评分项目 ==========',
IFNULL((SELECT GROUP_CONCAT(
CONCAT(
'INSERT INTO `blade_credit_score_item` (',
'`id`, `tenant_id`, `quantification_id`, `category_id`, `category_code`, `item_name`, ',
'`option_type`, `base_value`, `score`, `score_description`, `option_description`, `sort`, ',
'`create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`, `is_deleted`',
') VALUES (',
id, ', ',
IFNULL(QUOTE(tenant_id), 'NULL'), ', ',
quantification_id, ', ',
category_id, ', ',
IFNULL(QUOTE(category_code), 'NULL'), ', ',
IFNULL(QUOTE(item_name), 'NULL'), ', ',
IFNULL(QUOTE(option_type), 'NULL'), ', ',
IFNULL(CAST(base_value AS CHAR), 'NULL'), ', ',
IFNULL(CAST(score AS CHAR), '0.00'), ', ',
IFNULL(QUOTE(score_description), 'NULL'), ', ',
IFNULL(QUOTE(option_description), 'NULL'), ', ',
IFNULL(CAST(sort AS CHAR), '0'), ', ',
IFNULL(CAST(create_user AS CHAR), 'NULL'), ', ',
IFNULL(CAST(create_dept AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(update_user AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(update_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(status AS CHAR), 'NULL'), ', ',
IFNULL(CAST(is_deleted AS CHAR), '0'),
');'
)
ORDER BY sort, id
SEPARATOR '\n'
)
FROM blade_credit_score_item
WHERE quantification_id = @quantification_id AND is_deleted = 0), ''),
'',
'-- ========== 评分项目选项 ==========',
IFNULL((SELECT GROUP_CONCAT(
CONCAT(
'INSERT INTO `blade_credit_score_item_option` (',
'`id`, `tenant_id`, `quantification_id`, `item_id`, `option_name`, ',
'`change_type`, `change_value`, `change_unit`, `score_type`, `score`, `sort`, ',
'`create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`, `is_deleted`',
') VALUES (',
id, ', ',
IFNULL(QUOTE(tenant_id), 'NULL'), ', ',
quantification_id, ', ',
item_id, ', ',
IFNULL(QUOTE(option_name), 'NULL'), ', ',
IFNULL(QUOTE(change_type), 'NULL'), ', ',
IFNULL(CAST(change_value AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(change_unit), 'NULL'), ', ',
IFNULL(QUOTE(score_type), 'NULL'), ', ',
IFNULL(CAST(score AS CHAR), '0.00'), ', ',
IFNULL(CAST(sort AS CHAR), '0'), ', ',
IFNULL(CAST(create_user AS CHAR), 'NULL'), ', ',
IFNULL(CAST(create_dept AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(update_user AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(update_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(status AS CHAR), 'NULL'), ', ',
IFNULL(CAST(is_deleted AS CHAR), '0'),
');'
)
ORDER BY sort, id
SEPARATOR '\n'
)
FROM blade_credit_score_item_option
WHERE quantification_id = @quantification_id AND is_deleted = 0), ''),
'',
'-- ========== 信用等级评估标准 ==========',
IFNULL((SELECT GROUP_CONCAT(
CONCAT(
'INSERT INTO `blade_credit_rating_standard` (',
'`id`, `tenant_id`, `quantification_id`, `credit_level`, ',
'`score_rate_lower`, `score_rate_upper`, `score_rate_increase`, ',
'`credit_limit_lower`, `credit_limit_upper`, `credit_limit_increase`, ',
'`standard_description`, `sort`, ',
'`create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`, `is_deleted`',
') VALUES (',
id, ', ',
IFNULL(QUOTE(tenant_id), 'NULL'), ', ',
quantification_id, ', ',
IFNULL(QUOTE(credit_level), 'NULL'), ', ',
IFNULL(CAST(score_rate_lower AS CHAR), 'NULL'), ', ',
IFNULL(CAST(score_rate_upper AS CHAR), 'NULL'), ', ',
IFNULL(CAST(score_rate_increase AS CHAR), '1'), ', ',
IFNULL(CAST(credit_limit_lower AS CHAR), '0.00'), ', ',
IFNULL(CAST(credit_limit_upper AS CHAR), '0.00'), ', ',
IFNULL(CAST(credit_limit_increase AS CHAR), '0.00'), ', ',
IFNULL(QUOTE(standard_description), 'NULL'), ', ',
IFNULL(CAST(sort AS CHAR), '0'), ', ',
IFNULL(CAST(create_user AS CHAR), 'NULL'), ', ',
IFNULL(CAST(create_dept AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(update_user AS CHAR), 'NULL'), ', ',
IFNULL(QUOTE(DATE_FORMAT(update_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ',
IFNULL(CAST(status AS CHAR), 'NULL'), ', ',
IFNULL(CAST(is_deleted AS CHAR), '0'),
');'
)
ORDER BY sort, id
SEPARATOR '\n'
)
FROM blade_credit_rating_standard
WHERE quantification_id = @quantification_id AND is_deleted = 0), '')
) AS full_export_sql;
@@ -0,0 +1,5 @@
-- 评分量化表:减分项目最高扣分限制
-- 客商自评/复评时,所有减分项合计扣分不得超过该值;超出则按该上限计分
ALTER TABLE `blade_credit_score_quantification`
ADD COLUMN `max_minus_score` decimal(10,2) NULL DEFAULT NULL COMMENT '减分项目最高扣分限制' AFTER `standard_description`;
+5 -2
View File
@@ -18,21 +18,24 @@ CREATE TABLE `blade_customer_archive` (
`registered_region_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '注册地址行政区划',
`registered_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正式',
@@ -0,0 +1,7 @@
-- 客商档案:实缴资本、营业期限开始日、已使用额度;经营范围加长;联系电话改为可空(改由联系人维护)
ALTER TABLE `blade_customer_archive`
ADD COLUMN `business_start_date` date NULL DEFAULT NULL COMMENT '营业期限开始日' AFTER `business_term_type`,
ADD COLUMN `paid_in_capital` decimal(18,2) NULL DEFAULT NULL COMMENT '实缴资本(万元)' AFTER `registered_capital`,
ADD COLUMN `used_credit_limit` decimal(18,2) NULL DEFAULT NULL COMMENT '已使用额度(万元)' AFTER `apply_credit_limit`,
MODIFY COLUMN `business_scope` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '经营范围',
MODIFY COLUMN `contact_phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '联系电话';
@@ -0,0 +1,5 @@
-- 机构表:新增「是否业务部门」字段,默认否
-- 全量 OA 同步时须保留本地已配置值,不得覆盖
ALTER TABLE `blade_dept`
ADD COLUMN `is_business_dept` tinyint NOT NULL DEFAULT 0 COMMENT '是否业务部门:0否,1是' AFTER `is_platform_company`;
@@ -9,6 +9,7 @@ CREATE TABLE IF NOT EXISTS `blade_formal_settlement` (
`dept_id` bigint(20) DEFAULT NULL, `dept_name` varchar(100) DEFAULT NULL, `contract_id` bigint(20) NOT NULL,
`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 '已收已付合计含预付',
@@ -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 '创建人',
@@ -0,0 +1,12 @@
-- 小程序用户:数据权限范围、数据层级范围、自定义组织数据权限
ALTER TABLE `blade_mp_user`
ADD COLUMN `data_scope_range` tinyint NOT NULL DEFAULT 1 COMMENT '数据权限范围:1所属组织,2全部组织,3自定义' AFTER `dept_id`,
ADD COLUMN `data_level_range` tinyint NOT NULL DEFAULT 1 COMMENT '数据层级范围:1包含下级,2仅本级' AFTER `data_scope_range`;
CREATE TABLE IF NOT EXISTS `blade_mp_user_data_scope` (
`id` bigint NOT NULL COMMENT '主键',
`user_id` bigint NOT NULL COMMENT '小程序用户主键',
`dept_id` bigint NOT NULL COMMENT '组织主键',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_blade_mp_user_data_scope_user_dept` (`user_id`, `dept_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='小程序用户组织数据权限';
@@ -24,6 +24,8 @@ CREATE TABLE IF NOT EXISTS `blade_pre_settlement` (
`payee_name` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '收款方',
`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 '汇率日期',
@@ -0,0 +1,8 @@
-- 预结算单、正式结算单:业务模式、运输方式(主表头字段)
ALTER TABLE `blade_pre_settlement`
ADD COLUMN `business_model` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '业务模式' AFTER `settlement_amount`,
ADD COLUMN `transport_type` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '运输方式' AFTER `business_model`;
ALTER TABLE `blade_formal_settlement`
ADD COLUMN `business_model` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '业务模式' AFTER `settlement_amount`,
ADD COLUMN `transport_type` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '运输方式' AFTER `business_model`;
@@ -7,8 +7,12 @@
-- 3. 只统计各功能列表对应的主表。随主单保存的明细、联系人、附件、变更流水不重复计数。
-- 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