Merge branch 'dev' of https://git.websoft.top/b2894lxlx/tms-erp-api into dev
This commit is contained in:
+7
@@ -150,6 +150,13 @@ public class Dept extends TenantEntity {
|
||||
@Schema(description = "合同编码前缀")
|
||||
private String contractCodePrefix;
|
||||
|
||||
/**
|
||||
* 合同初始流水号(该部门当年首个合同时从此号起编,范围1-999)
|
||||
*/
|
||||
@Schema(description = "合同初始流水号")
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||
private Integer contractInitialSerial;
|
||||
|
||||
/**
|
||||
* 组织简称
|
||||
*/
|
||||
|
||||
+3
@@ -61,6 +61,9 @@ public class OaPersonSyncPageVO implements Serializable {
|
||||
@Schema(description = "本页跳过条数")
|
||||
private Integer skippedCount;
|
||||
|
||||
@Schema(description = "全量同步结束时删除的、不在本次 OA 数据中的 is_oa=2 人员数")
|
||||
private Integer removedCount;
|
||||
|
||||
@Schema(description = "是否已到最后一页")
|
||||
private Boolean finished;
|
||||
|
||||
|
||||
+2
@@ -36,6 +36,8 @@ public class PaymentApplicationSaveRequest implements Serializable {
|
||||
private List<Long> settlementIds;
|
||||
private Long preSettlementId;
|
||||
private List<Long> preSettlementIds;
|
||||
/** 预结算单号。驳回重提时用于在主键失真后找回预结算单 */
|
||||
private String preSettlementNo;
|
||||
private Long projectId;
|
||||
private String projectName;
|
||||
private Long deptId;
|
||||
|
||||
+6
@@ -20,6 +20,12 @@ public class WaybillImportBatchVO extends WaybillImportBatch {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private String importTypeName;
|
||||
private String statusName;
|
||||
/** 结算状态:unsettled/partial/settled */
|
||||
@Schema(description = "结算状态")
|
||||
private String settlementStatus;
|
||||
/** 结算状态名称:未结算/部分结算/已结算 */
|
||||
@Schema(description = "结算状态名称")
|
||||
private String settlementStatusName;
|
||||
private String createUserName;
|
||||
private String updateUserName;
|
||||
}
|
||||
|
||||
+2
-2
@@ -49,8 +49,8 @@ public interface DeptConvert {
|
||||
@Mapping(source = "modified", target = "oaModified")
|
||||
Dept baseConvert(OACompanyResponse company);
|
||||
|
||||
@Mapping(source = "departmentname", target = "deptName")
|
||||
@Mapping(source = "departmentmark", target = "fullName")
|
||||
@Mapping(source = "departmentmark", target = "deptName")
|
||||
@Mapping(source = "departmentname", target = "fullName")
|
||||
@Mapping(source = "supdepid", target = "parentCode")
|
||||
@Mapping(source = "subcompanyid1", target = "belongCompanyCode")
|
||||
@Mapping(source = "created", target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
|
||||
+4
-3
@@ -148,7 +148,7 @@ public interface UserConvert {
|
||||
/**
|
||||
* oa人员转本系统用户部门
|
||||
* @param person
|
||||
* @param userMap 所有用户手机号与id map
|
||||
* @param userMap OA登录名与用户id
|
||||
* @return
|
||||
*/
|
||||
default UserDept person2userDept(OAPersonResponse person, Map<String, Long> userMap) {
|
||||
@@ -162,8 +162,9 @@ public interface UserConvert {
|
||||
userDept.setStatus(status);
|
||||
// 排序
|
||||
userDept.setSort(OAUtils.parseInt(person.getDsporder()));
|
||||
// 用户id
|
||||
userDept.setUserId(userMap.get(resolveAccount(person)));
|
||||
// 用户id,按 OA 登录名关联本批已新增或更新的用户
|
||||
String oaLoginId = person == null ? null : StringUtils.trimToNull(person.getLoginid());
|
||||
userDept.setUserId(oaLoginId == null ? null : userMap.get(oaLoginId));
|
||||
userDept.setSyncTime(new Date());
|
||||
return userDept;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<result column="dept_code" property="deptCode"/>
|
||||
<result column="kingdee_dept_code" property="kingdeeDeptCode"/>
|
||||
<result column="contract_code_prefix" property="contractCodePrefix"/>
|
||||
<result column="contract_initial_serial" property="contractInitialSerial"/>
|
||||
<result column="short_name" property="shortName"/>
|
||||
<result column="pinyin_mnemonic" property="pinyinMnemonic"/>
|
||||
<result column="mnemonic_code" property="mnemonicCode"/>
|
||||
@@ -36,6 +37,7 @@
|
||||
<result column="dept_code" property="deptCode"/>
|
||||
<result column="kingdee_dept_code" property="kingdeeDeptCode"/>
|
||||
<result column="contract_code_prefix" property="contractCodePrefix"/>
|
||||
<result column="contract_initial_serial" property="contractInitialSerial"/>
|
||||
<result column="short_name" property="shortName"/>
|
||||
<result column="pinyin_mnemonic" property="pinyinMnemonic"/>
|
||||
<result column="mnemonic_code" property="mnemonicCode"/>
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
bud.user_id
|
||||
from
|
||||
blade_user_dept bud
|
||||
left join blade_dept bd on bd.dept_code = bud.dept_code
|
||||
left join blade_dept bd on bd.id = bud.dept_id and bd.is_deleted = 0
|
||||
where
|
||||
bd.id is not null
|
||||
and bud.is_deleted = 0
|
||||
|
||||
+11
@@ -687,6 +687,7 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
}
|
||||
validateDeptCategory(dept);
|
||||
validateDeptCode(dept, parent);
|
||||
validateContractInitialSerial(dept);
|
||||
return saveOrUpdate(dept);
|
||||
}
|
||||
|
||||
@@ -874,6 +875,16 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
}
|
||||
}
|
||||
|
||||
private void validateContractInitialSerial(Dept dept) {
|
||||
Integer serial = dept.getContractInitialSerial();
|
||||
if (serial == null) {
|
||||
return;
|
||||
}
|
||||
if (serial < 1 || serial > 999) {
|
||||
throw new ServiceException("合同初始流水号范围为1到999");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateDeptCode(Dept dept, Dept parent) {
|
||||
String deptCode = dept.getDeptCode();
|
||||
// 组织编码非必填,为空时存 null,避免唯一索引冲突
|
||||
|
||||
+31
-10
@@ -277,6 +277,11 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
}, 10000, ComposeLogUtil.getLastLog()::info).accept(oaPersons::addAll);
|
||||
OaUserListSyncHelper.OaOrgIndex orgIndex = oaUserListSyncHelper.syncOrgsFromPersons(oaPersons);
|
||||
OaUserListSyncHelper.PersonSyncCount personSyncCount = oaUserListSyncHelper.handlePerson(oaPersons, orgIndex);
|
||||
if (startTime == null) {
|
||||
oaUserListSyncHelper.removeOaUsersAbsentFrom(
|
||||
oaUserListSyncHelper.extractOaLoginIds(oaPersons),
|
||||
oaUserListSyncHelper.extractWorkcodes(oaPersons));
|
||||
}
|
||||
CacheUtil.clear(USER_CACHE);
|
||||
CacheUtil.clear(SYS_CACHE);
|
||||
CacheUtil.clear(SYS_CACHE, Boolean.FALSE);
|
||||
@@ -295,9 +300,10 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
private OaPersonSyncPageVO syncPersonFromOaPage(int current, int size, boolean incremental, String modifiedSince) {
|
||||
int pageNo = current < 1 ? 1 : current;
|
||||
int pageSize = size < 1 ? 20 : Math.min(size, 200);
|
||||
// 全量同步首页开启原始数据收集,结束后按父级 id 写入 JSON
|
||||
// 全量同步首页开启原始数据收集,并开始累积登录名,最后一页再删除本地多余人员
|
||||
if (!incremental && pageNo == 1) {
|
||||
oaSyncRawDumpHelper.beginFullDump();
|
||||
oaUserListSyncHelper.beginFullSyncReconcile();
|
||||
}
|
||||
IncrementalStart incrementalStart = resolvePersonIncrementalStart(incremental, modifiedSince);
|
||||
OAPersonSearch personSearch = buildPersonSearch(incrementalStart.getStartTime());
|
||||
@@ -314,9 +320,17 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
long totalSize = responseData.getTotalSize() == null ? 0L : responseData.getTotalSize();
|
||||
if (!incremental) {
|
||||
oaSyncRawDumpHelper.collectPersons(oaPersons);
|
||||
oaUserListSyncHelper.collectFullSyncPersons(oaPersons);
|
||||
}
|
||||
OaUserListSyncHelper.OaOrgIndex orgIndex = oaUserListSyncHelper.syncOrgsFromPersons(oaPersons);
|
||||
OaUserListSyncHelper.PersonSyncCount personSyncCount = oaUserListSyncHelper.handlePerson(oaPersons, orgIndex);
|
||||
boolean finished = oaPersons.isEmpty()
|
||||
|| oaPersons.size() < pageSize
|
||||
|| (long) pageNo * pageSize >= totalSize;
|
||||
int removedCount = personSyncCount.getRemovedCount();
|
||||
if (!incremental && finished) {
|
||||
removedCount += oaUserListSyncHelper.finishFullSyncReconcile();
|
||||
}
|
||||
CacheUtil.clear(USER_CACHE);
|
||||
CacheUtil.clear(SYS_CACHE);
|
||||
CacheUtil.clear(SYS_CACHE, Boolean.FALSE);
|
||||
@@ -327,17 +341,15 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
pageVO.setFetchedCount(oaPersons.size());
|
||||
pageVO.setSyncedCount(personSyncCount.getSyncedCount());
|
||||
pageVO.setSkippedCount(personSyncCount.getSkippedCount());
|
||||
pageVO.setRemovedCount(removedCount);
|
||||
pageVO.setModifiedSince(incrementalStart.getModifiedSince());
|
||||
boolean finished = oaPersons.isEmpty()
|
||||
|| oaPersons.size() < pageSize
|
||||
|| (long) pageNo * pageSize >= totalSize;
|
||||
pageVO.setFinished(finished);
|
||||
if (!incremental && finished) {
|
||||
pageVO.setDumpFilePath(oaSyncRawDumpHelper.finishFullDump());
|
||||
}
|
||||
ComposeLogUtil.getLastLog().info("OA人员分页同步完成 mode={} {}/{},成功{},跳过{}",
|
||||
ComposeLogUtil.getLastLog().info("OA人员分页同步完成 mode={} {}/{},成功{},跳过{},删除{}",
|
||||
incremental ? "incremental" : "full", pageNo, totalSize,
|
||||
personSyncCount.getSyncedCount(), personSyncCount.getSkippedCount());
|
||||
personSyncCount.getSyncedCount(), personSyncCount.getSkippedCount(), removedCount);
|
||||
return pageVO;
|
||||
}
|
||||
|
||||
@@ -761,7 +773,7 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
.filter(oaDept -> syncAll
|
||||
|| rootCompanyId == null
|
||||
|| !rootCompanyId.equals(oaDept.getSubcompanyid1())
|
||||
|| (OAConstant.ROOT_COMPANY_ID.equals(oaDept.getSupdepid()) && rootCompanyDeptNames.contains(oaDept.getDepartmentname())))
|
||||
|| (OAConstant.ROOT_COMPANY_ID.equals(oaDept.getSupdepid()) && rootCompanyDeptNames.contains(oaDept.getDepartmentmark())))
|
||||
.map(dept -> deptConvert.dept2dept(dept, companyByOaId))
|
||||
.toList();
|
||||
int filteredSkipCount = oaDepts.size() - allParam.size();
|
||||
@@ -921,8 +933,9 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
.eq(Dept::getDeptCategory, deptCategory.getCode())
|
||||
.isNotNull(Dept::getOaId)
|
||||
.ne(Dept::getOaId, "")
|
||||
.select(Dept::getId, Dept::getDeptCode, Dept::getOaModified, Dept::getParentId,
|
||||
Dept::getOaId, Dept::getBelongCompanyCode, Dept::getParentCode, Dept::getOaSupSubComId));
|
||||
.select(Dept::getId, Dept::getDeptCode, Dept::getDeptName, Dept::getFullName,
|
||||
Dept::getOaModified, Dept::getParentId, Dept::getOaId, Dept::getBelongCompanyCode,
|
||||
Dept::getParentCode, Dept::getOaSupSubComId));
|
||||
Map<String, Dept> byOaId = new HashMap<>();
|
||||
for (Dept dept : deptList) {
|
||||
if (StringUtils.isNotBlank(dept.getOaId())) {
|
||||
@@ -1047,7 +1060,7 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
|
||||
/**
|
||||
* oaModified 一致则跳过更新;任一侧为空无法确认一致,执行更新以补齐字段。
|
||||
* 组织编码已按新规则重算、或父子挂载与库中不一致时,即使 modified 相同也要更新。
|
||||
* 组织编码已按新规则重算、父子挂载不一致、或名称字段与当前映射不一致时,即使 modified 相同也要更新。
|
||||
*/
|
||||
private boolean shouldSkipOaUpdate(Dept existing, Dept incoming) {
|
||||
if (existing == null) {
|
||||
@@ -1066,6 +1079,14 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
StringUtils.trimToEmpty(incoming.getParentCode()))) {
|
||||
return false;
|
||||
}
|
||||
if (incoming != null && !StringUtils.equals(StringUtils.trimToEmpty(existing.getDeptName()),
|
||||
StringUtils.trimToEmpty(incoming.getDeptName()))) {
|
||||
return false;
|
||||
}
|
||||
if (incoming != null && !StringUtils.equals(StringUtils.trimToEmpty(existing.getFullName()),
|
||||
StringUtils.trimToEmpty(incoming.getFullName()))) {
|
||||
return false;
|
||||
}
|
||||
String localModified = StringUtils.trimToEmpty(existing.getOaModified());
|
||||
String remoteModified = StringUtils.trimToEmpty(incoming == null ? null : incoming.getOaModified());
|
||||
if (localModified.isEmpty() || remoteModified.isEmpty()) {
|
||||
|
||||
+473
-94
@@ -30,6 +30,7 @@ import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springblade.common.constant.CommonConstant;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.constant.BladeConstant;
|
||||
import org.springblade.core.tool.utils.DigestUtil;
|
||||
@@ -55,10 +56,12 @@ import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.springblade.common.constant.CommonConstant.DEFAULT_PARAM_PASSWORD;
|
||||
@@ -76,6 +79,22 @@ public class OaUserListSyncHelper {
|
||||
|
||||
private static final int MAX_DEPT_ID_LENGTH = 2000;
|
||||
|
||||
private static final int OA_LOGIN_ID_BATCH_SIZE = 500;
|
||||
|
||||
/**
|
||||
* OA 人员状态:5 离职。离职人员不入库,已有 is_oa=2 账号直接删除。
|
||||
*/
|
||||
private static final String OA_PERSON_STATUS_RESIGNED = "5";
|
||||
|
||||
/**
|
||||
* 全量同步跨页累积的 OA 登录名。分页接口每页独立事务,删除必须等最后一页再执行。
|
||||
*/
|
||||
private final Set<String> fullSyncLoginIds = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private final Set<String> fullSyncWorkcodes = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private volatile boolean fullSyncCollecting;
|
||||
|
||||
private final IDeptService deptService;
|
||||
private final IUserService userService;
|
||||
private final UserConvert userConvert;
|
||||
@@ -390,83 +409,327 @@ public class OaUserListSyncHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步人员并绑定到三级部门
|
||||
* 全量同步从第一页开始收集 OA 登录名,最后一页再删除本地多余的 is_oa=2 人员。
|
||||
*/
|
||||
public synchronized void beginFullSyncReconcile() {
|
||||
this.fullSyncLoginIds.clear();
|
||||
this.fullSyncWorkcodes.clear();
|
||||
this.fullSyncCollecting = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 累积本页 OA 人员登录名,供全量同步结束后比对删除。
|
||||
*
|
||||
* @param oaPersons 本页 OA 人员
|
||||
*/
|
||||
public synchronized void collectFullSyncPersons(List<OAPersonResponse> oaPersons) {
|
||||
if (!this.fullSyncCollecting || CollectionUtil.isEmpty(oaPersons)) {
|
||||
return;
|
||||
}
|
||||
this.fullSyncLoginIds.addAll(this.extractOaLoginIds(oaPersons));
|
||||
this.fullSyncWorkcodes.addAll(this.extractWorkcodes(oaPersons));
|
||||
}
|
||||
|
||||
/**
|
||||
* 全量同步结束后,删除登录名不在本次 OA 数据中的 is_oa=2 人员。
|
||||
*
|
||||
* @return 删除人数;未开始收集或登录名集合为空时返回 0,避免误删全部 OA 人员
|
||||
*/
|
||||
public synchronized int finishFullSyncReconcile() {
|
||||
if (!this.fullSyncCollecting) {
|
||||
return 0;
|
||||
}
|
||||
this.fullSyncCollecting = false;
|
||||
Set<String> oaLoginIds = new LinkedHashSet<>(this.fullSyncLoginIds);
|
||||
Set<String> workcodes = new LinkedHashSet<>(this.fullSyncWorkcodes);
|
||||
this.fullSyncLoginIds.clear();
|
||||
this.fullSyncWorkcodes.clear();
|
||||
return this.removeOaUsersAbsentFrom(oaLoginIds, workcodes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取仍在职的 OA 人员登录名。离职人员不计入,全量同步结束时会删掉对应本地账号。
|
||||
*
|
||||
* @param oaPersons OA人员
|
||||
* @return 去重后的登录名
|
||||
*/
|
||||
public Set<String> extractOaLoginIds(List<OAPersonResponse> oaPersons) {
|
||||
Set<String> oaLoginIds = new LinkedHashSet<>();
|
||||
if (CollectionUtil.isEmpty(oaPersons)) {
|
||||
return oaLoginIds;
|
||||
}
|
||||
for (OAPersonResponse oaPerson : oaPersons) {
|
||||
String oaLoginId = this.resolveOaLoginId(oaPerson);
|
||||
if (oaLoginId != null && !this.isOaResigned(oaPerson)) {
|
||||
oaLoginIds.add(oaLoginId);
|
||||
}
|
||||
}
|
||||
return oaLoginIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取仍在职人员的工号。没有登录名时靠工号保留本地账号。
|
||||
*
|
||||
* @param oaPersons OA人员
|
||||
* @return 去重后的工号
|
||||
*/
|
||||
public Set<String> extractWorkcodes(List<OAPersonResponse> oaPersons) {
|
||||
Set<String> workcodes = new LinkedHashSet<>();
|
||||
if (CollectionUtil.isEmpty(oaPersons)) {
|
||||
return workcodes;
|
||||
}
|
||||
for (OAPersonResponse oaPerson : oaPersons) {
|
||||
String workcode = this.resolveWorkcode(oaPerson);
|
||||
if (workcode != null && !this.isOaResigned(oaPerson)) {
|
||||
workcodes.add(workcode);
|
||||
}
|
||||
}
|
||||
return workcodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除当前租户中 is_oa=2、且既不在本次登录名里、也不是无登录名工号人员的账号。
|
||||
* 登录名和工号都没收集到时不删除。当前登录账号即使不在同步数据中也保留。
|
||||
*
|
||||
* @param oaLoginIds 本次全量同步出现过的 OA 登录名
|
||||
* @param workcodes 本次全量同步出现过的工号
|
||||
* @return 删除人数
|
||||
*/
|
||||
public int removeOaUsersAbsentFrom(Set<String> oaLoginIds, Set<String> workcodes) {
|
||||
boolean noLoginIds = CollectionUtil.isEmpty(oaLoginIds);
|
||||
boolean noWorkcodes = CollectionUtil.isEmpty(workcodes);
|
||||
if (noLoginIds && noWorkcodes) {
|
||||
ComposeLogUtil.getLastLog().warn("全量同步未收集到OA登录名和工号,跳过删除本地 is_oa=2 人员");
|
||||
return 0;
|
||||
}
|
||||
Set<String> loginIdSet = noLoginIds ? Set.of() : oaLoginIds;
|
||||
Set<String> workcodeSet = noWorkcodes ? Set.of() : workcodes;
|
||||
String tenantId = this.resolveTenantId();
|
||||
List<User> oaUsers = this.userService.list(Wrappers.<User>lambdaQuery()
|
||||
.select(User::getId, User::getOaLoginId, User::getCode)
|
||||
.eq(User::getTenantId, tenantId)
|
||||
.eq(User::getIsOa, CommonConstant.YES));
|
||||
Long currentUserId = AuthUtil.getUserId();
|
||||
List<Long> removeIds = new ArrayList<>();
|
||||
for (User oaUser : oaUsers) {
|
||||
if (oaUser.getId() == null) {
|
||||
continue;
|
||||
}
|
||||
if (currentUserId != null && currentUserId > 0 && currentUserId.equals(oaUser.getId())) {
|
||||
ComposeLogUtil.getLastLog().warn("全量同步跳过删除当前登录账号 userId={}", oaUser.getId());
|
||||
continue;
|
||||
}
|
||||
String oaLoginId = StringUtils.trimToNull(oaUser.getOaLoginId());
|
||||
String workcode = StringUtils.trimToNull(oaUser.getCode());
|
||||
boolean keptByLoginId = oaLoginId != null && loginIdSet.contains(oaLoginId);
|
||||
boolean keptByWorkcode = oaLoginId == null && workcode != null && workcodeSet.contains(workcode);
|
||||
if (!keptByLoginId && !keptByWorkcode) {
|
||||
removeIds.add(oaUser.getId());
|
||||
}
|
||||
}
|
||||
if (removeIds.isEmpty()) {
|
||||
ComposeLogUtil.getLastLog().info("全量同步无需删除 is_oa=2 人员");
|
||||
return 0;
|
||||
}
|
||||
return this.deleteOaUsers(removeIds, "全量同步删除不在OA数据中的 is_oa=2 人员");
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 OA 登录名删除已有 is_oa=2 人员。用于离职人员,全量和增量同步都会执行。
|
||||
*
|
||||
* @param tenantId 租户
|
||||
* @param oaLoginIds 离职人员登录名
|
||||
* @return 删除人数
|
||||
*/
|
||||
private int removeOaUsersByLoginIds(String tenantId, Collection<String> oaLoginIds) {
|
||||
if (CollectionUtil.isEmpty(oaLoginIds)) {
|
||||
return 0;
|
||||
}
|
||||
List<String> oaLoginIdList = oaLoginIds.stream()
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (oaLoginIdList.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
Long currentUserId = AuthUtil.getUserId();
|
||||
List<Long> removeIds = new ArrayList<>();
|
||||
for (int fromIndex = 0; fromIndex < oaLoginIdList.size(); fromIndex += OA_LOGIN_ID_BATCH_SIZE) {
|
||||
List<String> oaLoginIdBatch = oaLoginIdList.subList(fromIndex, Math.min(fromIndex + OA_LOGIN_ID_BATCH_SIZE, oaLoginIdList.size()));
|
||||
this.userService.list(Wrappers.<User>lambdaQuery()
|
||||
.select(User::getId, User::getOaLoginId)
|
||||
.eq(User::getTenantId, tenantId)
|
||||
.eq(User::getIsOa, CommonConstant.YES)
|
||||
.in(User::getOaLoginId, oaLoginIdBatch))
|
||||
.forEach(oaUser -> {
|
||||
if (oaUser.getId() == null) {
|
||||
return;
|
||||
}
|
||||
if (currentUserId != null && currentUserId > 0 && currentUserId.equals(oaUser.getId())) {
|
||||
ComposeLogUtil.getLastLog().warn("同步跳过删除当前登录账号 userId={}", oaUser.getId());
|
||||
return;
|
||||
}
|
||||
removeIds.add(oaUser.getId());
|
||||
});
|
||||
}
|
||||
return this.deleteOaUsers(removeIds, "同步删除OA离职人员");
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除没有登录名、且工号属于离职人员的 is_oa=2 账号。
|
||||
*
|
||||
* @param tenantId 租户
|
||||
* @param workcodes 离职人员工号
|
||||
* @return 删除人数
|
||||
*/
|
||||
private int removeOaUsersByBlankLoginWorkcodes(String tenantId, Collection<String> workcodes) {
|
||||
if (CollectionUtil.isEmpty(workcodes)) {
|
||||
return 0;
|
||||
}
|
||||
List<String> workcodeList = workcodes.stream()
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (workcodeList.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
Long currentUserId = AuthUtil.getUserId();
|
||||
List<Long> removeIds = new ArrayList<>();
|
||||
for (int fromIndex = 0; fromIndex < workcodeList.size(); fromIndex += OA_LOGIN_ID_BATCH_SIZE) {
|
||||
List<String> workcodeBatch = workcodeList.subList(fromIndex, Math.min(fromIndex + OA_LOGIN_ID_BATCH_SIZE, workcodeList.size()));
|
||||
this.userService.list(Wrappers.<User>lambdaQuery()
|
||||
.select(User::getId, User::getOaLoginId, User::getCode)
|
||||
.eq(User::getTenantId, tenantId)
|
||||
.eq(User::getIsOa, CommonConstant.YES)
|
||||
.in(User::getCode, workcodeBatch)
|
||||
.and(wrapper -> wrapper.isNull(User::getOaLoginId).or().eq(User::getOaLoginId, "")))
|
||||
.forEach(oaUser -> {
|
||||
if (oaUser.getId() == null) {
|
||||
return;
|
||||
}
|
||||
if (currentUserId != null && currentUserId > 0 && currentUserId.equals(oaUser.getId())) {
|
||||
ComposeLogUtil.getLastLog().warn("同步跳过删除当前登录账号 userId={}", oaUser.getId());
|
||||
return;
|
||||
}
|
||||
removeIds.add(oaUser.getId());
|
||||
});
|
||||
}
|
||||
return this.deleteOaUsers(removeIds, "同步删除无登录名的OA离职人员");
|
||||
}
|
||||
|
||||
/**
|
||||
* 逻辑删除用户及其部门关系。
|
||||
*
|
||||
* @param removeIds 用户 id
|
||||
* @param logMessage 日志说明
|
||||
* @return 删除人数
|
||||
*/
|
||||
private int deleteOaUsers(List<Long> removeIds, String logMessage) {
|
||||
if (CollectionUtil.isEmpty(removeIds)) {
|
||||
return 0;
|
||||
}
|
||||
for (int fromIndex = 0; fromIndex < removeIds.size(); fromIndex += OA_LOGIN_ID_BATCH_SIZE) {
|
||||
List<Long> removeIdBatch = removeIds.subList(fromIndex, Math.min(fromIndex + OA_LOGIN_ID_BATCH_SIZE, removeIds.size()));
|
||||
this.userService.deleteLogic(removeIdBatch);
|
||||
this.userDeptService.remove(Wrappers.<UserDept>lambdaQuery().in(UserDept::getUserId, removeIdBatch));
|
||||
}
|
||||
ComposeLogUtil.getLastLog().info("{}:{}", logMessage, removeIds.size());
|
||||
return removeIds.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* OA 状态 5 表示离职。
|
||||
*/
|
||||
private boolean isOaResigned(OAPersonResponse oaPerson) {
|
||||
return oaPerson != null && OA_PERSON_STATUS_RESIGNED.equals(StringUtils.trimToEmpty(oaPerson.getStatus()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步人员并绑定到三级部门。有登录名按登录名更新,没有登录名但有工号则按工号更新且不回填登录名。离职人员直接删除。
|
||||
*
|
||||
* @param oaPersons OA人员
|
||||
* @param orgIndex 组织索引
|
||||
* @return 本批同步成功与跳过数量
|
||||
* @return 本批同步成功、跳过与删除数量
|
||||
*/
|
||||
public PersonSyncCount handlePerson(List<OAPersonResponse> oaPersons, OaOrgIndex orgIndex) {
|
||||
if (CollectionUtil.isEmpty(oaPersons)) {
|
||||
return new PersonSyncCount(0, 0);
|
||||
return new PersonSyncCount(0, 0, 0);
|
||||
}
|
||||
Map<String, OAPersonResponse> uniquePersonMap = new LinkedHashMap<>();
|
||||
Set<String> resignedLoginIds = new LinkedHashSet<>();
|
||||
Set<String> resignedWorkcodes = new LinkedHashSet<>();
|
||||
int skippedCount = 0;
|
||||
for (OAPersonResponse oaPerson : oaPersons) {
|
||||
String account = userConvert.resolveAccount(oaPerson);
|
||||
if (StringUtils.isBlank(account)) {
|
||||
String syncKey = this.resolveSyncKey(oaPerson);
|
||||
if (syncKey == null) {
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
uniquePersonMap.putIfAbsent(account, oaPerson);
|
||||
}
|
||||
if (uniquePersonMap.isEmpty()) {
|
||||
ComposeLogUtil.getLastLog().warn("OA人员均缺少loginid/工号/手机号,跳过人员同步");
|
||||
return new PersonSyncCount(0, skippedCount);
|
||||
String oaLoginId = this.resolveOaLoginId(oaPerson);
|
||||
String workcode = this.resolveWorkcode(oaPerson);
|
||||
if (this.isOaResigned(oaPerson)) {
|
||||
uniquePersonMap.remove(syncKey);
|
||||
if (oaLoginId != null) {
|
||||
resignedLoginIds.add(oaLoginId);
|
||||
} else if (workcode != null) {
|
||||
resignedWorkcodes.add(workcode);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (oaLoginId != null) {
|
||||
resignedLoginIds.remove(oaLoginId);
|
||||
} else if (workcode != null) {
|
||||
resignedWorkcodes.remove(workcode);
|
||||
}
|
||||
uniquePersonMap.put(syncKey, oaPerson);
|
||||
}
|
||||
String tenantId = resolveTenantId();
|
||||
int removedCount = this.removeOaUsersByLoginIds(tenantId, resignedLoginIds)
|
||||
+ this.removeOaUsersByBlankLoginWorkcodes(tenantId, resignedWorkcodes);
|
||||
if (uniquePersonMap.isEmpty()) {
|
||||
if (resignedLoginIds.isEmpty() && resignedWorkcodes.isEmpty()) {
|
||||
ComposeLogUtil.getLastLog().warn("OA人员均缺少登录名和工号,跳过人员同步");
|
||||
}
|
||||
return new PersonSyncCount(0, skippedCount, removedCount);
|
||||
}
|
||||
String defaultPassword = DigestUtil.encrypt(ParamCache.getValue(DEFAULT_PARAM_PASSWORD));
|
||||
List<User> users = uniquePersonMap.values().stream()
|
||||
.map(person -> {
|
||||
User user = userConvert.person2user(person, defaultPassword);
|
||||
user.setTenantId(tenantId);
|
||||
return user;
|
||||
})
|
||||
.toList();
|
||||
List<String> accounts = users.stream()
|
||||
.map(User::getAccount)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.distinct()
|
||||
.toList();
|
||||
List<String> phones = users.stream()
|
||||
.map(User::getPhone)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.distinct()
|
||||
.toList();
|
||||
Map<String, User> existingByAccount = new HashMap<>();
|
||||
Map<String, User> existingByPhone = new HashMap<>();
|
||||
userService.list(Wrappers.<User>lambdaQuery()
|
||||
.and(wrapper -> {
|
||||
wrapper.in(User::getAccount, accounts);
|
||||
if (CollectionUtil.isNotEmpty(phones)) {
|
||||
wrapper.or().in(User::getPhone, phones);
|
||||
}
|
||||
})
|
||||
).stream()
|
||||
.peek(userService::decryptPhone)
|
||||
.forEach(user -> {
|
||||
if (StringUtils.isNotBlank(user.getAccount())) {
|
||||
existingByAccount.put(user.getAccount(), user);
|
||||
}
|
||||
if (StringUtils.isNotBlank(user.getPhone())) {
|
||||
existingByPhone.put(user.getPhone(), user);
|
||||
}
|
||||
});
|
||||
Set<String> oaLoginIds = new LinkedHashSet<>();
|
||||
Set<String> workcodes = new LinkedHashSet<>();
|
||||
for (OAPersonResponse oaPerson : uniquePersonMap.values()) {
|
||||
String oaLoginId = this.resolveOaLoginId(oaPerson);
|
||||
if (oaLoginId != null) {
|
||||
oaLoginIds.add(oaLoginId);
|
||||
continue;
|
||||
}
|
||||
String workcode = this.resolveWorkcode(oaPerson);
|
||||
if (workcode != null) {
|
||||
workcodes.add(workcode);
|
||||
}
|
||||
}
|
||||
Map<String, User> existingByOaLoginId = this.loadUsersByOaLoginId(tenantId, oaLoginIds);
|
||||
Map<String, User> existingByWorkcode = this.loadUsersByBlankLoginWorkcode(tenantId, workcodes);
|
||||
List<User> users = new ArrayList<>();
|
||||
Map<String, Long> userMap = new HashMap<>();
|
||||
Set<Long> existsUserIds = new HashSet<>();
|
||||
Set<Long> unchangedUserIds = new HashSet<>();
|
||||
users.forEach(user -> {
|
||||
User existingUser = existingByAccount.get(user.getAccount());
|
||||
if (existingUser == null && StringUtils.isNotBlank(user.getPhone())) {
|
||||
existingUser = existingByPhone.get(user.getPhone());
|
||||
for (Map.Entry<String, OAPersonResponse> entry : uniquePersonMap.entrySet()) {
|
||||
OAPersonResponse oaPerson = entry.getValue();
|
||||
String oaLoginId = this.resolveOaLoginId(oaPerson);
|
||||
String workcode = this.resolveWorkcode(oaPerson);
|
||||
User user = this.userConvert.person2user(oaPerson, defaultPassword);
|
||||
user.setTenantId(tenantId);
|
||||
User existingUser = oaLoginId != null ? existingByOaLoginId.get(oaLoginId) : existingByWorkcode.get(workcode);
|
||||
if (oaLoginId != null) {
|
||||
user.setOaLoginId(oaLoginId);
|
||||
user.setAccount(oaLoginId);
|
||||
} else {
|
||||
user.setOaLoginId(null);
|
||||
user.setAccount(existingUser == null ? workcode : null);
|
||||
}
|
||||
if (existingUser != null) {
|
||||
user.setId(existingUser.getId());
|
||||
// 全局更新策略为 not_null,置空后不会回写密码和登录名
|
||||
user.setPassword(null);
|
||||
existsUserIds.add(existingUser.getId());
|
||||
if (shouldSkipOaUpdate(existingUser, user)) {
|
||||
unchangedUserIds.add(existingUser.getId());
|
||||
}
|
||||
} else {
|
||||
user.setId(DefaultIdentifierGenerator.getInstance().nextId(null));
|
||||
user.setPostId("-1");
|
||||
@@ -474,10 +737,11 @@ public class OaUserListSyncHelper {
|
||||
user.setDataScopeRange(1);
|
||||
user.setDataLevelRange(1);
|
||||
user.setIncludeNewCustomer(0);
|
||||
userService.encryptPhone(user);
|
||||
}
|
||||
userMap.put(user.getAccount(), user.getId());
|
||||
});
|
||||
this.userService.encryptPhone(user);
|
||||
users.add(user);
|
||||
userMap.put(entry.getKey(), user.getId());
|
||||
}
|
||||
|
||||
List<User> addUsers = users.stream()
|
||||
.filter(user -> !existsUserIds.contains(user.getId()))
|
||||
@@ -488,46 +752,63 @@ public class OaUserListSyncHelper {
|
||||
}
|
||||
List<User> updateUsers = users.stream()
|
||||
.filter(user -> existsUserIds.contains(user.getId()))
|
||||
.filter(user -> !unchangedUserIds.contains(user.getId()))
|
||||
.toList();
|
||||
if (CollectionUtil.isNotEmpty(updateUsers)) {
|
||||
ComposeLogUtil.getLastLog().info("批量修改用户:{}", updateUsers.size());
|
||||
userService.updateBatchById(updateUsers);
|
||||
}
|
||||
if (!unchangedUserIds.isEmpty()) {
|
||||
ComposeLogUtil.getLastLog().info("跳过未变更用户:{}", unchangedUserIds.size());
|
||||
}
|
||||
ComposeLogUtil.getLastLog().info("缺少账号已跳过的人员:{}", skippedCount);
|
||||
ComposeLogUtil.getLastLog().info("缺少登录名和工号已跳过的人员:{}", skippedCount);
|
||||
int syncedCount = addUsers.size() + updateUsers.size();
|
||||
skippedCount += unchangedUserIds.size();
|
||||
if (userMap.isEmpty()) {
|
||||
return new PersonSyncCount(0, skippedCount);
|
||||
return new PersonSyncCount(0, skippedCount, removedCount);
|
||||
}
|
||||
|
||||
Map<String, Long> userDeptMap = userDeptService.list(Wrappers.<UserDept>lambdaQuery()
|
||||
.in(UserDept::getUserId, userMap.values())
|
||||
).stream()
|
||||
List<UserDept> existingUserDeptList = userDeptService.list(Wrappers.<UserDept>lambdaQuery()
|
||||
.in(UserDept::getUserId, userMap.values()));
|
||||
Map<String, Long> userDeptMap = existingUserDeptList.stream()
|
||||
.collect(Collectors.toMap(this::getUserDeptKey, UserDept::getId, (first, second) -> second));
|
||||
Set<Long> existsUserDeptIds = new HashSet<>(userDeptMap.values());
|
||||
List<UserDept> userDeptList = oaPersons.stream()
|
||||
Map<String, UserDept> currentUserDeptMap = new LinkedHashMap<>();
|
||||
oaPersons.stream()
|
||||
.filter(oaPerson -> {
|
||||
String account = userConvert.resolveAccount(oaPerson);
|
||||
if (!userMap.containsKey(account)) {
|
||||
return false;
|
||||
}
|
||||
return !unchangedUserIds.contains(userMap.get(account));
|
||||
String syncKey = this.resolveSyncKey(oaPerson);
|
||||
return syncKey != null && userMap.containsKey(syncKey) && !this.isOaResigned(oaPerson);
|
||||
})
|
||||
.map(oaPerson -> this.buildUserDept(oaPerson, userMap, orgIndex))
|
||||
.filter(Objects::nonNull)
|
||||
.forEach(userDept -> {
|
||||
String userDeptKey = getUserDeptKey(userDept);
|
||||
if (userDeptMap.containsKey(userDeptKey)) {
|
||||
userDept.setId(userDeptMap.get(userDeptKey));
|
||||
} else {
|
||||
UserDept duplicated = currentUserDeptMap.get(userDeptKey);
|
||||
userDept.setId(duplicated == null
|
||||
? DefaultIdentifierGenerator.getInstance().nextId(null)
|
||||
: duplicated.getId());
|
||||
}
|
||||
currentUserDeptMap.put(userDeptKey, userDept);
|
||||
});
|
||||
List<UserDept> userDeptList = new ArrayList<>(currentUserDeptMap.values());
|
||||
Set<Long> currentUserDeptIds = userDeptList.stream()
|
||||
.map(UserDept::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
Set<Long> reboundUserIds = userDeptList.stream()
|
||||
.map(UserDept::getUserId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
List<Long> staleUserDeptIds = existingUserDeptList.stream()
|
||||
.filter(userDept -> userDept.getId() != null && reboundUserIds.contains(userDept.getUserId()))
|
||||
.map(UserDept::getId)
|
||||
.filter(userDeptId -> !currentUserDeptIds.contains(userDeptId))
|
||||
.toList();
|
||||
userDeptList.forEach(userDept -> {
|
||||
String userDeptKey = getUserDeptKey(userDept);
|
||||
if (userDeptMap.containsKey(userDeptKey)) {
|
||||
userDept.setId(userDeptMap.get(userDeptKey));
|
||||
} else {
|
||||
userDept.setId(DefaultIdentifierGenerator.getInstance().nextId(null));
|
||||
if (CollectionUtil.isNotEmpty(staleUserDeptIds)) {
|
||||
ComposeLogUtil.getLastLog().info("移除不再属于当前OA部门的人员关系:{}", staleUserDeptIds.size());
|
||||
for (int fromIndex = 0; fromIndex < staleUserDeptIds.size(); fromIndex += OA_LOGIN_ID_BATCH_SIZE) {
|
||||
List<Long> staleIdBatch = staleUserDeptIds.subList(fromIndex, Math.min(fromIndex + OA_LOGIN_ID_BATCH_SIZE, staleUserDeptIds.size()));
|
||||
userDeptService.removeByIds(staleIdBatch);
|
||||
}
|
||||
});
|
||||
}
|
||||
List<UserDept> addList = userDeptList.stream()
|
||||
.filter(userDept -> !existsUserDeptIds.contains(userDept.getId()))
|
||||
.toList();
|
||||
@@ -554,7 +835,6 @@ public class OaUserListSyncHelper {
|
||||
.collect(Collectors.toSet());
|
||||
String defaultRoleId = getDefaultRoleId();
|
||||
List<User> updateUserParams = list.stream()
|
||||
.filter(userDeptIds -> !unchangedUserIds.contains(userDeptIds.getUserId()))
|
||||
.filter(userDeptIds -> {
|
||||
if (StringUtils.isBlank(userDeptIds.getDeptIds())) {
|
||||
return false;
|
||||
@@ -582,22 +862,110 @@ public class OaUserListSyncHelper {
|
||||
if (CollectionUtil.isNotEmpty(updateUserParams)) {
|
||||
userService.updateBatchById(updateUserParams);
|
||||
}
|
||||
return new PersonSyncCount(syncedCount, skippedCount);
|
||||
return new PersonSyncCount(syncedCount, skippedCount, removedCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* oaModified 一致则跳过更新;任一侧为空无法确认一致,执行更新以补齐字段
|
||||
* 按 OA 登录名加载已有 is_oa=2 用户。同一登录名有多条时保留较早的一条,后续同步都更新这一条。
|
||||
*/
|
||||
private boolean shouldSkipOaUpdate(User existing, User incoming) {
|
||||
if (existing == null) {
|
||||
return false;
|
||||
private Map<String, User> loadUsersByOaLoginId(String tenantId, Collection<String> oaLoginIds) {
|
||||
Map<String, User> existingByOaLoginId = new HashMap<>();
|
||||
if (CollectionUtil.isEmpty(oaLoginIds)) {
|
||||
return existingByOaLoginId;
|
||||
}
|
||||
String localModified = StringUtils.trimToEmpty(existing.getOaModified());
|
||||
String remoteModified = StringUtils.trimToEmpty(incoming == null ? null : incoming.getOaModified());
|
||||
if (localModified.isEmpty() || remoteModified.isEmpty()) {
|
||||
return false;
|
||||
List<String> oaLoginIdList = oaLoginIds.stream()
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.distinct()
|
||||
.toList();
|
||||
for (int fromIndex = 0; fromIndex < oaLoginIdList.size(); fromIndex += OA_LOGIN_ID_BATCH_SIZE) {
|
||||
List<String> oaLoginIdBatch = oaLoginIdList.subList(fromIndex, Math.min(fromIndex + OA_LOGIN_ID_BATCH_SIZE, oaLoginIdList.size()));
|
||||
userService.list(Wrappers.<User>lambdaQuery()
|
||||
.eq(User::getTenantId, tenantId)
|
||||
.eq(User::getIsOa, CommonConstant.YES)
|
||||
.in(User::getOaLoginId, oaLoginIdBatch))
|
||||
.forEach(user -> {
|
||||
String oaLoginId = StringUtils.trimToNull(user.getOaLoginId());
|
||||
if (oaLoginId == null) {
|
||||
return;
|
||||
}
|
||||
User previous = existingByOaLoginId.get(oaLoginId);
|
||||
if (previous == null) {
|
||||
existingByOaLoginId.put(oaLoginId, user);
|
||||
return;
|
||||
}
|
||||
if (user.getId() != null && previous.getId() != null && user.getId() < previous.getId()) {
|
||||
ComposeLogUtil.getLastLog().warn("OA登录名重复,保留较早用户 oaLoginId={} keepUserId={} dropUserId={}",
|
||||
oaLoginId, user.getId(), previous.getId());
|
||||
existingByOaLoginId.put(oaLoginId, user);
|
||||
return;
|
||||
}
|
||||
ComposeLogUtil.getLastLog().warn("OA登录名重复,保留较早用户 oaLoginId={} keepUserId={} dropUserId={}",
|
||||
oaLoginId, previous.getId(), user.getId());
|
||||
});
|
||||
}
|
||||
return localModified.equals(remoteModified);
|
||||
return existingByOaLoginId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按工号加载没有登录名的 is_oa=2 用户。同一工号有多条时保留较早的一条。
|
||||
*/
|
||||
private Map<String, User> loadUsersByBlankLoginWorkcode(String tenantId, Collection<String> workcodes) {
|
||||
Map<String, User> existingByWorkcode = new HashMap<>();
|
||||
if (CollectionUtil.isEmpty(workcodes)) {
|
||||
return existingByWorkcode;
|
||||
}
|
||||
List<String> workcodeList = workcodes.stream()
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.distinct()
|
||||
.toList();
|
||||
for (int fromIndex = 0; fromIndex < workcodeList.size(); fromIndex += OA_LOGIN_ID_BATCH_SIZE) {
|
||||
List<String> workcodeBatch = workcodeList.subList(fromIndex, Math.min(fromIndex + OA_LOGIN_ID_BATCH_SIZE, workcodeList.size()));
|
||||
this.userService.list(Wrappers.<User>lambdaQuery()
|
||||
.eq(User::getTenantId, tenantId)
|
||||
.eq(User::getIsOa, CommonConstant.YES)
|
||||
.in(User::getCode, workcodeBatch)
|
||||
.and(wrapper -> wrapper.isNull(User::getOaLoginId).or().eq(User::getOaLoginId, "")))
|
||||
.forEach(user -> {
|
||||
String workcode = StringUtils.trimToNull(user.getCode());
|
||||
if (workcode == null) {
|
||||
return;
|
||||
}
|
||||
User previous = existingByWorkcode.get(workcode);
|
||||
if (previous == null || (user.getId() != null && previous.getId() != null && user.getId() < previous.getId())) {
|
||||
existingByWorkcode.put(workcode, user);
|
||||
}
|
||||
});
|
||||
}
|
||||
return existingByWorkcode;
|
||||
}
|
||||
|
||||
private String resolveOaLoginId(OAPersonResponse oaPerson) {
|
||||
if (oaPerson == null) {
|
||||
return null;
|
||||
}
|
||||
return StringUtils.trimToNull(oaPerson.getLoginid());
|
||||
}
|
||||
|
||||
private String resolveWorkcode(OAPersonResponse oaPerson) {
|
||||
if (oaPerson == null) {
|
||||
return null;
|
||||
}
|
||||
return StringUtils.trimToNull(oaPerson.getWorkcode());
|
||||
}
|
||||
|
||||
/**
|
||||
* 有登录名用登录名,没有登录名但有工号用工号。两者都没有则不同步。
|
||||
*/
|
||||
private String resolveSyncKey(OAPersonResponse oaPerson) {
|
||||
String oaLoginId = this.resolveOaLoginId(oaPerson);
|
||||
if (oaLoginId != null) {
|
||||
return "L:" + oaLoginId;
|
||||
}
|
||||
String workcode = this.resolveWorkcode(oaPerson);
|
||||
if (workcode != null) {
|
||||
return "W:" + workcode;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isPersonBypassCode(String deptCode, DeptCategory deptCategory) {
|
||||
@@ -638,12 +1006,17 @@ public class OaUserListSyncHelper {
|
||||
Dept department = orgIndex.findDept(oaPerson);
|
||||
if (department == null || department.getId() == null) {
|
||||
ComposeLogUtil.getLastLog().warn(
|
||||
"人员未匹配到已有部门,请先同步组织架构 account={} departmentid={} departmentname={} subcompanyid1={}",
|
||||
userConvert.resolveAccount(oaPerson), oaPerson.getDepartmentid(), oaPerson.getDepartmentname(),
|
||||
"人员未匹配到已有部门,请先同步组织架构 oaLoginId={} departmentid={} departmentname={} subcompanyid1={}",
|
||||
resolveOaLoginId(oaPerson), oaPerson.getDepartmentid(), oaPerson.getDepartmentname(),
|
||||
oaPerson.getSubcompanyid1());
|
||||
return null;
|
||||
}
|
||||
UserDept userDept = userConvert.person2userDept(oaPerson, userMap);
|
||||
String syncKey = this.resolveSyncKey(oaPerson);
|
||||
userDept.setUserId(syncKey == null ? null : userMap.get(syncKey));
|
||||
if (StringUtils.isBlank(this.resolveOaLoginId(oaPerson))) {
|
||||
userDept.setOaLoginId(null);
|
||||
}
|
||||
if (userDept.getUserId() == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -693,10 +1066,12 @@ public class OaUserListSyncHelper {
|
||||
public static class PersonSyncCount {
|
||||
private final int syncedCount;
|
||||
private final int skippedCount;
|
||||
private final int removedCount;
|
||||
|
||||
public PersonSyncCount(int syncedCount, int skippedCount) {
|
||||
public PersonSyncCount(int syncedCount, int skippedCount, int removedCount) {
|
||||
this.syncedCount = syncedCount;
|
||||
this.skippedCount = skippedCount;
|
||||
this.removedCount = removedCount;
|
||||
}
|
||||
|
||||
public int getSyncedCount() {
|
||||
@@ -706,6 +1081,10 @@ public class OaUserListSyncHelper {
|
||||
public int getSkippedCount() {
|
||||
return skippedCount;
|
||||
}
|
||||
|
||||
public int getRemovedCount() {
|
||||
return removedCount;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+7
-4
@@ -61,8 +61,9 @@ public class BillLedgerController extends BladeController {
|
||||
@ApiOperationSupport(order = 4)
|
||||
@Operation(summary = "付款申请可用汇票")
|
||||
public R<List<BillLedgerVO>> availableOptions(@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false) Long deptId, @RequestParam(required = false) Long selectedId) {
|
||||
return R.data(billLedgerService.availableOptions(keyword, deptId, selectedId));
|
||||
@RequestParam(required = false) Long deptId, @RequestParam(required = false) Long selectedId,
|
||||
@RequestParam(required = false) String payerName, @RequestParam(required = false) String payeeName) {
|
||||
return R.data(billLedgerService.availableOptions(keyword, deptId, selectedId, payerName, payeeName));
|
||||
}
|
||||
|
||||
@GetMapping("/available-page")
|
||||
@@ -70,8 +71,10 @@ public class BillLedgerController extends BladeController {
|
||||
@Operation(summary = "付款申请可用汇票分页")
|
||||
public R<IPage<BillLedgerVO>> availablePage(Query pageQuery,
|
||||
@RequestParam(required = false) String keyword, @RequestParam(required = false) Long deptId,
|
||||
@RequestParam(required = false) Long selectedId) {
|
||||
return R.data(billLedgerService.availablePage(Condition.getPage(pageQuery), keyword, deptId, selectedId));
|
||||
@RequestParam(required = false) Long selectedId, @RequestParam(required = false) String payerName,
|
||||
@RequestParam(required = false) String payeeName) {
|
||||
return R.data(billLedgerService.availablePage(Condition.getPage(pageQuery), keyword, deptId, selectedId,
|
||||
payerName, payeeName));
|
||||
}
|
||||
|
||||
@PostMapping("/submit")
|
||||
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* 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.controller;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.tenant.annotation.TenantIgnore;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* HCS 合规回调 Mock 端点
|
||||
* <p>
|
||||
* 合同法务审批结果回调(D103)、付款法务审批结果回调(D104)、项目金蝶同步结果回调(D902)、
|
||||
* 合同金蝶同步结果回传接收(D903)四个入站回调的纯 mock 实现:仅校验来源系统标识后返回固定结果,
|
||||
* 不读写业务数据、不联动状态。真实处理逻辑保留在对应 Support 中,联调时把端点切回
|
||||
* {@link } 并调用 Support 方法即可。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@TenantIgnore
|
||||
@RequestMapping("hcs")
|
||||
@Tag(name = "HCS合规回调Mock", description = "HCS合规回调Mock端点(仅自测,返回固定 mock 数据)")
|
||||
public class HcsMockController {
|
||||
|
||||
/**
|
||||
* 来源系统标识(合规方约定值,与 third-party.hcs.src-system 默认值一致)
|
||||
*/
|
||||
private static final String SOURCE_SYSTEM = "WLTMS";
|
||||
|
||||
/**
|
||||
* 合同法务审批结果回调(D103)
|
||||
*/
|
||||
@PostMapping("/contractLegal/result")
|
||||
@ApiOperationSupport(order = 15)
|
||||
@Operation(summary = "合同法务审批结果回调Mock", description = "D103 合规推送合同特批(金蝶同步)结果,仅校验来源系统并返回 mock 数据")
|
||||
public R<String> contractLegalResult(@RequestBody(required = false) Map<String, Object> param) {
|
||||
this.checkSourceSystem("合同法务审批结果", param);
|
||||
return R.data("mock特批结果已处理:approved");
|
||||
}
|
||||
|
||||
/**
|
||||
* 付款法务审批结果回调(D104)
|
||||
*/
|
||||
@PostMapping("/paymentLegal/result")
|
||||
@ApiOperationSupport(order = 16)
|
||||
@Operation(summary = "付款法务审批结果回调Mock", description = "D104 合规推送付款特批结果,仅校验来源系统并返回 mock 数据")
|
||||
public R<String> paymentLegalResult(@RequestBody(required = false) Map<String, Object> param) {
|
||||
this.checkSourceSystem("付款法务审批结果", param);
|
||||
return R.data("mock付款特批结果接收已处理:approved");
|
||||
}
|
||||
|
||||
/**
|
||||
* 项目金蝶同步结果回调(D902)
|
||||
*/
|
||||
@PostMapping("/projectLegal/result")
|
||||
@ApiOperationSupport(order = 18)
|
||||
@Operation(summary = "项目金蝶同步结果回调Mock", description = "D902 合规推送项目金蝶同步结果,仅校验来源系统并返回 mock 数据")
|
||||
public R<String> projectLegalResult(@RequestBody(required = false) Map<String, Object> param) {
|
||||
this.checkSourceSystem("项目金蝶同步结果", param);
|
||||
return R.data("mock项目金蝶同步结果接收已处理:approved");
|
||||
}
|
||||
|
||||
/**
|
||||
* 合同金蝶同步结果回传接收(D903)
|
||||
*/
|
||||
@PostMapping("/contractKingdee/result")
|
||||
@ApiOperationSupport(order = 19)
|
||||
@Operation(summary = "合同金蝶同步结果回传接收Mock", description = "D903 合规推送合同金蝶最终同步结果,仅校验来源系统并返回 mock 数据")
|
||||
public R<String> contractKingdeeResult(@RequestBody(required = false) Map<String, Object> param) {
|
||||
this.checkSourceSystem("合同金蝶同步结果", param);
|
||||
return R.data("mock合同金蝶同步结果接收处理:approved");
|
||||
}
|
||||
|
||||
/**
|
||||
* 来源系统校验:仅接受合规方约定的来源标识,防伪造回传
|
||||
*
|
||||
* @param bizName 业务名称(日志用)
|
||||
* @param param 回调报文
|
||||
*/
|
||||
private void checkSourceSystem(String bizName, Map<String, Object> param) {
|
||||
String sourceSystem = param == null ? null : Objects.toString(param.get("sourceSystem"), null);
|
||||
if (!StrUtil.equalsIgnoreCase(sourceSystem, SOURCE_SYSTEM)) {
|
||||
log.warn("{}推送来源系统标识错误:sourceSystem={}", bizName, sourceSystem);
|
||||
throw new ServiceException("来源系统标识错误");
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -39,6 +39,7 @@ import org.springblade.transport.pojo.vo.AdminVehicleOptionVO;
|
||||
import org.springblade.transport.pojo.vo.AdminWaybillCardVO;
|
||||
import org.springblade.transport.pojo.vo.AdminWaybillDetailVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillPunchRecordsVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillTrackVO;
|
||||
import org.springblade.transport.service.IManageWaybillService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -159,4 +160,14 @@ public class ManageWaybillController extends BladeController {
|
||||
return R.status(waybillService.savePunchRecord(request));
|
||||
}
|
||||
|
||||
@PostMapping("/track")
|
||||
@ApiOperationSupport(order = 11)
|
||||
@Operation(summary = "车辆历史轨迹", description = "小程序调度端:仅需登录态,按运单绑定车牌和日期区间查询 LBS 轨迹")
|
||||
public R<WaybillTrackVO> track(
|
||||
@Parameter(description = "运单ID", required = true) @RequestParam Long id,
|
||||
@Parameter(description = "开始日期 YYYY-MM-DD", required = true) @RequestParam String startDate,
|
||||
@Parameter(description = "结束日期 YYYY-MM-DD", required = true) @RequestParam String endDate) {
|
||||
return R.data(waybillService.trackVehicle(id, startDate, endDate));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+30
-10
@@ -54,7 +54,8 @@ import java.util.Set;
|
||||
/**
|
||||
* 导入结算单 Excel 读取器(Apache POI)。
|
||||
* <p>
|
||||
* 定位「*运输费」或「运输费」列,其后直到「结算金额」为动态费用列。
|
||||
* 定位「*运输费」或「运输费」列,其后直到日期/备注等固定列(兼容旧模板中的「结算金额」)为动态费用列。
|
||||
* 结算金额不由模板填写,解析时按「运输费 + 各费用项」自动合计。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@@ -66,6 +67,14 @@ public final class SettlementImportFeeReader {
|
||||
"actualStartDate", "actualEndDate", "planStartDate", "planEndDate"
|
||||
);
|
||||
private static final Set<String> OPTIONAL_DECIMAL_FIELDS = Set.of("mileage", "unitPrice");
|
||||
/** 费用列结束边界:其后为固定业务列;含旧模板「结算金额」以便兼容历史文件 */
|
||||
private static final Set<String> FEE_END_HEADERS = Set.of(
|
||||
"结算金额",
|
||||
"*实际发货时间", "实际发货时间",
|
||||
"*实际完成时间", "实际完成时间",
|
||||
"预计发货时间", "预计完成时间",
|
||||
"备注", "同一运单标识号"
|
||||
);
|
||||
|
||||
private SettlementImportFeeReader() {
|
||||
}
|
||||
@@ -119,15 +128,12 @@ public final class SettlementImportFeeReader {
|
||||
if (transportFeeColumn < 0) {
|
||||
throw new ServiceException("未找到「*运输费」或「运输费」列");
|
||||
}
|
||||
int settlementAmountColumn = findColumnExact(headers, "结算金额");
|
||||
List<Integer> feeColumns = new ArrayList<>();
|
||||
List<String> feeItemNames = new ArrayList<>();
|
||||
int endColumn = settlementAmountColumn > transportFeeColumn
|
||||
? settlementAmountColumn
|
||||
: headers.keySet().stream().mapToInt(Integer::intValue).max().orElse(transportFeeColumn) + 1;
|
||||
int endColumn = findFeeEndColumn(headers, transportFeeColumn);
|
||||
for (int column = transportFeeColumn + 1; column < endColumn; column++) {
|
||||
String name = headers.get(column);
|
||||
if (name == null || name.isBlank() || "结算金额".equals(name)) {
|
||||
if (name == null || name.isBlank() || FEE_END_HEADERS.contains(name)) {
|
||||
continue;
|
||||
}
|
||||
feeColumns.add(column);
|
||||
@@ -229,7 +235,8 @@ public final class SettlementImportFeeReader {
|
||||
} else if ("transportFee".equals(field) || "quantity".equals(field)) {
|
||||
mapped.put(field, decimal(cellString(poiCell), trimmedHeader, excelRowIndex));
|
||||
} else if ("settlementAmount".equals(field)) {
|
||||
mapped.put(field, optionalDecimal(cellString(poiCell), trimmedHeader, excelRowIndex));
|
||||
// 旧模板可能仍含结算金额列,读入后忽略,统一按运输费+费用项自动合计
|
||||
continue;
|
||||
} else if ("serialNumber".equals(field)) {
|
||||
mapped.put(field, intValue(cellString(poiCell)));
|
||||
} else {
|
||||
@@ -255,9 +262,7 @@ public final class SettlementImportFeeReader {
|
||||
BigDecimal feeItemTotal = feeItems.values().stream()
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add)
|
||||
.setScale(2, RoundingMode.HALF_UP);
|
||||
BigDecimal settlementAmount = mapped.get("settlementAmount") == null
|
||||
? transportFee.add(feeItemTotal).setScale(2, RoundingMode.HALF_UP)
|
||||
: toDecimal(mapped.get("settlementAmount"));
|
||||
BigDecimal settlementAmount = transportFee.add(feeItemTotal).setScale(2, RoundingMode.HALF_UP);
|
||||
mapped.put("settlementAmount", settlementAmount);
|
||||
mapped.put("结算金额", settlementAmount);
|
||||
mapped.put("feeItems", feeItems);
|
||||
@@ -284,6 +289,21 @@ public final class SettlementImportFeeReader {
|
||||
return findColumnExact(headers, "运输费");
|
||||
}
|
||||
|
||||
/** 费用列右边界:取运输费之后第一个固定结束表头列下标;无则取表头最大下标 + 1。 */
|
||||
private static int findFeeEndColumn(Map<Integer, String> headers, int transportFeeColumn) {
|
||||
int endColumn = headers.keySet().stream().mapToInt(Integer::intValue).max().orElse(transportFeeColumn) + 1;
|
||||
for (Map.Entry<Integer, String> entry : headers.entrySet()) {
|
||||
int column = entry.getKey();
|
||||
if (column <= transportFeeColumn) {
|
||||
continue;
|
||||
}
|
||||
if (FEE_END_HEADERS.contains(entry.getValue()) && column < endColumn) {
|
||||
endColumn = column;
|
||||
}
|
||||
}
|
||||
return endColumn;
|
||||
}
|
||||
|
||||
private static int findColumnExact(Map<Integer, String> headers, String header) {
|
||||
return headers.entrySet().stream()
|
||||
.filter(entry -> header.equals(entry.getValue()))
|
||||
|
||||
+3
-6
@@ -37,7 +37,7 @@ import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 导入结算单 Excel 模板(固定列 + 运输单价 + *运输费 + 示例费用项)
|
||||
* 导入结算单 Excel 固定列模型(失败明细兜底导出;费用项由用户在原模板中自行增列)。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@@ -96,11 +96,8 @@ public class SettlementImportTemplateExcel implements Serializable {
|
||||
private BigDecimal unitPrice;
|
||||
@ExcelProperty("*运输费")
|
||||
private BigDecimal transportFee;
|
||||
@ExcelProperty("装卸费")
|
||||
private BigDecimal loadingFee;
|
||||
@ExcelProperty("过路费")
|
||||
private BigDecimal tollFee;
|
||||
@ExcelProperty("结算金额")
|
||||
/** 模板已去掉结算金额列;仅失败明细内部保留,不导出为表头 */
|
||||
@ExcelIgnore
|
||||
private BigDecimal settlementAmount;
|
||||
@ExcelProperty("*实际发货时间")
|
||||
private String actualStartDate;
|
||||
|
||||
+25
-34
@@ -47,12 +47,12 @@ import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 导入结算单模板导出:填写说明 + 数据表头 + 运输方式下拉 + 系统费用项列。
|
||||
* 导入结算单模板导出:填写说明 + 固定数据表头 + 运输方式下拉。
|
||||
* <p>
|
||||
* 模板不预置费用项列;用户可在「*运输费」与后续固定列之间自行插入系统已启用的费用项列。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@@ -85,7 +85,6 @@ public final class SettlementImportTemplateExporter {
|
||||
);
|
||||
|
||||
private static final List<String> FIXED_HEADERS_AFTER_FEE = List.of(
|
||||
"结算金额",
|
||||
"*实际发货时间",
|
||||
"*实际完成时间",
|
||||
"预计发货时间",
|
||||
@@ -94,15 +93,18 @@ public final class SettlementImportTemplateExporter {
|
||||
"同一运单标识号"
|
||||
);
|
||||
|
||||
private static final List<String> DEFAULT_FEE_ITEMS = List.of("装卸费", "过路费");
|
||||
|
||||
private SettlementImportTemplateExporter() {
|
||||
}
|
||||
|
||||
public static void export(HttpServletResponse response, List<String> transportTypeOptions,
|
||||
List<String> feeItemNames) {
|
||||
List<String> headers = buildHeaders(feeItemNames);
|
||||
List<String> transportOptions = defaultTransportOptions(transportTypeOptions);
|
||||
List<String> headers = buildHeaders();
|
||||
// 运输方式由调用方传入业务字典 transport_type 的字典名称,禁止写死
|
||||
List<String> transportOptions = transportTypeOptions == null ? List.of() : transportTypeOptions.stream()
|
||||
.filter(item -> item != null && !item.isBlank())
|
||||
.map(String::trim)
|
||||
.distinct()
|
||||
.toList();
|
||||
try (Workbook workbook = new XSSFWorkbook()) {
|
||||
writeInstructionSheet(workbook, transportOptions, feeItemNames);
|
||||
Sheet dataSheet = workbook.createSheet("导入结算单模板");
|
||||
@@ -128,20 +130,9 @@ public final class SettlementImportTemplateExporter {
|
||||
}
|
||||
}
|
||||
|
||||
private static List<String> buildHeaders(List<String> feeItemNames) {
|
||||
/** 固定列模板:不预置费用项列,由用户自行插入。 */
|
||||
private static List<String> buildHeaders() {
|
||||
List<String> headers = new ArrayList<>(FIXED_HEADERS_BEFORE_FEE);
|
||||
Set<String> feeNames = new LinkedHashSet<>();
|
||||
if (feeItemNames != null) {
|
||||
for (String name : feeItemNames) {
|
||||
if (name != null && !name.isBlank()) {
|
||||
feeNames.add(name.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (feeNames.isEmpty()) {
|
||||
feeNames.addAll(DEFAULT_FEE_ITEMS);
|
||||
}
|
||||
headers.addAll(feeNames);
|
||||
headers.addAll(FIXED_HEADERS_AFTER_FEE);
|
||||
return headers;
|
||||
}
|
||||
@@ -155,18 +146,24 @@ public final class SettlementImportTemplateExporter {
|
||||
titleStyle.setFont(titleFont);
|
||||
List<String> lines = new ArrayList<>();
|
||||
lines.add("导入结算单填写说明");
|
||||
lines.add("1. 请不要修改「导入结算单模板」工作表的表头;带 * 号为必填项。");
|
||||
lines.add("2. 运输方式请从下拉项中选择,须与系统字典一致。");
|
||||
lines.add("1. 请不要修改「导入结算单模板」工作表的固定表头;带 * 号为必填项。");
|
||||
lines.add("2. 运输方式请从下拉项中选择,须与业务字典 transport_type 的字典名称一致。");
|
||||
lines.add("3. 日期请填写 yyyy-MM-dd(如 2026-01-01);Excel 日期单元格也可直接选择。");
|
||||
lines.add("4. 里程(km) 可为空;填写时须为正数。");
|
||||
lines.add("5. 「*运输费」之后、「结算金额」之前的列为费用项,列名须与系统费用项主数据名称完全一致;金额不为 0 时若不存在或已停用则无法导入。");
|
||||
lines.add("6. 结算金额 = 运输费 + 各费用项合计,系统会按此重算。");
|
||||
lines.add("7. 当前可选运输方式:" + String.join("、", transportOptions) + "。");
|
||||
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())
|
||||
lines.add("8. 费用项请按实际需要在模板中自行增列,列名须与系统已启用费用项一致;当前系统已启用费用项:"
|
||||
+ String.join("、", feeItemNames.stream().limit(30).toList())
|
||||
+ (feeItemNames.size() > 30 ? " 等" : "") + "。");
|
||||
} else {
|
||||
lines.add("8. 模板默认包含装卸费、过路费列,可按系统费用项主数据增删费用列。");
|
||||
lines.add("8. 费用项请按实际需要在模板中自行增列,列名须与系统已启用的费用项主数据名称完全一致。");
|
||||
}
|
||||
for (int index = 0; index < lines.size(); index++) {
|
||||
Row row = sheet.createRow(index);
|
||||
@@ -202,10 +199,4 @@ public final class SettlementImportTemplateExporter {
|
||||
return style;
|
||||
}
|
||||
|
||||
private static List<String> defaultTransportOptions(List<String> transportTypeOptions) {
|
||||
if (transportTypeOptions != null && !transportTypeOptions.isEmpty()) {
|
||||
return transportTypeOptions;
|
||||
}
|
||||
return List.of("公路运输", "铁路运输", "水路运输", "航空运输");
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -18,8 +18,9 @@ public interface IBillLedgerService extends BaseService<BillLedger> {
|
||||
IPage<BillLedgerVO> selectPage(IPage<BillLedger> page, BillLedgerVO query);
|
||||
BillLedgerVO detail(Long id);
|
||||
Map<String, Long> expiryCounts();
|
||||
List<BillLedgerVO> availableOptions(String keyword, Long deptId, Long selectedId);
|
||||
IPage<BillLedgerVO> availablePage(IPage<BillLedger> page, String keyword, Long deptId, Long selectedId);
|
||||
List<BillLedgerVO> availableOptions(String keyword, Long deptId, Long selectedId, String payerName, String payeeName);
|
||||
IPage<BillLedgerVO> availablePage(IPage<BillLedger> page, String keyword, Long deptId, Long selectedId,
|
||||
String payerName, String payeeName);
|
||||
Long submit(BillLedgerSaveRequest request);
|
||||
void removeLedger(Long id);
|
||||
}
|
||||
|
||||
+38
-9
@@ -86,14 +86,15 @@ public class BillLedgerServiceImpl extends BaseServiceImpl<BillLedgerMapper, Bil
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BillLedgerVO> availableOptions(String keyword, Long deptId, Long selectedId) {
|
||||
return availableList(keyword, deptId, selectedId);
|
||||
public List<BillLedgerVO> availableOptions(String keyword, Long deptId, Long selectedId, String payerName,
|
||||
String payeeName) {
|
||||
return availableList(keyword, deptId, selectedId, payerName, payeeName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<BillLedgerVO> availablePage(IPage<BillLedger> page, String keyword, Long deptId,
|
||||
Long selectedId) {
|
||||
List<BillLedgerVO> available = availableList(keyword, deptId, selectedId);
|
||||
Long selectedId, String payerName, String payeeName) {
|
||||
List<BillLedgerVO> available = availableList(keyword, deptId, selectedId, payerName, payeeName);
|
||||
long current = Math.max(page.getCurrent(), 1);
|
||||
long size = Math.max(page.getSize(), 1);
|
||||
long from = Math.min((current - 1) * size, available.size());
|
||||
@@ -103,27 +104,50 @@ public class BillLedgerServiceImpl extends BaseServiceImpl<BillLedgerMapper, Bil
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<BillLedgerVO> availableList(String keyword, Long deptId, Long selectedId) {
|
||||
private List<BillLedgerVO> availableList(String keyword, Long deptId, Long selectedId, String payerName,
|
||||
String payeeName) {
|
||||
LocalDate today = LocalDate.now();
|
||||
// 付款申请仅可选:收票类型、未到期、有可用余额;已选中的汇票始终带回以便回显
|
||||
// 未到期且有余额;收票且部门可用,或出票/收票单位与结算收付款方一致(开票、收票都可)
|
||||
return list(Wrappers.<BillLedger>lambdaQuery()
|
||||
.and(Func.isNotEmpty(keyword), wrapper -> wrapper.like(BillLedger::getBillNo, keyword)
|
||||
.or().like(BillLedger::getIssuerName, keyword)
|
||||
.or().like(BillLedger::getReceiverName, keyword))
|
||||
.and(wrapper -> wrapper
|
||||
.and(available -> available
|
||||
.eq(BillLedger::getBillType, "received")
|
||||
.ge(BillLedger::getMaturityDate, today)
|
||||
.gt(BillLedger::getAvailableBalance, BigDecimal.ZERO))
|
||||
.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)
|
||||
|| departmentAvailable(item, deptId))
|
||||
|| 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) {
|
||||
@@ -141,6 +165,11 @@ public class BillLedgerServiceImpl extends BaseServiceImpl<BillLedgerMapper, Bil
|
||||
}
|
||||
CustomerArchive issuer = customer(request.getIssuerId(), "出票单位");
|
||||
CustomerArchive feeBearer = customer(request.getFeeBearerId(), "费用承担方");
|
||||
String receiverName = required(request.getReceiverName(), "收票单位", 100);
|
||||
if (!Objects.equals(feeBearer.getId(), issuer.getId())
|
||||
&& !customerName(feeBearer).equals(receiverName)) {
|
||||
throw new ServiceException("费用承担方只能选择收票单位或出票单位");
|
||||
}
|
||||
BigDecimal faceAmount = positive(request.getFaceAmount(), "票面金额").setScale(2, RoundingMode.HALF_UP);
|
||||
BigDecimal usedAmount = entity.getId() == null ? BigDecimal.ZERO : activeUsedAmount(entity.getId());
|
||||
if (faceAmount.compareTo(usedAmount) < 0) {
|
||||
@@ -150,7 +179,7 @@ public class BillLedgerServiceImpl extends BaseServiceImpl<BillLedgerMapper, Bil
|
||||
entity.setBillNo(billNo);
|
||||
entity.setIssuerId(issuer.getId());
|
||||
entity.setIssuerName(customerName(issuer));
|
||||
entity.setReceiverName(required(request.getReceiverName(), "收票单位", 100));
|
||||
entity.setReceiverName(receiverName);
|
||||
entity.setBillType(request.getBillType());
|
||||
entity.setFaceAmount(faceAmount);
|
||||
entity.setAvailableBalance(faceAmount.subtract(usedAmount).setScale(2, RoundingMode.HALF_UP));
|
||||
|
||||
+39
-4
@@ -298,6 +298,7 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
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());
|
||||
@@ -507,8 +508,7 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
contractManage.setOrganizationId(TransportBusinessSupport.currentDeptId("合同管理"));
|
||||
contractManage.setOrganizationName(TransportBusinessSupport.currentDept("合同管理").getDeptName());
|
||||
}
|
||||
// 新增始终按部门规则生成合同编号,避免前端预览号并发冲突
|
||||
contractManage.setContractNo(nextContractNo(contractManage.getOrganizationId()));
|
||||
applyContractNo(contractManage, contractManage.getContractNo(), true);
|
||||
if (Func.isEmpty(contractManage.getHandlerUserId())) {
|
||||
contractManage.setHandlerUserId(AuthUtil.getUserId());
|
||||
contractManage.setHandlerUserName(AuthUtil.getUserName());
|
||||
@@ -516,7 +516,7 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
contractManage.setArchiveStatus(ARCHIVE_STATUS_PENDING);
|
||||
} else {
|
||||
ContractManage oldRecord = loadExists(contractManage.getId());
|
||||
contractManage.setContractNo(oldRecord.getContractNo());
|
||||
applyContractNo(contractManage, contractManage.getContractNo(), false);
|
||||
if (Func.isEmpty(contractManage.getOrganizationId())) {
|
||||
contractManage.setOrganizationId(oldRecord.getOrganizationId());
|
||||
contractManage.setOrganizationName(oldRecord.getOrganizationName());
|
||||
@@ -535,6 +535,28 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存合同编号。新增未填写时按部门规则生成,已填写或编辑时保留手工编号并校验唯一。
|
||||
*/
|
||||
private void applyContractNo(ContractManage contractManage, String contractNo, boolean generateWhenEmpty) {
|
||||
String normalizedContractNo = TransportBusinessSupport.trimToNull(contractNo);
|
||||
if (Func.isEmpty(normalizedContractNo)) {
|
||||
if (!generateWhenEmpty) {
|
||||
throw new ServiceException("请输入合同编号");
|
||||
}
|
||||
normalizedContractNo = nextContractNo(contractManage.getOrganizationId());
|
||||
}
|
||||
TransportBusinessSupport.validateLength(normalizedContractNo, 100, "合同编号不能超过100字");
|
||||
Long duplicatedCount = count(Wrappers.<ContractManage>lambdaQuery()
|
||||
.eq(ContractManage::getIsDeleted, 0)
|
||||
.eq(ContractManage::getContractNo, normalizedContractNo)
|
||||
.ne(Func.isNotEmpty(contractManage.getId()), ContractManage::getId, contractManage.getId()));
|
||||
if (duplicatedCount != null && duplicatedCount > 0) {
|
||||
throw new ServiceException("合同编号已存在");
|
||||
}
|
||||
contractManage.setContractNo(normalizedContractNo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized String nextContractNo(Long organizationId) {
|
||||
if (Func.isEmpty(organizationId)) {
|
||||
@@ -558,7 +580,8 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
.orderByDesc(ContractManage::getContractNo)
|
||||
.last("LIMIT 50"));
|
||||
Pattern pattern = Pattern.compile("^" + Pattern.quote(prefix) + "(" + year + ")(\\d{3})号$");
|
||||
int next = 1;
|
||||
// 当年尚无匹配编号时,从组织配置的初始流水号起编;否则在已有最大流水号上加1
|
||||
int next = resolveContractInitialSerial(dept);
|
||||
for (ContractManage item : latestList) {
|
||||
String contractNo = TransportBusinessSupport.trimToNull(item.getContractNo());
|
||||
if (Func.isEmpty(contractNo)) {
|
||||
@@ -577,6 +600,17 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
return head + String.format("%03d", next) + "号";
|
||||
}
|
||||
|
||||
private int resolveContractInitialSerial(Dept dept) {
|
||||
Integer serial = dept.getContractInitialSerial();
|
||||
if (serial == null || serial < 1) {
|
||||
return 1;
|
||||
}
|
||||
if (serial > 999) {
|
||||
throw new ServiceException("组织机构中配置的合同初始流水号不能超过999");
|
||||
}
|
||||
return serial;
|
||||
}
|
||||
|
||||
private void refreshArchiveStatusOnApprove(ContractManage contractManage) {
|
||||
if (hasSealArchiveFile(contractManage.getContractFileJson())) {
|
||||
contractManage.setArchiveStatus(ARCHIVE_STATUS_ARCHIVED);
|
||||
@@ -1015,6 +1049,7 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
|
||||
private Map<String, Object> buildContractChangeSnapshot(ContractManage contractManage) {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("contractNo", contractManage.getContractNo());
|
||||
data.put("contractName", contractManage.getContractName());
|
||||
data.put("partyB", contractManage.getPartyB());
|
||||
data.put("startDate", contractManage.getStartDate());
|
||||
|
||||
+6
@@ -350,9 +350,14 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
|
||||
/**
|
||||
* 按打卡记录 / 计划时间推导轨迹查询起止日期
|
||||
* <p>
|
||||
* TODO(临时):当前仅查当天,便于联调 LBS;恢复正式逻辑时删掉本段 early-return。
|
||||
*/
|
||||
private LocalDate[] resolveTrackDateRange(Waybill waybill) {
|
||||
LocalDate today = LocalDate.now();
|
||||
// TEMP: 行驶轨迹时间段临时改为仅当天
|
||||
return new LocalDate[]{today, today};
|
||||
/*
|
||||
List<LocalDate> punchDates = new ArrayList<>();
|
||||
nodePunchMapper.selectList(Wrappers.<WaybillNodePunch>lambdaQuery()
|
||||
.eq(WaybillNodePunch::getWaybillId, waybill.getId())
|
||||
@@ -384,6 +389,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
return new LocalDate[]{waybill.getStartDate(), today};
|
||||
}
|
||||
return new LocalDate[]{today.minusDays(3), today};
|
||||
*/
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+10
-3
@@ -886,11 +886,18 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
|
||||
/**
|
||||
* 调度端可见组织:普通用户(含各级角色)只出自己所属组织,超级管理员不过滤。
|
||||
* <p>
|
||||
* 与后台运单列表同一条规则({@link TransportBusinessSupport#resolveDeptFilterIds}),
|
||||
* 小程序端不提供「全部组织」开关,故两个入参固定传空;详情与重新派单不按组织收口。
|
||||
* 小程序不校验「所属组织不能为空」:账号没有组织时不按组织收口,避免首页、待处理等接口直接报错。
|
||||
* 详情与重新派单本身不走这里。
|
||||
*/
|
||||
private List<Long> manageableDeptIds() {
|
||||
return TransportBusinessSupport.resolveDeptFilterIds(null, null, "运单管理");
|
||||
if (TransportBusinessSupport.isAdministrator()) {
|
||||
return List.of();
|
||||
}
|
||||
Long deptId = Func.firstLong(AuthUtil.getDeptId());
|
||||
if (Func.isEmpty(deptId) || deptId <= 0) {
|
||||
return List.of();
|
||||
}
|
||||
return List.of(deptId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+112
-19
@@ -374,8 +374,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
matchedInvoiceAmount = matchedInvoiceAmount.add(money(item.getMatchedAmount()));
|
||||
}
|
||||
entity.setMatchedInvoiceAmount(matchedInvoiceAmount);
|
||||
// 进度预付只关联预结算单。编辑回传的 settlementId 可能是预结算单 ID,不能拿去查正式结算单。
|
||||
normalizeSettlementReference(request);
|
||||
// 进度预付只关联预结算单。编辑回传的 settlementId 可能是预结算单 ID 或已失效的正式结算单 ID。
|
||||
normalizeSettlementReference(request, entity);
|
||||
if (TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT.equals(request.getPaymentType()) && request.getSettlementId() == null
|
||||
&& request.getSettlementIds() != null && !request.getSettlementIds().isEmpty()) {
|
||||
request.setSettlementId(request.getSettlementIds().get(0));
|
||||
@@ -1897,16 +1897,19 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
/**
|
||||
* 进度预付只认预结算单;尾款付款若 settlementId 实际是预结算单 ID 或正式结算单已不存在,改为按预结算单保存。
|
||||
*/
|
||||
private void normalizeSettlementReference(PaymentApplicationSaveRequest request) {
|
||||
if ("progress_advance".equals(request.getPaymentType())) {
|
||||
if (request.getPreSettlementId() == null && request.getPreSettlementIds() != null) {
|
||||
request.setPreSettlementId(request.getPreSettlementIds().stream().filter(Objects::nonNull).findFirst().orElse(null));
|
||||
private void normalizeSettlementReference(PaymentApplicationSaveRequest request, PaymentApplication entity) {
|
||||
if ("progress_advance".equals(request.getPaymentType())
|
||||
|| (entity != null && "progress_advance".equals(entity.getPaymentType())
|
||||
&& Func.isEmpty(request.getPaymentType()))) {
|
||||
if (Func.isEmpty(request.getPaymentType())) {
|
||||
request.setPaymentType("progress_advance");
|
||||
}
|
||||
if (request.getPreSettlementId() == null) {
|
||||
request.setPreSettlementId(request.getSettlementId());
|
||||
}
|
||||
if (request.getPreSettlementId() == null && request.getSettlementIds() != null) {
|
||||
request.setPreSettlementId(request.getSettlementIds().stream().filter(Objects::nonNull).findFirst().orElse(null));
|
||||
Long preSettlementId = resolveProgressPreSettlementId(request, entity);
|
||||
request.setPreSettlementId(preSettlementId);
|
||||
if (preSettlementId == null) {
|
||||
request.setPreSettlementIds(null);
|
||||
} else if (request.getPreSettlementIds() == null || request.getPreSettlementIds().stream().noneMatch(Objects::nonNull)) {
|
||||
request.setPreSettlementIds(List.of(preSettlementId));
|
||||
}
|
||||
request.setSettlementId(null);
|
||||
request.setSettlementIds(null);
|
||||
@@ -1963,8 +1966,61 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
entity.setSettlementNo(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 进度预付驳回重提时,请求里的结算主键可能是失效正式结算单或精度失真后的 ID。
|
||||
* 依次用预结算主键、正式结算位上的预结算主键、单据已保存的预结算主键和预结算单号找回。
|
||||
*/
|
||||
private Long resolveProgressPreSettlementId(PaymentApplicationSaveRequest request, PaymentApplication entity) {
|
||||
List<Long> candidates = new java.util.ArrayList<>();
|
||||
if (request.getPreSettlementId() != null) {
|
||||
candidates.add(request.getPreSettlementId());
|
||||
}
|
||||
if (request.getPreSettlementIds() != null) {
|
||||
request.getPreSettlementIds().stream().filter(Objects::nonNull).forEach(candidates::add);
|
||||
}
|
||||
if (request.getSettlementId() != null) {
|
||||
candidates.add(request.getSettlementId());
|
||||
}
|
||||
if (request.getSettlementIds() != null) {
|
||||
request.getSettlementIds().stream().filter(Objects::nonNull).forEach(candidates::add);
|
||||
}
|
||||
if (entity != null && entity.getPreSettlementId() != null) {
|
||||
candidates.add(entity.getPreSettlementId());
|
||||
}
|
||||
for (Long candidate : candidates) {
|
||||
PreSettlement preSettlement = preSettlementMapper.selectById(candidate);
|
||||
if (preSettlement != null && !Objects.equals(preSettlement.getIsDeleted(), 1)) {
|
||||
return preSettlement.getId();
|
||||
}
|
||||
}
|
||||
String preSettlementNo = request.getPreSettlementNo();
|
||||
if (Func.isEmpty(preSettlementNo) && entity != null) {
|
||||
preSettlementNo = entity.getPreSettlementNo();
|
||||
}
|
||||
if (Func.isNotEmpty(preSettlementNo)) {
|
||||
String settlementNo = preSettlementNo.split("、")[0].trim();
|
||||
PreSettlement preSettlement = preSettlementMapper.selectOne(Wrappers.<PreSettlement>lambdaQuery()
|
||||
.eq(PreSettlement::getPreSettlementNo, settlementNo)
|
||||
.eq(PreSettlement::getIsDeleted, 0)
|
||||
.last("limit 1"));
|
||||
if (preSettlement != null) {
|
||||
return preSettlement.getId();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void fillReference(PaymentApplication entity, PaymentApplicationSaveRequest request) {
|
||||
if ("project_advance".equals(request.getPaymentType())) {
|
||||
if ("progress_advance".equals(request.getPaymentType())) {
|
||||
Long preSettlementId = resolveProgressPreSettlementId(request, entity);
|
||||
request.setPreSettlementId(preSettlementId);
|
||||
if (preSettlementId == null) {
|
||||
throw new ServiceException("进度预付必须关联预结算单");
|
||||
}
|
||||
request.setSettlementId(null);
|
||||
request.setSettlementIds(null);
|
||||
fillPreSettlementReference(entity, requireApprovedPreSettlement(request.getPreSettlementId()));
|
||||
} else if ("project_advance".equals(request.getPaymentType())) {
|
||||
ProjectApply project = projectApplyMapper.selectById(request.getProjectId());
|
||||
if (project == null || Objects.equals(project.getIsDeleted(), 1)) throw new ServiceException("所属项目不存在");
|
||||
entity.setSettlementId(null); entity.setSettlementNo(null); entity.setPreSettlementId(null); entity.setPreSettlementNo(null);
|
||||
@@ -1981,6 +2037,10 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
} else if (request.getPreSettlementId() != null) {
|
||||
request.setSettlementId(null);
|
||||
fillPreSettlementReference(entity, requireApprovedPreSettlement(request.getPreSettlementId()));
|
||||
} else if (entity.getPreSettlementId() != null) {
|
||||
request.setSettlementId(null);
|
||||
request.setPreSettlementId(entity.getPreSettlementId());
|
||||
fillPreSettlementReference(entity, requireApprovedPreSettlement(entity.getPreSettlementId()));
|
||||
} else {
|
||||
throw new ServiceException("正式结算单不存在");
|
||||
}
|
||||
@@ -2487,7 +2547,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
return;
|
||||
}
|
||||
BillLedger ledger = billLedgerMapper.selectById(request.getBillLedgerId());
|
||||
validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true, entity.getId());
|
||||
validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true, entity.getId(),
|
||||
entity.getSettlementId(), entity.getPreSettlementId());
|
||||
entity.setBillLedgerId(ledger.getId());
|
||||
entity.setBillNo(ledger.getBillNo());
|
||||
}
|
||||
@@ -2495,25 +2556,57 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
private void validateSelectedBill(PaymentApplication entity, boolean locked) {
|
||||
if (!isBillPayment(entity.getPaymentMethod())) return;
|
||||
BillLedger ledger = locked ? lockedBill(entity.getBillLedgerId()) : billLedgerMapper.selectById(entity.getBillLedgerId());
|
||||
validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true, entity.getId());
|
||||
validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true, entity.getId(),
|
||||
entity.getSettlementId(), entity.getPreSettlementId());
|
||||
}
|
||||
|
||||
private void validateBill(BillLedger ledger, Long deptId, BigDecimal amount, boolean checkMaturity, Long paymentApplicationId) {
|
||||
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("所选汇票台账不存在");
|
||||
if (!"received".equals(ledger.getBillType())) {
|
||||
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 (!departmentAvailable(ledger, deptId)) 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) {
|
||||
BillLedger ledger = lockedBill(entity.getBillLedgerId());
|
||||
validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true, null);
|
||||
validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true, null,
|
||||
entity.getSettlementId(), entity.getPreSettlementId());
|
||||
BillLedgerUsage exists = lockedBillUsage(entity.getId());
|
||||
if (exists != null && TransportBusiConstant.APPROVED.equals(exists.getUsageStatus())) {
|
||||
throw new ServiceException("该付款申请已生成汇票使用记录");
|
||||
|
||||
+82
-65
@@ -206,6 +206,9 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
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)));
|
||||
vo.setPrintTemplates(resolvePrintTemplates(settlement));
|
||||
return vo;
|
||||
@@ -355,8 +358,10 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
synchronizeDetails(settlement, request.getSourceDetailIds(), Boolean.TRUE.equals(request.getAllowSourceMismatch()));
|
||||
}
|
||||
settlement.setExchangeRate(normalizeRate(request.getExchangeRate(), settlement.getCurrency()));
|
||||
List<PreSettlementSummaryFee> summaryBefore = created ? new ArrayList<>() : listSummaryFees(settlement.getId());
|
||||
rebuildSummaryFees(settlement.getId(), true);
|
||||
applySummaryRequest(settlement.getId(), contractChanged ? List.of() : request.getSummaryFees());
|
||||
recordSummaryFeeChanges(settlement.getId(), summaryBefore);
|
||||
refreshSettlementAmount(settlement);
|
||||
projectAdvanceWriteOffService.replaceWriteOffs(
|
||||
TransportBusiConstant.SETTLEMENT_BILL_PRE,
|
||||
@@ -425,7 +430,6 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
settlement.setCurrentProcessor(AuthUtil.getUserName());
|
||||
settlement.setApprovedTime(LocalDateTime.now());
|
||||
updateById(settlement);
|
||||
saveChange(settlement.getId(), "结算单基本信息", null, "调整", "预结算审批通过", request.getReason());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1129,11 +1133,6 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
throw new ServiceException("存在无效的手工费用行");
|
||||
}
|
||||
boolean creating = row.getId() == null;
|
||||
String beforeFeeType = creating ? null : row.getFeeType();
|
||||
String beforeFeeItem = creating ? null : row.getFeeItem();
|
||||
BigDecimal beforeAdjustAmount = creating ? null : money(row.getAdjustAmount());
|
||||
BigDecimal beforeSettlementAmount = creating ? null : money(row.getSettlementAmount());
|
||||
String beforeRemark = creating ? null : row.getRemark();
|
||||
row.setPreSettlementId(settlementId);
|
||||
row.setLineNo(lineNo++);
|
||||
row.setFeeType(feeType);
|
||||
@@ -1149,8 +1148,6 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
summaryFeeMapper.updateById(row);
|
||||
}
|
||||
retainedManualIds.add(row.getId());
|
||||
saveManualSummaryFeeChange(settlementId, creating, beforeFeeType, beforeFeeItem,
|
||||
beforeAdjustAmount, beforeSettlementAmount, beforeRemark, row);
|
||||
continue;
|
||||
}
|
||||
String feeItem = requiredText(requestRow.getFeeItem(), "费用项");
|
||||
@@ -1175,45 +1172,16 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
row.setFeeItem(feeItem);
|
||||
// 生成费用行的三个金额必须来源于结算明细快照,不能接受前端回传值覆盖原金额。
|
||||
// rebuildSummaryFees 已按明细重建 original/adjust/settlement,此处仅保留备注。
|
||||
BigDecimal before = money(row.getAdjustAmount());
|
||||
row.setOriginalAmount(money(row.getOriginalAmount()));
|
||||
row.setSettlementAmount(money(row.getSettlementAmount()));
|
||||
row.setAdjustAmount(money(row.getSettlementAmount().subtract(row.getOriginalAmount())));
|
||||
row.setRemark(limitRemark(requestRow.getRemark(), 50));
|
||||
if (row.getId() == null) summaryFeeMapper.insert(row); else summaryFeeMapper.updateById(row);
|
||||
retainedGeneratedIds.add(row.getId());
|
||||
if (before.compareTo(row.getAdjustAmount()) != 0) {
|
||||
Map<String, Object> beforeData = new LinkedHashMap<>();
|
||||
Map<String, Object> afterData = new LinkedHashMap<>();
|
||||
beforeData.put("费用项", row.getFeeItem());
|
||||
beforeData.put("调整金额", before);
|
||||
beforeData.put("结算金额", money(row.getOriginalAmount().add(before)));
|
||||
afterData.put("费用项", row.getFeeItem());
|
||||
afterData.put("调整金额", row.getAdjustAmount());
|
||||
afterData.put("结算金额", money(row.getSettlementAmount()));
|
||||
saveChange(settlementId, "合计费用项", row.getLineNo(), "调整",
|
||||
"【" + row.getFeeItem() + "-调整金额】从【" + before + "】调整为【" + row.getAdjustAmount() + "】",
|
||||
"", beforeData, afterData);
|
||||
}
|
||||
}
|
||||
for (PreSettlementSummaryFee manualRow : manualRows) {
|
||||
if (!retainedManualIds.contains(manualRow.getId())) {
|
||||
summaryFeeMapper.deleteById(manualRow.getId());
|
||||
Map<String, Object> beforeData = new LinkedHashMap<>();
|
||||
Map<String, Object> afterData = new LinkedHashMap<>();
|
||||
beforeData.put("费用类型", manualRow.getFeeType());
|
||||
beforeData.put("费用项", manualRow.getFeeItem());
|
||||
beforeData.put("调整金额", money(manualRow.getAdjustAmount()));
|
||||
beforeData.put("结算金额", money(manualRow.getSettlementAmount()));
|
||||
beforeData.put("备注", manualRow.getRemark());
|
||||
afterData.put("费用类型", "");
|
||||
afterData.put("费用项", "");
|
||||
afterData.put("调整金额", BigDecimal.ZERO.setScale(2));
|
||||
afterData.put("结算金额", BigDecimal.ZERO.setScale(2));
|
||||
afterData.put("备注", "");
|
||||
saveChange(settlementId, "合计费用项", manualRow.getLineNo(), "删除",
|
||||
"删除合计费用项【" + manualRow.getFeeItem() + "】金额【" + money(manualRow.getSettlementAmount()) + "】",
|
||||
"", beforeData, afterData);
|
||||
}
|
||||
}
|
||||
for (PreSettlementSummaryFee generatedRow : existingGeneratedRows) {
|
||||
@@ -1563,38 +1531,87 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
}
|
||||
}
|
||||
|
||||
private void saveManualSummaryFeeChange(Long settlementId, boolean creating, String beforeFeeType,
|
||||
String beforeFeeItem, BigDecimal beforeAdjustAmount, BigDecimal beforeSettlementAmount, String beforeRemark,
|
||||
PreSettlementSummaryFee row) {
|
||||
List<String> changes = new ArrayList<>();
|
||||
private void recordSummaryFeeChanges(Long settlementId, List<PreSettlementSummaryFee> beforeRows) {
|
||||
List<Map<String, Object>> beforeLines = summaryFeeLines(beforeRows);
|
||||
List<Map<String, Object>> afterLines = summaryFeeLines(listSummaryFees(settlementId));
|
||||
if (beforeLines.equals(afterLines)) {
|
||||
return;
|
||||
}
|
||||
String content = summaryFeeChangeContent(beforeLines, afterLines);
|
||||
if (Func.isEmpty(content)) {
|
||||
return;
|
||||
}
|
||||
Map<String, Object> beforeData = new LinkedHashMap<>();
|
||||
Map<String, Object> afterData = new LinkedHashMap<>();
|
||||
if (creating) {
|
||||
changes.add("新增合计费用项【" + row.getFeeItem() + "】金额【" + money(row.getSettlementAmount()) + "】");
|
||||
beforeData.put("费用类型", "");
|
||||
beforeData.put("费用项", "");
|
||||
beforeData.put("调整金额", BigDecimal.ZERO.setScale(2));
|
||||
beforeData.put("结算金额", BigDecimal.ZERO.setScale(2));
|
||||
beforeData.put("备注", "");
|
||||
afterData.put("费用类型", row.getFeeType());
|
||||
afterData.put("费用项", row.getFeeItem());
|
||||
afterData.put("调整金额", money(row.getAdjustAmount()));
|
||||
afterData.put("结算金额", money(row.getSettlementAmount()));
|
||||
afterData.put("备注", row.getRemark());
|
||||
saveChange(settlementId, "合计费用项", row.getLineNo(), "新增", String.join(";", changes), "",
|
||||
beforeData, afterData);
|
||||
return;
|
||||
beforeData.put("结算合计", beforeLines);
|
||||
afterData.put("结算合计", afterLines);
|
||||
String operationType = beforeLines.isEmpty() ? "新增" : afterLines.isEmpty() ? "删除" : "调整";
|
||||
saveChange(settlementId, "结算合计", null, operationType, content, "", beforeData, afterData);
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> summaryFeeLines(List<PreSettlementSummaryFee> rows) {
|
||||
Map<String, String> categoryNames = DictBizCache.getList("fee_category").stream()
|
||||
.collect(Collectors.toMap(DictBiz::getDictKey, DictBiz::getDictValue, (first, second) -> first));
|
||||
List<Map<String, Object>> lines = new ArrayList<>();
|
||||
for (PreSettlementSummaryFee row : rows) {
|
||||
String feeType = row.getFeeType() == null ? "" : row.getFeeType();
|
||||
Map<String, Object> line = new LinkedHashMap<>();
|
||||
line.put("来源", Integer.valueOf(1).equals(row.getManualFlag()) ? "手工新增" : "系统归集");
|
||||
line.put("费用类型", categoryNames.getOrDefault(feeType, feeType));
|
||||
line.put("费用项", row.getFeeItem() == null ? "" : row.getFeeItem());
|
||||
line.put("原金额", money(row.getOriginalAmount()));
|
||||
line.put("调整金额", money(row.getAdjustAmount()));
|
||||
line.put("结算金额", money(row.getSettlementAmount()));
|
||||
line.put("备注", row.getRemark() == null ? "" : row.getRemark());
|
||||
lines.add(line);
|
||||
}
|
||||
appendTextChange(changes, beforeData, afterData, "费用类型", beforeFeeType, row.getFeeType());
|
||||
appendTextChange(changes, beforeData, afterData, "费用项", beforeFeeItem, row.getFeeItem());
|
||||
appendChange(changes, beforeData, afterData, "调整金额", beforeAdjustAmount, row.getAdjustAmount());
|
||||
appendChange(changes, beforeData, afterData, "结算金额", beforeSettlementAmount, row.getSettlementAmount());
|
||||
appendTextChange(changes, beforeData, afterData, "备注", beforeRemark, row.getRemark());
|
||||
if (changes.isEmpty()) {
|
||||
return;
|
||||
return lines;
|
||||
}
|
||||
|
||||
private String summaryFeeChangeContent(List<Map<String, Object>> beforeLines, List<Map<String, Object>> afterLines) {
|
||||
Map<String, Map<String, Object>> beforeMap = indexSummaryLines(beforeLines);
|
||||
Map<String, Map<String, Object>> afterMap = indexSummaryLines(afterLines);
|
||||
List<String> changes = new ArrayList<>();
|
||||
afterMap.forEach((key, afterLine) -> {
|
||||
Map<String, Object> beforeLine = beforeMap.get(key);
|
||||
String label = summaryLineLabel(afterLine);
|
||||
if (beforeLine == null) {
|
||||
changes.add("新增结算合计【" + label + "】结算金额【" + afterLine.get("结算金额") + "】");
|
||||
return;
|
||||
}
|
||||
if (!Objects.equals(beforeLine.get("原金额"), afterLine.get("原金额"))
|
||||
|| !Objects.equals(beforeLine.get("调整金额"), afterLine.get("调整金额"))
|
||||
|| !Objects.equals(beforeLine.get("结算金额"), afterLine.get("结算金额"))
|
||||
|| !Objects.equals(String.valueOf(beforeLine.get("备注")), String.valueOf(afterLine.get("备注")))) {
|
||||
changes.add("调整结算合计【" + label + "】结算金额从【" + beforeLine.get("结算金额")
|
||||
+ "】调整为【" + afterLine.get("结算金额") + "】");
|
||||
}
|
||||
});
|
||||
beforeMap.forEach((key, beforeLine) -> {
|
||||
if (!afterMap.containsKey(key)) {
|
||||
changes.add("删除结算合计【" + summaryLineLabel(beforeLine) + "】结算金额【" + beforeLine.get("结算金额") + "】");
|
||||
}
|
||||
});
|
||||
return String.join(";", changes);
|
||||
}
|
||||
|
||||
private Map<String, Map<String, Object>> indexSummaryLines(List<Map<String, Object>> lines) {
|
||||
Map<String, Map<String, Object>> indexed = new LinkedHashMap<>();
|
||||
Map<String, Integer> counts = new HashMap<>();
|
||||
for (Map<String, Object> line : lines) {
|
||||
String base = String.valueOf(line.get("来源")) + "|" + line.get("费用类型") + "|" + line.get("费用项");
|
||||
int index = counts.merge(base, 1, Integer::sum);
|
||||
indexed.put(base + "#" + index, line);
|
||||
}
|
||||
saveChange(settlementId, "合计费用项", row.getLineNo(), "调整", String.join(";", changes), "",
|
||||
beforeData, afterData);
|
||||
return indexed;
|
||||
}
|
||||
|
||||
private String summaryLineLabel(Map<String, Object> line) {
|
||||
String feeType = String.valueOf(line.getOrDefault("费用类型", ""));
|
||||
String feeItem = String.valueOf(line.getOrDefault("费用项", ""));
|
||||
String source = String.valueOf(line.getOrDefault("来源", ""));
|
||||
String name = feeType.isBlank() ? feeItem : feeType + "-" + feeItem;
|
||||
return source + " " + name;
|
||||
}
|
||||
|
||||
private void saveChange(Long settlementId, String changeType, Integer lineNo, String operationType,
|
||||
|
||||
+27
-11
@@ -424,9 +424,7 @@ public class ReceivablePayableDetailServiceImpl
|
||||
}
|
||||
});
|
||||
ReceivablePayableFeeDetailVO result = buildFeeDetail(rows);
|
||||
LinkedHashSet<String> feeItemNames = new LinkedHashSet<>(contractFeeItemNames(detail.getContractId()));
|
||||
feeItemNames.addAll(result.getFeeItemNames());
|
||||
result.setFeeItemNames(new ArrayList<>(feeItemNames));
|
||||
// 只展示本明细已生成的费用项。合同上其他计费方案的费用项不能叠到当前明细。
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1968,21 +1966,35 @@ public class ReceivablePayableDetailServiceImpl
|
||||
}
|
||||
|
||||
private List<ReceivablePayableCargoFee> calculatedFees(Waybill waybill, ContractManage contract, String planId) {
|
||||
return calculatedFees(waybill, contract, planId, false);
|
||||
return calculatedFees(waybill, contract, planId, false, false);
|
||||
}
|
||||
|
||||
private List<ReceivablePayableCargoFee> calculatedFees(Waybill waybill, ContractManage contract, String planId, boolean matchOnly) {
|
||||
private List<ReceivablePayableCargoFee> calculatedFees(Waybill waybill, ContractManage contract, String planId,
|
||||
boolean matchOnly) {
|
||||
return calculatedFees(waybill, contract, planId, matchOnly, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param strictPlan 更新费用时为 true:选定方案有规则但未命中时,不再回退运单历史费用,避免旧费用项叠加
|
||||
*/
|
||||
private List<ReceivablePayableCargoFee> calculatedFees(Waybill waybill, ContractManage contract, String planId,
|
||||
boolean matchOnly, boolean strictPlan) {
|
||||
List<Map<String, Object>> plans = parseList(contract == null ? null : contract.getBillingPlanJson());
|
||||
Map<String, Object> plan = "__matched__".equals(planId)
|
||||
? resolveDefaultBillingPlan(plans, waybill.getTransportType())
|
||||
: resolveBillingPlan(plans, planId);
|
||||
return calculatedFees(waybill, plan, matchOnly);
|
||||
return calculatedFees(waybill, plan, matchOnly, strictPlan);
|
||||
}
|
||||
|
||||
private List<ReceivablePayableCargoFee> calculatedFees(Waybill waybill, Map<String, Object> plan,
|
||||
boolean matchOnly) {
|
||||
return calculatedFees(waybill, plan, matchOnly, false);
|
||||
}
|
||||
|
||||
private List<ReceivablePayableCargoFee> calculatedFees(Waybill waybill, Map<String, Object> plan,
|
||||
boolean matchOnly, boolean strictPlan) {
|
||||
if (plan == null || !(plan.get("rules") instanceof List<?>)) {
|
||||
return matchOnly ? List.of() : buildCargoFees(waybill);
|
||||
return matchOnly || strictPlan ? List.of() : buildCargoFees(waybill);
|
||||
}
|
||||
Map<List<String>, ReceivablePayableCargoFee> feesByCargo = new LinkedHashMap<>();
|
||||
Map<List<String>, Map<String, BigDecimal>> feeItemsByCargo = new LinkedHashMap<>();
|
||||
@@ -2024,7 +2036,11 @@ public class ReceivablePayableDetailServiceImpl
|
||||
ReceivablePayableCargoFee fee = result.get(index);
|
||||
fee.setLineNo(String.format("%04d", index + 1));
|
||||
}
|
||||
return result.isEmpty() && !matchOnly ? buildCargoFees(waybill) : result;
|
||||
// 已选定计费方案时,只保留该方案算出的费用项,不用运单上的历史费用回填。
|
||||
if (strictPlan || matchOnly) {
|
||||
return result;
|
||||
}
|
||||
return result.isEmpty() ? buildCargoFees(waybill) : result;
|
||||
}
|
||||
|
||||
private ReceivablePayableCargoFee buildCalculatedCargoFee(Waybill waybill, Waybill feeWaybill,
|
||||
@@ -2934,7 +2950,7 @@ public class ReceivablePayableDetailServiceImpl
|
||||
throw new ServiceException("关联运单不存在");
|
||||
}
|
||||
ContractManage contract = contractManageService.getById(detail.getContractId());
|
||||
List<ReceivablePayableCargoFee> fees = calculatedFees(waybill, contract, billingPlanId);
|
||||
List<ReceivablePayableCargoFee> fees = calculatedFees(waybill, contract, billingPlanId, false, true);
|
||||
cargoFeeMapper.delete(Wrappers.<ReceivablePayableCargoFee>lambdaQuery().eq(ReceivablePayableCargoFee::getDetailId, detail.getId()));
|
||||
fees.forEach(fee -> { fee.setDetailId(detail.getId()); cargoFeeMapper.insert(fee); });
|
||||
BigDecimal freight = fees.stream().map(fee -> money(fee.getFreightAmount()))
|
||||
@@ -2959,7 +2975,7 @@ public class ReceivablePayableDetailServiceImpl
|
||||
ContractManage contract = contractManageService.getById(detail.getContractId());
|
||||
List<Waybill> masterGoods = masterOrderGoods(masterOrder);
|
||||
List<ReceivablePayableCargoFee> fees = masterGoods.stream()
|
||||
.flatMap(goods -> calculatedFees(goods, contract, billingPlanId).stream()).toList();
|
||||
.flatMap(goods -> calculatedFees(goods, contract, billingPlanId, false, true).stream()).toList();
|
||||
normalizeMasterFeeLines(fees);
|
||||
cargoFeeMapper.delete(Wrappers.<ReceivablePayableCargoFee>lambdaQuery()
|
||||
.eq(ReceivablePayableCargoFee::getDetailId, detail.getId()));
|
||||
@@ -2995,7 +3011,7 @@ public class ReceivablePayableDetailServiceImpl
|
||||
}
|
||||
ContractManage contract = contractManageService.getById(detail.getContractId());
|
||||
List<ReceivablePayableCargoFee> fees = waybills.stream()
|
||||
.flatMap(waybill -> calculatedFees(waybill, contract, billingPlanId).stream()).toList();
|
||||
.flatMap(waybill -> calculatedFees(waybill, contract, billingPlanId, false, true).stream()).toList();
|
||||
normalizeWaybillFeeLines(fees);
|
||||
cargoFeeMapper.delete(Wrappers.<ReceivablePayableCargoFee>lambdaQuery()
|
||||
.eq(ReceivablePayableCargoFee::getDetailId, detail.getId()));
|
||||
|
||||
+10
-22
@@ -41,7 +41,6 @@ import org.springblade.core.tool.utils.BeanUtil;
|
||||
import org.springblade.core.tool.utils.DateUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.system.cache.DictBizCache;
|
||||
import org.springblade.system.cache.DictCache;
|
||||
import org.springblade.system.cache.UserCache;
|
||||
import org.springblade.system.feign.ISysClient;
|
||||
import org.springblade.system.pojo.entity.DictBiz;
|
||||
@@ -824,7 +823,7 @@ public class SettlementImportBatchServiceImpl
|
||||
for (Map.Entry<String, BigDecimal> entry : feeItems.entrySet()) {
|
||||
String name = normalizeFeeName(entry.getKey());
|
||||
BigDecimal amount = entry.getValue() == null ? BigDecimal.ZERO : entry.getValue();
|
||||
// 模板自带的装卸费、过路费等列未填写时金额为 0,不要求费用项主数据存在同名档案
|
||||
// 用户自行增列的费用项金额为 0 时跳过校验;金额不为 0 须匹配系统已启用费用项
|
||||
if (Func.isEmpty(name) || amount.compareTo(BigDecimal.ZERO) == 0) {
|
||||
continue;
|
||||
}
|
||||
@@ -1149,9 +1148,6 @@ public class SettlementImportBatchServiceImpl
|
||||
excel.setMileage(decimalValue(row, "mileage", "里程(km)", "里程"));
|
||||
excel.setUnitPrice(decimalValue(row, "unitPrice", "运输单价", "单价"));
|
||||
excel.setTransportFee(decimalValue(row, "transportFee", "*运输费", "运输费"));
|
||||
Map<String, BigDecimal> feeItems = extractFeeItems(row, List.of("装卸费", "过路费"));
|
||||
excel.setLoadingFee(feeItems.get("装卸费"));
|
||||
excel.setTollFee(feeItems.get("过路费"));
|
||||
excel.setSettlementAmount(decimalValue(row, "settlementAmount", "结算金额"));
|
||||
excel.setActualStartDate(stringValue(row, "actualStartDate", "*实际发货时间", "实际发货时间"));
|
||||
excel.setActualEndDate(stringValue(row, "actualEndDate", "*实际完成时间", "实际完成时间"));
|
||||
@@ -1330,34 +1326,26 @@ public class SettlementImportBatchServiceImpl
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 运输方式取业务字典 transport_type 的字典名称(dictValue),不写死枚举。
|
||||
*/
|
||||
private List<String> loadTransportTypeLabels() {
|
||||
LinkedHashSet<String> labels = new LinkedHashSet<>();
|
||||
try {
|
||||
List<DictBiz> dictBizList = DictBizCache.getList("transport_type");
|
||||
if (Func.isNotEmpty(dictBizList)) {
|
||||
dictBizList.stream()
|
||||
.map(DictBiz::getDictValue)
|
||||
.filter(dict -> dict != null && Func.isNotEmpty(dict.getDictValue())
|
||||
&& !"-1".equals(Func.toStr(dict.getDictKey())))
|
||||
.map(dict -> dict.getDictValue().trim())
|
||||
.filter(Func::isNotEmpty)
|
||||
.map(String::trim)
|
||||
.forEach(labels::add);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 业务字典不可用时继续读取系统字典
|
||||
}
|
||||
try {
|
||||
List<org.springblade.system.pojo.entity.Dict> dictList = DictCache.getList("transport_type");
|
||||
if (Func.isNotEmpty(dictList)) {
|
||||
dictList.stream()
|
||||
.map(org.springblade.system.pojo.entity.Dict::getDictValue)
|
||||
.filter(Func::isNotEmpty)
|
||||
.map(String::trim)
|
||||
.forEach(labels::add);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 忽略
|
||||
} catch (Exception exception) {
|
||||
throw new ServiceException("业务字典 transport_type 读取失败,请检查业务字典配置");
|
||||
}
|
||||
if (labels.isEmpty()) {
|
||||
return List.of("公路运输", "铁路运输", "水路运输", "航空运输");
|
||||
throw new ServiceException("业务字典 transport_type 未配置运输方式,请先维护字典名称");
|
||||
}
|
||||
return new ArrayList<>(labels);
|
||||
}
|
||||
|
||||
+31
-1
@@ -81,6 +81,8 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
|
||||
|
||||
@Override
|
||||
public IPage<TransportPlanVO> selectTransportPlanPage(IPage<TransportPlan> page, TransportPlanVO transportPlan) {
|
||||
// 历史手工创建未写入 dept_id,allDept=0 按组织过滤时会被排除
|
||||
backfillMissingDept(transportPlan);
|
||||
IPage<TransportPlan> entityPage = page(page, buildQuery(transportPlan));
|
||||
IPage<TransportPlanVO> resultPage = TransportPlanWrapper.build().pageVO(entityPage);
|
||||
fillCustomerNamesFromContracts(resultPage.getRecords());
|
||||
@@ -151,6 +153,12 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
|
||||
if (created) {
|
||||
transportPlan.setPlanNo(nextCode());
|
||||
}
|
||||
// 新建或历史空组织一律落到当前组织,避免列表按 dept_id 过滤后看不到
|
||||
if (created || Func.isEmpty(transportPlan.getDeptId()) || transportPlan.getDeptId() <= 0) {
|
||||
Dept dept = TransportBusinessSupport.currentDept("运输计划");
|
||||
transportPlan.setDeptId(dept.getId());
|
||||
transportPlan.setDeptName(dept.getDeptName());
|
||||
}
|
||||
validate(transportPlan);
|
||||
return saveOrUpdate(transportPlan);
|
||||
}
|
||||
@@ -662,11 +670,33 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 把所属组织为空、且创建组织等于当前组织的运输计划回填 dept_id,使 allDept=0 列表能查到。
|
||||
*/
|
||||
private void backfillMissingDept(TransportPlanVO transportPlan) {
|
||||
if (Objects.equals(transportPlan.getAllDept(), 1)) {
|
||||
return;
|
||||
}
|
||||
Dept dept = TransportBusinessSupport.currentDept("运输计划");
|
||||
if (dept == null || dept.getId() == null) {
|
||||
return;
|
||||
}
|
||||
update(Wrappers.<TransportPlan>lambdaUpdate()
|
||||
.set(TransportPlan::getDeptId, dept.getId())
|
||||
.set(TransportPlan::getDeptName, dept.getDeptName())
|
||||
.isNull(TransportPlan::getDeptId)
|
||||
.eq(TransportPlan::getCreateDept, dept.getId())
|
||||
.eq(TransportPlan::getIsDeleted, 0));
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<TransportPlan> buildQuery(TransportPlanVO transportPlan) {
|
||||
TransportBusinessSupport.validateAllDept(transportPlan.getAllDept(), "运输计划");
|
||||
LambdaQueryWrapper<TransportPlan> queryWrapper = Wrappers.<TransportPlan>lambdaQuery().eq(TransportPlan::getIsDeleted, 0);
|
||||
if (!Objects.equals(transportPlan.getAllDept(), 1)) {
|
||||
queryWrapper.eq(TransportPlan::getDeptId, TransportBusinessSupport.currentDeptId("运输计划"));
|
||||
Long currentDeptId = TransportBusinessSupport.currentDeptId("运输计划");
|
||||
// dept_id 为空的历史数据按创建组织归入当前组织
|
||||
queryWrapper.and(wrapper -> wrapper.eq(TransportPlan::getDeptId, currentDeptId)
|
||||
.or(item -> item.isNull(TransportPlan::getDeptId).eq(TransportPlan::getCreateDept, currentDeptId)));
|
||||
} else if (Func.isNotEmpty(transportPlan.getDeptId())) {
|
||||
queryWrapper.eq(TransportPlan::getDeptId, transportPlan.getDeptId());
|
||||
}
|
||||
|
||||
+2
-2
@@ -291,13 +291,13 @@ public class TransportVehicleServiceImpl extends BaseServiceImpl<TransportVehicl
|
||||
if (Func.isEmpty(vehicle.getDrivingLicenseNo())) {
|
||||
throw new ServiceException("行驶证档案编号不能为空");
|
||||
}
|
||||
if (Func.isEmpty(vehicle.getDrivingLicenseEndDate())) {
|
||||
if (vehicle.getDrivingLicenseLongTerm() != 1 && Func.isEmpty(vehicle.getDrivingLicenseEndDate())) {
|
||||
throw new ServiceException("行驶证有效期不能为空");
|
||||
}
|
||||
if (Func.isEmpty(vehicle.getRoadTransportCertNo())) {
|
||||
throw new ServiceException("道路运输证号不能为空");
|
||||
}
|
||||
if (Func.isEmpty(vehicle.getRoadTransportCertEndDate())) {
|
||||
if (vehicle.getRoadTransportCertLongTerm() != 1 && Func.isEmpty(vehicle.getRoadTransportCertEndDate())) {
|
||||
throw new ServiceException("道路运输证有效期不能为空");
|
||||
}
|
||||
if (vehicle.getAnnualReviewLongTerm() != 1 && Func.isEmpty(vehicle.getAnnualReviewEndDate())) {
|
||||
|
||||
+177
-43
@@ -30,7 +30,9 @@ import org.springblade.transport.excel.WaybillImportBatchExcel;
|
||||
import org.springblade.transport.mapper.WaybillImportBatchMapper;
|
||||
import org.springblade.transport.pojo.dto.WaybillImportBatchRequest;
|
||||
import org.springblade.transport.pojo.entity.CustomerArchive;
|
||||
import org.springblade.transport.pojo.entity.LoadingManage;
|
||||
import org.springblade.transport.pojo.entity.ProjectApply;
|
||||
import org.springblade.transport.pojo.entity.ReceivablePayableDetail;
|
||||
import org.springblade.transport.pojo.entity.TransportPlan;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.pojo.entity.WaybillImportBatch;
|
||||
@@ -76,6 +78,13 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
private static final String STATUS_COMPLETED = "completed";
|
||||
private static final String IMPORT_TYPE_WAYBILL = "waybill";
|
||||
private static final String IMPORT_TYPE_SETTLEMENT = "settlement";
|
||||
/** 批次结算状态:全部运单均未进入预/正式结算。 */
|
||||
private static final String SETTLEMENT_UNSETTLED = "unsettled";
|
||||
/** 批次结算状态:部分运单已进入结算。 */
|
||||
private static final String SETTLEMENT_PARTIAL = "partial";
|
||||
/** 批次结算状态:全部运单均已进入结算。 */
|
||||
private static final String SETTLEMENT_SETTLED = "settled";
|
||||
private static final String LOADING_DATA_SOURCE_IMPORT = "批量导入";
|
||||
/** 车牌号校验正则:首位汉字,次位大写字母,总长度7或8位 */
|
||||
private static final Pattern VEHICLE_NO_PATTERN = Pattern.compile("^[一-龥][A-Z][A-Z0-9]{5,6}$");
|
||||
/** 中国机动车号牌省份简称。 */
|
||||
@@ -358,6 +367,7 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
}
|
||||
IPage<WaybillImportBatch> entityPage = page(page, queryWrapper);
|
||||
List<WaybillImportBatchVO> records = entityPage.getRecords().stream().map(this::toVO).toList();
|
||||
fillSettlementStatus(records);
|
||||
Page<WaybillImportBatchVO> resultPage = new Page<>(entityPage.getCurrent(), entityPage.getSize(), entityPage.getTotal());
|
||||
resultPage.setRecords(records);
|
||||
return resultPage;
|
||||
@@ -389,18 +399,33 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
List<WaybillImportBatch> batches = listByIds(idList).stream()
|
||||
.filter(batch -> !Objects.equals(batch.getIsDeleted(), 1))
|
||||
.toList();
|
||||
// 与前端一致:仅草稿可删;选中含已完成/进行中时整批拒绝
|
||||
boolean hasNonDraft = batches.stream()
|
||||
.anyMatch(batch -> !STATUS_DRAFT.equals(batch.getImportStatus()));
|
||||
if (hasNonDraft) {
|
||||
throw new ServiceException("已完成运单不可删除");
|
||||
if (Func.isEmpty(batches)) {
|
||||
throw new ServiceException("运单批次不存在或已删除");
|
||||
}
|
||||
Map<Long, List<Waybill>> waybillsByBatchId = listWaybillsByBatchIds(
|
||||
batches.stream().map(WaybillImportBatch::getId).toList());
|
||||
List<Long> allWaybillIds = waybillsByBatchId.values().stream()
|
||||
.flatMap(List::stream)
|
||||
.map(Waybill::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
Set<Long> settledWaybillIds = receivablePayableDetailService.settlementLinkedWaybillIds(allWaybillIds);
|
||||
// 仅未结算批次可整批删除;选中含部分结算/已结算时整批拒绝
|
||||
for (WaybillImportBatch batch : batches) {
|
||||
List<Waybill> waybills = waybillsByBatchId.getOrDefault(batch.getId(), List.of());
|
||||
String settlementStatus = resolveSettlementStatus(waybills, settledWaybillIds);
|
||||
if (!SETTLEMENT_UNSETTLED.equals(settlementStatus)) {
|
||||
throw new ServiceException("仅未结算的运单批次可以删除");
|
||||
}
|
||||
}
|
||||
BusinessRemoveResultVO result = new BusinessRemoveResultVO();
|
||||
for (WaybillImportBatch batch : batches) {
|
||||
List<Waybill> waybills = waybillService.list(Wrappers.<Waybill>lambdaQuery()
|
||||
.eq(Waybill::getImportBatchId, batch.getId()).eq(Waybill::getIsDeleted, 0));
|
||||
if (Func.isNotEmpty(waybills)) {
|
||||
waybillService.deleteLogic(waybills.stream().map(Waybill::getId).toList());
|
||||
List<Waybill> waybills = waybillsByBatchId.getOrDefault(batch.getId(), List.of());
|
||||
List<Long> waybillIds = waybills.stream().map(Waybill::getId).filter(Objects::nonNull).toList();
|
||||
if (Func.isNotEmpty(waybillIds)) {
|
||||
clearUnsettledReceivableDetails(waybillIds);
|
||||
clearImportedLoadings(waybills);
|
||||
waybillService.deleteLogic(waybillIds);
|
||||
}
|
||||
deleteLogic(List.of(batch.getId()));
|
||||
result.setSuccessCount(result.getSuccessCount() + 1);
|
||||
@@ -671,11 +696,148 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
if (vo == null) throw new ServiceException("运单批次数据转换失败");
|
||||
vo.setImportTypeName(IMPORT_TYPE_SETTLEMENT.equals(batch.getImportType()) ? "结算单" : "运单");
|
||||
vo.setStatusName(importStatusName(batch.getImportStatus()));
|
||||
vo.setSettlementStatus(SETTLEMENT_UNSETTLED);
|
||||
vo.setSettlementStatusName(settlementStatusName(SETTLEMENT_UNSETTLED));
|
||||
vo.setCreateUserName(UserCache.getUserRealName(batch.getCreateUser()));
|
||||
vo.setUpdateUserName(UserCache.getUserRealName(batch.getUpdateUser()));
|
||||
return vo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按批次下运单是否已关联预/正式结算单,汇总结算状态:
|
||||
* 全部未关联=未结算,部分关联=部分结算,全部关联=已结算。
|
||||
*/
|
||||
private void fillSettlementStatus(List<WaybillImportBatchVO> records) {
|
||||
if (Func.isEmpty(records)) {
|
||||
return;
|
||||
}
|
||||
List<Long> batchIds = records.stream().map(WaybillImportBatchVO::getId).filter(Objects::nonNull).toList();
|
||||
Map<Long, List<Waybill>> waybillsByBatchId = listWaybillsByBatchIds(batchIds);
|
||||
List<Long> allWaybillIds = waybillsByBatchId.values().stream()
|
||||
.flatMap(List::stream)
|
||||
.map(Waybill::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
Set<Long> settledWaybillIds = receivablePayableDetailService.settlementLinkedWaybillIds(allWaybillIds);
|
||||
for (WaybillImportBatchVO record : records) {
|
||||
List<Waybill> waybills = waybillsByBatchId.getOrDefault(record.getId(), List.of());
|
||||
String settlementStatus = resolveSettlementStatus(waybills, settledWaybillIds);
|
||||
record.setSettlementStatus(settlementStatus);
|
||||
record.setSettlementStatusName(settlementStatusName(settlementStatus));
|
||||
}
|
||||
}
|
||||
|
||||
private Map<Long, List<Waybill>> listWaybillsByBatchIds(List<Long> batchIds) {
|
||||
if (Func.isEmpty(batchIds)) {
|
||||
return Map.of();
|
||||
}
|
||||
List<Waybill> waybills = waybillService.list(Wrappers.<Waybill>lambdaQuery()
|
||||
.select(Waybill::getId, Waybill::getImportBatchId, Waybill::getLoadingNo)
|
||||
.in(Waybill::getImportBatchId, batchIds)
|
||||
.eq(Waybill::getIsDeleted, 0));
|
||||
return waybills.stream()
|
||||
.filter(waybill -> waybill.getImportBatchId() != null)
|
||||
.collect(Collectors.groupingBy(Waybill::getImportBatchId));
|
||||
}
|
||||
|
||||
private String resolveSettlementStatus(List<Waybill> waybills, Set<Long> settledWaybillIds) {
|
||||
if (Func.isEmpty(waybills)) {
|
||||
return SETTLEMENT_UNSETTLED;
|
||||
}
|
||||
int settledCount = 0;
|
||||
for (Waybill waybill : waybills) {
|
||||
if (waybill.getId() != null && settledWaybillIds.contains(waybill.getId())) {
|
||||
settledCount++;
|
||||
}
|
||||
}
|
||||
if (settledCount <= 0) {
|
||||
return SETTLEMENT_UNSETTLED;
|
||||
}
|
||||
if (settledCount >= waybills.size()) {
|
||||
return SETTLEMENT_SETTLED;
|
||||
}
|
||||
return SETTLEMENT_PARTIAL;
|
||||
}
|
||||
|
||||
private String settlementStatusName(String settlementStatus) {
|
||||
if (SETTLEMENT_SETTLED.equals(settlementStatus)) {
|
||||
return "已结算";
|
||||
}
|
||||
if (SETTLEMENT_PARTIAL.equals(settlementStatus)) {
|
||||
return "部分结算";
|
||||
}
|
||||
return "未结算";
|
||||
}
|
||||
|
||||
/** 删除批次前清理尚未进入预/正式结算的应收应付明细,避免残留待结算数据。 */
|
||||
private void clearUnsettledReceivableDetails(List<Long> waybillIds) {
|
||||
if (Func.isEmpty(waybillIds)) {
|
||||
return;
|
||||
}
|
||||
List<ReceivablePayableDetail> details = receivablePayableDetailService.list(
|
||||
Wrappers.<ReceivablePayableDetail>lambdaQuery()
|
||||
.in(ReceivablePayableDetail::getWaybillId, waybillIds)
|
||||
.eq(ReceivablePayableDetail::getIsDeleted, 0)
|
||||
.and(wrapper -> wrapper.isNull(ReceivablePayableDetail::getPreSettlementNo)
|
||||
.or().eq(ReceivablePayableDetail::getPreSettlementNo, ""))
|
||||
.and(wrapper -> wrapper.isNull(ReceivablePayableDetail::getFormalSettlementNo)
|
||||
.or().eq(ReceivablePayableDetail::getFormalSettlementNo, "")));
|
||||
if (Func.isNotEmpty(details)) {
|
||||
receivablePayableDetailService.deleteLogic(
|
||||
details.stream().map(ReceivablePayableDetail::getId).toList());
|
||||
}
|
||||
}
|
||||
|
||||
/** 清理本批次运单关联的批量导入配载单(配载单运单均属本批时整单删除)。 */
|
||||
private void clearImportedLoadings(List<Waybill> waybills) {
|
||||
if (Func.isEmpty(waybills)) {
|
||||
return;
|
||||
}
|
||||
Set<Long> batchWaybillIds = waybills.stream()
|
||||
.map(Waybill::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toCollection(HashSet::new));
|
||||
Set<String> loadingNos = waybills.stream()
|
||||
.map(Waybill::getLoadingNo)
|
||||
.filter(Func::isNotEmpty)
|
||||
.collect(Collectors.toCollection(HashSet::new));
|
||||
if (Func.isEmpty(loadingNos)) {
|
||||
return;
|
||||
}
|
||||
List<LoadingManage> loadings = loadingManageService.list(Wrappers.<LoadingManage>lambdaQuery()
|
||||
.in(LoadingManage::getLoadingNo, loadingNos)
|
||||
.eq(LoadingManage::getDataSource, LOADING_DATA_SOURCE_IMPORT)
|
||||
.eq(LoadingManage::getIsDeleted, 0));
|
||||
List<Long> deleteLoadingIds = new ArrayList<>();
|
||||
for (LoadingManage loading : loadings) {
|
||||
List<Long> loadingWaybillIds = parseLoadingWaybillIds(loading.getWaybillIdsJson());
|
||||
if (Func.isEmpty(loadingWaybillIds) || batchWaybillIds.containsAll(loadingWaybillIds)) {
|
||||
deleteLoadingIds.add(loading.getId());
|
||||
}
|
||||
}
|
||||
if (Func.isNotEmpty(deleteLoadingIds)) {
|
||||
loadingManageService.deleteLogic(deleteLoadingIds);
|
||||
}
|
||||
}
|
||||
|
||||
private List<Long> parseLoadingWaybillIds(String waybillIdsJson) {
|
||||
if (Func.isEmpty(waybillIdsJson)) {
|
||||
return List.of();
|
||||
}
|
||||
try {
|
||||
Object parsed = JsonUtil.parse(waybillIdsJson, List.class);
|
||||
if (parsed instanceof List<?> list) {
|
||||
return list.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(item -> Long.valueOf(String.valueOf(item)))
|
||||
.toList();
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 兼容历史逗号分隔存储
|
||||
}
|
||||
return Func.toLongList(waybillIdsJson);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String nextBatchNo() {
|
||||
return nextCode();
|
||||
@@ -758,25 +920,22 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
// 10. 里程校验
|
||||
validatePositiveNumber(row, "mileage", "里程(km)", errors);
|
||||
|
||||
// 11. 运费合计校验
|
||||
validateFreightTotal(row, errors);
|
||||
|
||||
// 12. 实际发货时间校验
|
||||
// 11. 实际发货时间校验
|
||||
validateActualStartDate(row, isDraft, errors);
|
||||
|
||||
// 13. 实际完成时间校验
|
||||
// 12. 实际完成时间校验
|
||||
validateActualEndDate(row, isDraft, errors);
|
||||
|
||||
// 14. 预计发货时间校验
|
||||
// 13. 预计发货时间校验
|
||||
validatePlanStartDate(row, errors);
|
||||
|
||||
// 15. 预计完成时间校验
|
||||
// 14. 预计完成时间校验
|
||||
validatePlanEndDate(row, errors);
|
||||
|
||||
// 16. 同一运单标识号校验
|
||||
// 15. 同一运单标识号校验
|
||||
validateWaybillIdentifier(row, i, waybillIdentifierMap, rows, errors);
|
||||
|
||||
// 17. 同一运单标识号组内一致性校验(地址/配载标识号/数量单位,多货物合并的前提)
|
||||
// 16. 同一运单标识号组内一致性校验(地址/配载标识号/数量单位,多货物合并的前提)
|
||||
validateWaybillIdentifierConsistency(row, i, waybillIdentifierMap, rows, errors);
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
@@ -939,31 +1098,6 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
}
|
||||
}
|
||||
|
||||
private void validateFreightTotal(Map<String, Object> row, List<String> errors) {
|
||||
String freightTotalStr = stringValue(row, "freightTotal", "运费合计");
|
||||
if (Func.isEmpty(freightTotalStr) || isImportOptionalNumberEmpty(freightTotalStr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
BigDecimal freightTotal = new BigDecimal(freightTotalStr);
|
||||
String freightStr = stringValue(row, "freight", "运费");
|
||||
String otherFeeTotalStr = stringValue(row, "otherFeeTotal", "其他费用合计");
|
||||
|
||||
BigDecimal freight = Func.isNotEmpty(freightStr) && !isImportOptionalNumberEmpty(freightStr)
|
||||
? new BigDecimal(freightStr) : BigDecimal.ZERO;
|
||||
BigDecimal otherFeeTotal = Func.isNotEmpty(otherFeeTotalStr) && !isImportOptionalNumberEmpty(otherFeeTotalStr)
|
||||
? new BigDecimal(otherFeeTotalStr) : BigDecimal.ZERO;
|
||||
|
||||
BigDecimal calculatedTotal = freight.add(otherFeeTotal);
|
||||
if (freightTotal.compareTo(calculatedTotal) != 0) {
|
||||
errors.add("运费合计应等于运费+其他费用合计");
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
errors.add("运费相关字段格式不正确");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateActualStartDate(Map<String, Object> row, boolean isDraft, List<String> errors) {
|
||||
Object actualStartDate = row.get("actualStartDate");
|
||||
if (actualStartDate == null || String.valueOf(actualStartDate).isBlank()) {
|
||||
|
||||
+6
-6
@@ -52,7 +52,7 @@ import static org.mockito.Mockito.when;
|
||||
* 三个取数入口必须同一条规则——列表、待处理、首页三档统计各走一次查询,漏一处就会出现
|
||||
* 「统计说 46 张、列表只出 18 张」这种对不上的数;待重新派单另走库层分组查询,也要单独带上。
|
||||
* <p>
|
||||
* 组织取不到时报错而不是放行:放行等于把全租户的运单发给一个没有组织的账号。
|
||||
* 所属组织为空时不拦截:小程序调度端不要求账号必须挂组织。
|
||||
*/
|
||||
class ManageWaybillOrgScopeTest {
|
||||
|
||||
@@ -190,13 +190,13 @@ class ManageWaybillOrgScopeTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("组织取不到时报错,不退回不带组织条件")
|
||||
void blankDeptFailsClosed() {
|
||||
@DisplayName("组织取不到时不拦截,小程序接口不按组织收口")
|
||||
void blankDeptDoesNotFail() {
|
||||
authUtil.when(AuthUtil::getDeptId).thenReturn("");
|
||||
|
||||
assertThrows(ServiceException.class,
|
||||
() -> service.pageList(1, 10, null, null, null, null, null, null),
|
||||
"没有组织的账号不能拿到全租户运单");
|
||||
service.pageList(1, 10, null, null, null, null, null, null);
|
||||
|
||||
assertTrue(capturedPageQuery().getDeptIds().isEmpty(), "没有组织时不报错、不按组织过滤");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 夹具
|
||||
|
||||
@@ -305,6 +305,7 @@ CREATE TABLE `blade_dept` (
|
||||
`dept_code` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '组织编码',
|
||||
`kingdee_dept_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '金蝶部门代码',
|
||||
`contract_code_prefix` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '合同编码前缀',
|
||||
`contract_initial_serial` int NULL DEFAULT NULL COMMENT '合同初始流水号',
|
||||
`short_name` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '组织简称',
|
||||
`pinyin_mnemonic` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '拼音助记码',
|
||||
`mnemonic_code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '助记码',
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
-- 组织机构新增合同初始流水号:该部门当年首个合同从此流水号起编,后续递增
|
||||
ALTER TABLE `blade_dept`
|
||||
ADD COLUMN `contract_initial_serial` int(11) DEFAULT NULL COMMENT '合同初始流水号' AFTER `contract_code_prefix`;
|
||||
Reference in New Issue
Block a user