🐛 修复收款流水继续认领与付款可付口径,并改用OA登录名提交流程

收款流水部分认领预填关联结算单并修正认领完成判定;正式结算尾款扣减预付占用;预付驳回重提纠正结算引用;临时额度取消强制项目额度校验;用户增加OA登录名供MK流程使用。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-19 17:12:22 +08:00
parent 68a5d581f3
commit fce582e80b
15 changed files with 337 additions and 84 deletions
@@ -39,6 +39,7 @@ import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 收款流水视图实体类
@@ -91,4 +92,9 @@ public class ReceiptFlowVO extends KingdeeReceiptFlow {
private BigDecimal receiptAmountEnd;
@TableField(exist = false)
private List<ReceiptFlowRecord> claimRecords = new ArrayList<>();
/**
* 本流水历史关联且仍有剩余可认领金额的应收正式结算单(继续认领时预填)
*/
@TableField(exist = false)
private List<Map<String, Object>> relatedSettlements = new ArrayList<>();
}
@@ -132,6 +132,10 @@ public class User extends TenantEntity implements IPhone {
* 是否是oa的账号
*/
private Integer isOa;
/**
* OA登录名
*/
private String oaLoginId;
/**
* OA上次更新时间(OA 的 modified 字段),仅同步比对使用
*/
@@ -11,7 +11,6 @@ import lombok.AllArgsConstructor;
import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.mp.support.Condition;
import org.springblade.core.mp.support.Query;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.process.pojo.dto.ApprovalDTO;
@@ -49,7 +48,6 @@ public class BusinessProcessController extends BladeController {
if (param == null) {
param = new ApprovalDTO();
}
param.setLoginName(AuthUtil.getUserAccount());
IPage<ApprovalVO> pages = businessProcessService.queryMkApprovalList(Condition.getPage(query), param);
return R.data(pages);
}
@@ -71,7 +71,7 @@ public interface IBusinessProcessService extends IService<BusinessProcess> {
* 获取流程当前节点详情
*
* @param processInstanceId 流程实例id
* @param loginName MK登录名(手机号),可为空,为空时按流程发起人解析
* @param loginName MK登录名(OA登录名),可为空,为空时按流程发起人解析
* @return 当前节点列表
*/
List<?> getCurrentNodes(String processInstanceId, String loginName);
@@ -80,7 +80,7 @@ public interface IBusinessProcessService extends IService<BusinessProcess> {
* 获取流程实例详情
*
* @param processInstanceId 流程实例id
* @param loginName MK登录名(手机号),可为空,为空时按流程发起人解析
* @param loginName MK登录名(OA登录名),可为空,为空时按流程发起人解析
* @return 流程实例详情
*/
Object getProcessInfo(String processInstanceId, String loginName);
@@ -132,9 +132,9 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
public String processSubmit(MKProcessCreateDTO param) {
AssertUtils.notNull(param, "提交流程参数不能为空");
AssertUtils.notBlank(param.getTemplateCode(), "模板编码不能为空");
// 前端拿到的手机号可能经 @Sensitive 脱敏(如 137****8880),这里从库取真实手机号覆盖
String loginName = resolveCurrentUserPhone();
AssertUtils.notBlank(loginName, "当前用户手机号为空,无法提交审核流");
// MK loginName 使用当前用户 OA 登录名,未配置则不允许提交
String loginName = resolveCurrentUserOaLoginId();
AssertUtils.notBlank(loginName, "当前用户OA登录名为空,无法提交审核流");
param.setSubmitIdentity(loginName);
param.setLoginName(loginName);
String bizType = param.getBizType();
@@ -258,7 +258,7 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
resolvedLoginName = businessProcess.getPromoterLoginName();
}
if (StringUtil.isBlank(resolvedLoginName)) {
resolvedLoginName = resolveCurrentUserPhone();
resolvedLoginName = resolveCurrentUserOaLoginId();
}
if (StringUtil.isBlank(resolvedLoginName)) {
return false;
@@ -291,10 +291,10 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
}
String loginName = businessProcess.getPromoterLoginName();
if (StringUtil.isBlank(loginName)) {
loginName = resolveCurrentUserPhone();
loginName = resolveCurrentUserOaLoginId();
}
if (StringUtil.isBlank(loginName)) {
log.warn("查询MK流程实例跳过,登录名为空 bizId={} processInstanceId={}",
log.warn("查询MK流程实例跳过,OA登录名为空 bizId={} processInstanceId={}",
bizId, businessProcess.getProcessInstanceId());
return false;
}
@@ -357,9 +357,9 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
}
String loginName = businessProcess.getPromoterLoginName();
if (StringUtil.isBlank(loginName)) {
loginName = resolveCurrentUserPhone();
loginName = resolveCurrentUserOaLoginId();
}
AssertUtils.notBlank(loginName, "当前用户手机号为空,无法删除审核流");
AssertUtils.notBlank(loginName, "当前用户OA登录名为空,无法删除审核流");
log.info("调用mk processDelete processInstanceId={} loginName={} bizId={} formInstanceId={}",
businessProcess.getProcessInstanceId(), loginName, bizId, businessProcess.getFormInstanceId());
boolean deleted = mkService.processDelete(businessProcess.getProcessInstanceId(), loginName);
@@ -563,27 +563,18 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
}
/**
* 从当前登录用户实体读取真实手机号(库内 SM2 密文先解密,绕过接口脱敏)
* 从当前登录用户实体读取 OA 登录名,作为 MK 接口 loginName
*/
private String resolveCurrentUserPhone() {
private String resolveCurrentUserOaLoginId() {
Long userId = AuthUtil.getUserId();
if (userId != null) {
User user = userService.getById(userId);
if (user != null) {
userService.decryptPhone(user);
if (StringUtil.isNotBlank(user.getPhone()) && !user.getPhone().contains("*")) {
return user.getPhone().trim();
}
if (StringUtil.isNotBlank(user.getAccount()) && user.getAccount().matches("^1\\d{10}$")) {
return user.getAccount().trim();
}
}
if (userId == null) {
return null;
}
String account = AuthUtil.getUserAccount();
if (StringUtil.isNotBlank(account) && account.matches("^1\\d{10}$")) {
return account.trim();
User user = userService.getById(userId);
if (user == null || StringUtil.isBlank(user.getOaLoginId())) {
return null;
}
return null;
return user.getOaLoginId().trim();
}
@Transactional(rollbackFor = Exception.class)
@@ -703,8 +694,8 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
processInstanceId = businessProcess.getProcessInstanceId();
}
try {
String loginName = resolveCurrentUserPhone();
AssertUtils.notBlank(loginName, "当前用户手机号为空,无法获取审核跳转地址");
String loginName = resolveCurrentUserOaLoginId();
AssertUtils.notBlank(loginName, "当前用户OA登录名为空,无法获取审核跳转地址");
String mkApprovalUrl = mkService.getMKApprovalUrl(processInstanceId, loginName);
AssertUtils.notBlank(mkApprovalUrl, "获取mk审批页面链接异常");
return mkApprovalUrl;
@@ -921,6 +912,9 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
@Override
public IPage<ApprovalVO> queryMkApprovalList(IPage<ApprovalVO> page, ApprovalDTO param) {
String loginName = resolveCurrentUserOaLoginId();
AssertUtils.notBlank(loginName, "当前用户OA登录名为空,无法查询审核列表");
param.setLoginName(loginName);
if (MKDoc.RELATED.getCode().equals(param.getDocType())) {
// 我参与的,调用流程列表接口
MKProcessDTO processParam = approvalConvert.dto2mk(param);
@@ -70,6 +70,7 @@ public interface UserConvert {
@Mapping(source = "lastname", target = "realName")
@Mapping(source = "mobile", target = "phone")
@Mapping(source = "email", target = "email")
@Mapping(source = "loginid", target = "oaLoginId")
@Mapping(source = "modified", target = "oaModified")
User baseConvert(OAPersonResponse person);
@@ -26,6 +26,7 @@
<result column="dept_id" property="deptId"/>
<result column="post_id" property="postId"/>
<result column="remark" property="remark"/>
<result column="oa_login_id" property="oaLoginId"/>
<result column="oa_person_id" property="oaPersonId"/>
</resultMap>
@@ -366,6 +366,7 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
user.setPostId(null);
user.setAccount(null);
user.setIsLeader(null);
user.setOaLoginId(null);
CacheUtil.clear(USER_CACHE);
return submitUserInfo(user);
}
@@ -1729,7 +1729,8 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
Map<Long, BigDecimal> receiptClaimPaidBySettlement = hasReceivable
? receiptClaimSettlementMapper.selectList(Wrappers.<ReceiptClaimSettlement>lambdaQuery()
.in(ReceiptClaimSettlement::getFormalSettlementId, settledIds)
.eq(ReceiptClaimSettlement::getStatus, 1)).stream()
.eq(ReceiptClaimSettlement::getStatus, 1)
.eq(ReceiptClaimSettlement::getIsDeleted, 0)).stream()
.collect(Collectors.groupingBy(ReceiptClaimSettlement::getFormalSettlementId,
Collectors.reducing(BigDecimal.ZERO,
item -> money(item.getAllocatedReceiptAmount()), BigDecimal::add)))
@@ -2161,7 +2162,8 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
: receiptClaimSettlementMapper.selectList(
Wrappers.<ReceiptClaimSettlement>lambdaQuery()
.eq(ReceiptClaimSettlement::getFormalSettlementId, settlement.getId())
.eq(ReceiptClaimSettlement::getStatus, 1))
.eq(ReceiptClaimSettlement::getStatus, 1)
.eq(ReceiptClaimSettlement::getIsDeleted, 0))
.stream().map(ReceiptClaimSettlement::getAllocatedReceiptAmount).map(this::money)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
@@ -321,11 +321,15 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
BigDecimal writeOffAmount = preSettlementReference
? projectAdvanceWriteOffService.sumBySettlement(TransportBusiConstant.SETTLEMENT_BILL_PRE, referenceId)
: projectAdvanceWriteOffService.sumBySettlement(TransportBusiConstant.SETTLEMENT_BILL_FORMAL, referenceId);
// 正式结算尾款还需扣减来源预结算进度预付(与正式结算剩余应付口径一致)
BigDecimal advanceOccupied = preSettlementReference
? BigDecimal.ZERO
: formalSettlementAdvanceOccupied(referenceId);
BigDecimal occupiedTotal = cumulativeAppliedAmount.add(writeOffAmount).add(advanceOccupied);
PaymentApplicationReferenceAmountVO amount = new PaymentApplicationReferenceAmountVO();
amount.setSettlementAmount(settlementAmount);
amount.setCumulativeAppliedAmount(cumulativeAppliedAmount.add(writeOffAmount));
amount.setPayableAmount(settlementAmount.subtract(cumulativeAppliedAmount).subtract(writeOffAmount)
.max(BigDecimal.ZERO));
amount.setCumulativeAppliedAmount(occupiedTotal);
amount.setPayableAmount(settlementAmount.subtract(occupiedTotal).max(BigDecimal.ZERO));
amount.setPaymentSequence(resolvePaymentSequence(preSettlementReference, referenceId, excludeId));
return amount;
}
@@ -1891,7 +1895,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
}
/**
* 进度预付只认预结算单。尾款付款若同时带回一个并不存在的正式结算单 ID,改为按预结算单保存。
* 进度预付只认预结算单;尾款付款若 settlementId 实际是预结算单 ID 或正式结算单已不存在,改为按预结算单保存。
*/
private void normalizeSettlementReference(PaymentApplicationSaveRequest request) {
if ("progress_advance".equals(request.getPaymentType())) {
@@ -1901,33 +1905,62 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
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));
}
request.setSettlementId(null);
request.setSettlementIds(null);
return;
}
if (!TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT.equals(request.getPaymentType()) || request.getSettlementId() == null) {
if (!TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT.equals(request.getPaymentType())) {
return;
}
boolean hasPreSettlement = request.getPreSettlementId() != null
|| (request.getPreSettlementIds() != null && request.getPreSettlementIds().stream().anyMatch(Objects::nonNull));
if (!hasPreSettlement) {
// settlementIds 中若含无效正式结算单 ID,先尝试纠正为预结算
if (request.getSettlementId() == null && request.getSettlementIds() != null
&& !request.getSettlementIds().isEmpty()) {
request.setSettlementId(request.getSettlementIds().stream().filter(Objects::nonNull).findFirst().orElse(null));
}
if (request.getSettlementId() == null) {
return;
}
FormalSettlement formalSettlement = formalSettlementMapper.selectById(request.getSettlementId());
if (formalSettlement == null || Objects.equals(formalSettlement.getIsDeleted(), 1)) {
if (formalSettlement != null && !Objects.equals(formalSettlement.getIsDeleted(), 1)) {
return;
}
// 正式结算单不存在:若该 ID 实为预结算单,或请求已带预结算引用,则清空正式结算引用
PreSettlement preSettlement = preSettlementMapper.selectById(request.getSettlementId());
boolean isPreSettlement = preSettlement != null && !Objects.equals(preSettlement.getIsDeleted(), 1);
boolean hasPreSettlement = request.getPreSettlementId() != null
|| (request.getPreSettlementIds() != null && request.getPreSettlementIds().stream().anyMatch(Objects::nonNull));
if (isPreSettlement && request.getPreSettlementId() == null) {
request.setPreSettlementId(request.getSettlementId());
if (request.getPreSettlementIds() == null || request.getPreSettlementIds().isEmpty()) {
request.setPreSettlementIds(List.of(request.getSettlementId()));
}
}
if (isPreSettlement || hasPreSettlement) {
request.setSettlementId(null);
request.setSettlementIds(null);
}
}
/** 预结算付款不挂正式结算单时,清掉历史误写入的正式结算单 ID,避免再次提交按正式结算单刷新。 */
/**
* 预结算付款不挂正式结算单时,清掉历史误写入的正式结算单 ID,避免再次提交按正式结算单刷新。
*/
private void clearDetachedFormalReference(PaymentApplication entity) {
if (entity.getId() == null || entity.getPreSettlementId() == null || entity.getSettlementId() != null) {
if (entity.getId() == null) {
return;
}
boolean progressAdvance = "progress_advance".equals(entity.getPaymentType());
boolean preOnly = entity.getPreSettlementId() != null && entity.getSettlementId() == null;
if (!progressAdvance && !preOnly) {
return;
}
baseMapper.update(null, Wrappers.<PaymentApplication>lambdaUpdate()
.eq(PaymentApplication::getId, entity.getId())
.setSql("settlement_id = null, settlement_no = null"));
entity.setSettlementId(null);
entity.setSettlementNo(null);
}
private void fillReference(PaymentApplication entity, PaymentApplicationSaveRequest request) {
@@ -1938,15 +1971,26 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
entity.setProjectId(request.getProjectId()); entity.setProjectName(request.getProjectName()); entity.setDeptId(request.getDeptId()); entity.setDeptName(request.getDeptName()); entity.setContractId(request.getContractId()); entity.setContractNo(request.getContractNo()); entity.setContractName(request.getContractName()); entity.setPayerName(request.getPayerName()); entity.setPayeeName(request.getPayeeName()); entity.setSettlementAmount(request.getSettlementAmount()); entity.setPayableAmount(money(project.getFundDemand()).multiply(BigDecimal.valueOf(10000))); entity.setBillType(Func.isEmpty(project.getSettlementMode()) ? null : "项目预付");
} else if (request.getSettlementId() != null) {
FormalSettlement source = formalSettlementMapper.selectById(request.getSettlementId());
if (source == null) throw new ServiceException("正式结算单不存在");
if (!TransportBusiConstant.APPROVED.equals(source.getApprovalStatus())) throw new ServiceException("只能选择审批通过、未作废的正式结算单");
entity.setSettlementId(source.getId()); entity.setSettlementNo(source.getFormalSettlementNo()); entity.setPreSettlementId(null); entity.setPreSettlementNo(null);
entity.setProjectId(source.getProjectId()); entity.setProjectName(source.getProjectName()); entity.setDeptId(source.getDeptId()); entity.setDeptName(source.getDeptName()); entity.setContractId(source.getContractId()); entity.setContractNo(source.getContractNo()); entity.setContractName(source.getContractName()); entity.setPayerName(source.getPayerName()); entity.setPayeeName(source.getPayeeName()); entity.setSettlementAmount(source.getSettlementAmount()); entity.setPayableAmount(payableOfFormalSettlement(source.getId(), source.getSettlementAmount(), entity.getId())); entity.setBillType("正式结算单");
if (source == null || Objects.equals(source.getIsDeleted(), 1)) {
// 驳回重提等场景:settlementId 可能误存为预结算单 ID,降级走预结算分支
PreSettlement preSettlement = preSettlementMapper.selectById(request.getSettlementId());
if (preSettlement != null && !Objects.equals(preSettlement.getIsDeleted(), 1)) {
request.setPreSettlementId(preSettlement.getId());
request.setSettlementId(null);
fillPreSettlementReference(entity, preSettlement);
} else if (request.getPreSettlementId() != null) {
request.setSettlementId(null);
fillPreSettlementReference(entity, requireApprovedPreSettlement(request.getPreSettlementId()));
} else {
throw new ServiceException("正式结算单不存在");
}
} else {
if (!TransportBusiConstant.APPROVED.equals(source.getApprovalStatus())) throw new ServiceException("只能选择审批通过、未作废的正式结算单");
entity.setSettlementId(source.getId()); entity.setSettlementNo(source.getFormalSettlementNo()); entity.setPreSettlementId(null); entity.setPreSettlementNo(null);
entity.setProjectId(source.getProjectId()); entity.setProjectName(source.getProjectName()); entity.setDeptId(source.getDeptId()); entity.setDeptName(source.getDeptName()); entity.setContractId(source.getContractId()); entity.setContractNo(source.getContractNo()); entity.setContractName(source.getContractName()); entity.setPayerName(source.getPayerName()); entity.setPayeeName(source.getPayeeName()); entity.setSettlementAmount(source.getSettlementAmount()); entity.setPayableAmount(payableOfFormalSettlement(source.getId(), source.getSettlementAmount(), entity.getId())); entity.setBillType("正式结算单");
}
} else if (request.getPreSettlementId() != null) {
PreSettlement source = preSettlementMapper.selectById(request.getPreSettlementId());
if (source == null) throw new ServiceException("预结算单不存在");
if (!TransportBusiConstant.APPROVED.equals(source.getApprovalStatus())) throw new ServiceException("只能选择审批通过、未作废的预结算单");
entity.setPreSettlementId(source.getId()); entity.setPreSettlementNo(source.getPreSettlementNo()); entity.setSettlementId(null); entity.setSettlementNo(null); entity.setProjectId(source.getProjectId()); entity.setProjectName(source.getProjectName()); entity.setDeptId(source.getDeptId()); entity.setDeptName(source.getDeptName()); entity.setContractId(source.getContractId()); entity.setContractNo(source.getContractNo()); entity.setContractName(source.getContractName()); entity.setPayerName(source.getPayerName()); entity.setPayeeName(source.getPayeeName()); entity.setSettlementAmount(source.getSettlementAmount()); entity.setPayableAmount(payableOfPreSettlement(source.getId(), source.getSettlementAmount(), entity.getId())); entity.setBillType("预结算单");
fillPreSettlementReference(entity, requireApprovedPreSettlement(request.getPreSettlementId()));
} else {
entity.setProjectId(request.getProjectId()); entity.setProjectName(request.getProjectName()); entity.setDeptId(request.getDeptId()); entity.setDeptName(request.getDeptName()); entity.setContractId(request.getContractId()); entity.setContractNo(request.getContractNo()); entity.setContractName(request.getContractName()); entity.setPayerName(request.getPayerName()); entity.setPayeeName(request.getPayeeName()); entity.setSettlementAmount(request.getSettlementAmount()); entity.setPayableAmount(request.getPayableAmount()); entity.setBillType(request.getBillType());
}
@@ -1955,6 +1999,36 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
&& money(entity.getPayableAmount()).compareTo(BigDecimal.ZERO) > 0) throw new ServiceException("申请付款金额不能超过可付款金额");
}
private PreSettlement requireApprovedPreSettlement(Long preSettlementId) {
PreSettlement source = preSettlementMapper.selectById(preSettlementId);
if (source == null || Objects.equals(source.getIsDeleted(), 1)) {
throw new ServiceException("预结算单不存在");
}
if (!TransportBusiConstant.APPROVED.equals(source.getApprovalStatus())) {
throw new ServiceException("只能选择审批通过、未作废的预结算单");
}
return source;
}
private void fillPreSettlementReference(PaymentApplication entity, PreSettlement source) {
entity.setPreSettlementId(source.getId());
entity.setPreSettlementNo(source.getPreSettlementNo());
entity.setSettlementId(null);
entity.setSettlementNo(null);
entity.setProjectId(source.getProjectId());
entity.setProjectName(source.getProjectName());
entity.setDeptId(source.getDeptId());
entity.setDeptName(source.getDeptName());
entity.setContractId(source.getContractId());
entity.setContractNo(source.getContractNo());
entity.setContractName(source.getContractName());
entity.setPayerName(source.getPayerName());
entity.setPayeeName(source.getPayeeName());
entity.setSettlementAmount(source.getSettlementAmount());
entity.setPayableAmount(payableOfPreSettlement(source.getId(), source.getSettlementAmount(), entity.getId()));
entity.setBillType("预结算单");
}
private List<FormalSettlement> resolveFormalSettlements(PaymentApplicationSaveRequest request, PaymentApplication entity) {
if (!TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT.equals(request.getPaymentType())) return List.of();
List<Long> ids = request.getSettlementIds() == null ? List.of() : request.getSettlementIds();
@@ -2358,15 +2432,45 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
return relationAmount.add(legacyAmount);
}
/** 正式结算可付款 = 结算金额 − 已申请付款 − 项目预付核销 */
/**
* 正式结算可付款 = 结算金额 − 已申请付款 − 项目预付核销 − 来源预结算进度预付占用。
* 进度预付占用与正式结算台账 remainingPayable 口径对齐,避免转正式后仍可按全额申请尾款。
*/
private BigDecimal payableOfFormalSettlement(Long settlementId, BigDecimal settlementAmount, Long excludeId) {
return money(settlementAmount)
.subtract(cumulativeFormalSettlementApplied(settlementId, excludeId))
.subtract(projectAdvanceWriteOffService.sumBySettlement(
TransportBusiConstant.SETTLEMENT_BILL_FORMAL, settlementId))
.subtract(formalSettlementAdvanceOccupied(settlementId))
.max(BigDecimal.ZERO);
}
/**
* 正式结算来源预结算的进度预付占用合计:优先取来源行快照,快照为空时回退预结算实时已申请/已付。
*/
private BigDecimal formalSettlementAdvanceOccupied(Long settlementId) {
if (settlementId == null) {
return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
}
List<FormalSettlementSource> sources = formalSettlementSourceMapper.selectList(
Wrappers.<FormalSettlementSource>lambdaQuery()
.eq(FormalSettlementSource::getFormalSettlementId, settlementId)
.eq(FormalSettlementSource::getIsDeleted, 0));
if (sources.isEmpty()) {
return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
}
BigDecimal total = BigDecimal.ZERO;
for (FormalSettlementSource source : sources) {
BigDecimal occupied = money(source.getAdvanceAppliedAmount()).max(money(source.getAdvancePaidAmount()));
if (occupied.compareTo(BigDecimal.ZERO) <= 0 && source.getPreSettlementId() != null) {
occupied = findPreSettlementApplied(source.getPreSettlementId())
.max(findPreSettlementPaid(source.getPreSettlementId()));
}
total = total.add(occupied);
}
return total;
}
/** 预结算可付款 = 结算金额 − 已申请付款 − 项目预付核销 */
private BigDecimal payableOfPreSettlement(Long preSettlementId, BigDecimal settlementAmount, Long excludeId) {
return money(settlementAmount)
@@ -252,8 +252,8 @@ public class ReceiptClaimRecordServiceImpl extends BaseServiceImpl<ReceiptClaimM
BigDecimal flowClaimedAfter = money(flow.getClaimedAmount())
.subtract(money(claim.getClaimAmount())).max(BigDecimal.ZERO);
flow.setClaimedAmount(flowClaimedAfter);
// 认领状态对照公告未认领金额重算(作废分摊不计入累计)
flow.setClaimStatus(amountClaimStatus(flowClaimedAfter, flow.getUnclaimedAmount()));
// 认领状态对照可认领上限重算(作废分摊不计入累计)
flow.setClaimStatus(amountClaimStatus(flowClaimedAfter, claimCeiling(flow)));
receiptFlowMapper.updateById(flow);
// 作废不生成金蝶认领冲单:金蝶单号/状态保留原值(未推送为空,已推送为真实收款单信息),
@@ -411,13 +411,28 @@ public class ReceiptClaimRecordServiceImpl extends BaseServiceImpl<ReceiptClaimM
}
/**
* 累计认领金额 → 流水认领状态:对照公告未认领金额判定(与收款流水侧口径一致)
* 累计认领金额 → 流水认领状态:对照可认领上限判定(与收款流水侧口径一致)
*/
private String amountClaimStatus(BigDecimal claimedAmount, BigDecimal unclaimedAmount) {
private String amountClaimStatus(BigDecimal claimedAmount, BigDecimal claimCeiling) {
if (claimedAmount.compareTo(BigDecimal.ZERO) <= 0) {
return "unclaimed";
}
return claimedAmount.compareTo(money(unclaimedAmount)) >= 0 ? "claimed" : "partial";
return claimedAmount.compareTo(money(claimCeiling)) >= 0 ? "claimed" : "partial";
}
/**
* 可认领上限:公告未认领金额与收款金额取较小正值(与收款流水侧口径一致)
*/
private BigDecimal claimCeiling(KingdeeReceiptFlow flow) {
BigDecimal unclaimed = money(flow.getUnclaimedAmount());
BigDecimal receipt = money(flow.getReceiptAmount());
if (receipt.compareTo(BigDecimal.ZERO) <= 0) {
return unclaimed;
}
if (unclaimed.compareTo(BigDecimal.ZERO) <= 0) {
return unclaimed;
}
return unclaimed.min(receipt);
}
private String amountStatus(BigDecimal paidAmount, BigDecimal settlementAmount) {
@@ -194,12 +194,16 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl<KingdeeReceiptFlowMa
.eq(ReceiptFlowRecord::getReceiptFlowId, id)
.in(ReceiptFlowRecord::getActionType, List.of("claim", "push"))
.orderByDesc(ReceiptFlowRecord::getCreateTime)));
// 继续认领:带回本流水历史关联且仍有剩余可认领金额的结算单
vo.setRelatedSettlements(relatedSettlementCandidates(id));
return vo;
}
@Override
public List<Map<String, Object>> settlementCandidates(String keyword, Long flowId) {
existing(flowId);
// 本流水历史关联结算单优先保留(避免仅按创建时间 limit 截断后选不到)
Set<Long> relatedIds = relatedSettlementIds(flowId);
List<FormalSettlement> settlements = formalSettlementMapper.selectList(
Wrappers.<FormalSettlement>lambdaQuery()
.eq(FormalSettlement::getSettlementType, RECEIVABLE)
@@ -210,7 +214,24 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl<KingdeeReceiptFlowMa
.or().like(FormalSettlement::getProjectName, keyword)
.or().like(FormalSettlement::getContractName, keyword))
.orderByDesc(FormalSettlement::getCreateTime)
.last("limit 200"));
.last("limit 500"));
if (!relatedIds.isEmpty()) {
Set<Long> loadedIds = settlements.stream().map(FormalSettlement::getId)
.filter(Objects::nonNull).collect(Collectors.toSet());
List<Long> missingIds = relatedIds.stream().filter(id -> !loadedIds.contains(id)).toList();
if (!missingIds.isEmpty()) {
List<FormalSettlement> relatedSettlements = formalSettlementMapper.selectList(
Wrappers.<FormalSettlement>lambdaQuery()
.in(FormalSettlement::getId, missingIds)
.eq(FormalSettlement::getSettlementType, RECEIVABLE)
.eq(FormalSettlement::getApprovalStatus, APPROVED)
.eq(FormalSettlement::getStatus, 1));
List<FormalSettlement> merged = new ArrayList<>(relatedSettlements.size() + settlements.size());
merged.addAll(relatedSettlements);
merged.addAll(settlements);
settlements = merged;
}
}
// 候选结算单的累计认领金额一次聚合查询(避免逐单 N+1)
Map<Long, BigDecimal> claimedMap = receiptClaimedAmounts(settlements.stream()
.map(FormalSettlement::getId).toList());
@@ -218,6 +239,14 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl<KingdeeReceiptFlowMa
.map(settlement -> candidateRow(settlement,
claimedMap.getOrDefault(settlement.getId(), BigDecimal.ZERO)))
.filter(row -> ((BigDecimal) row.get("remainingReceiptAmount")).compareTo(BigDecimal.ZERO) > 0)
.sorted((left, right) -> {
boolean leftRelated = relatedIds.contains(toLong(left.get("id")));
boolean rightRelated = relatedIds.contains(toLong(right.get("id")));
if (leftRelated == rightRelated) {
return 0;
}
return leftRelated ? -1 : 1;
})
.toList();
}
@@ -234,6 +263,7 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl<KingdeeReceiptFlowMa
Wrappers.<ReceiptClaimSettlement>lambdaQuery()
.eq(ReceiptClaimSettlement::getFormalSettlementId, formalSettlementId)
.eq(ReceiptClaimSettlement::getStatus, 1)
.eq(ReceiptClaimSettlement::getIsDeleted, 0)
.orderByDesc(ReceiptClaimSettlement::getCreateTime));
if (relations.isEmpty()) {
return List.of();
@@ -315,19 +345,19 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl<KingdeeReceiptFlowMa
allocatedTotal = allocatedTotal.add(allocated);
}
// 公告未认领金额(认领上限与认领完成判定基准,A301-2 口径)
// 认领上限:公告未认领金额与收款金额取较小值(避免未认领金额异常偏大导致无法认领完成)
if (flow.getUnclaimedAmount() == null) {
throw new ServiceException("公告未认领金额缺失,请先手动同步金蝶收款流水");
}
BigDecimal unclaimedAmount = money(flow.getUnclaimedAmount());
if (unclaimedAmount.compareTo(BigDecimal.ZERO) <= 0) {
throw new ServiceException("该流水公告未认领金额为0,无需认领");
BigDecimal claimCeiling = claimCeiling(flow);
if (claimCeiling.compareTo(BigDecimal.ZERO) <= 0) {
throw new ServiceException("该流水可认领金额为0,无需认领");
}
// 流水认领前已认领金额
BigDecimal previousFlowClaimed = flowClaimedAmount(flow.getId());
if (previousFlowClaimed.add(allocatedTotal).compareTo(unclaimedAmount) > 0) {
throw new ServiceException("累计认领金额不能超过公告未认领金额,剩余可认领 "
+ unclaimedAmount.subtract(previousFlowClaimed).max(BigDecimal.ZERO));
if (previousFlowClaimed.add(allocatedTotal).compareTo(claimCeiling) > 0) {
throw new ServiceException("累计认领金额不能超过可认领上限,剩余可认领 "
+ claimCeiling.subtract(previousFlowClaimed).max(BigDecimal.ZERO));
}
// 当前用户部门
@@ -380,6 +410,9 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl<KingdeeReceiptFlowMa
relation.setClaimedReceiptAmount(previousClaimed);
// 本次分摊金额
relation.setAllocatedReceiptAmount(allocated);
// 有效分摊(与汇总/候选查询 status=1 口径一致)
relation.setStatus(1);
relation.setIsDeleted(0);
claimSettlementMapper.insert(relation);
}
@@ -390,8 +423,8 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl<KingdeeReceiptFlowMa
String fromStatus = normalizeClaimStatus(flow.getClaimStatus());
// 认领后累计金额
BigDecimal claimedAfter = previousFlowClaimed.add(allocatedTotal);
// 认领后状态(对照公告未认领金额)
String toStatus = amountClaimStatus(claimedAfter, unclaimedAmount);
// 认领后状态(对照可认领上限)
String toStatus = amountClaimStatus(claimedAfter, claimCeiling);
// 更新流水已认领金额
flow.setClaimedAmount(claimedAfter);
// 更新流水认领状态
@@ -499,9 +532,9 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl<KingdeeReceiptFlowMa
// 复制公告数据到实体(不覆盖本地认领字段)
copyAnnounceRow(entity, announce, detailSerialNo, receiptAmount);
// 根据本地累计认领金额与最新公告未认领金额重新计算认领状态
// 根据本地累计认领金额与可认领上限重新计算认领状态
entity.setClaimStatus(amountClaimStatus(money(entity.getClaimedAmount()),
entity.getUnclaimedAmount()));
claimCeiling(entity)));
if (created) {
toInsert.add(entity);
@@ -621,6 +654,46 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl<KingdeeReceiptFlowMa
}
}
/**
* 本流水历史关联过的结算单 id(有效分摊)
*/
private Set<Long> relatedSettlementIds(Long flowId) {
if (flowId == null) {
return Set.of();
}
return claimSettlementMapper.selectList(Wrappers.<ReceiptClaimSettlement>lambdaQuery()
.select(ReceiptClaimSettlement::getFormalSettlementId)
.eq(ReceiptClaimSettlement::getReceiptFlowId, flowId)
.eq(ReceiptClaimSettlement::getStatus, 1)
.eq(ReceiptClaimSettlement::getIsDeleted, 0)).stream()
.map(ReceiptClaimSettlement::getFormalSettlementId)
.filter(Objects::nonNull)
.collect(Collectors.toCollection(HashSet::new));
}
/**
* 继续认领预填:本流水历史关联且仍有剩余可认领金额的结算单
*/
private List<Map<String, Object>> relatedSettlementCandidates(Long flowId) {
Set<Long> relatedIds = relatedSettlementIds(flowId);
if (relatedIds.isEmpty()) {
return List.of();
}
List<FormalSettlement> settlements = formalSettlementMapper.selectList(
Wrappers.<FormalSettlement>lambdaQuery()
.in(FormalSettlement::getId, relatedIds)
.eq(FormalSettlement::getSettlementType, RECEIVABLE)
.eq(FormalSettlement::getApprovalStatus, APPROVED)
.eq(FormalSettlement::getStatus, 1));
Map<Long, BigDecimal> claimedMap = receiptClaimedAmounts(settlements.stream()
.map(FormalSettlement::getId).toList());
return settlements.stream()
.map(settlement -> candidateRow(settlement,
claimedMap.getOrDefault(settlement.getId(), BigDecimal.ZERO)))
.filter(row -> ((BigDecimal) row.get("remainingReceiptAmount")).compareTo(BigDecimal.ZERO) > 0)
.toList();
}
/**
* 批量聚合结算单的累计有效认领金额:一次 IN 查询按结算单分组求和,避免逐单 N+1
*/
@@ -631,7 +704,8 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl<KingdeeReceiptFlowMa
}
return claimSettlementMapper.selectList(Wrappers.<ReceiptClaimSettlement>lambdaQuery()
.in(ReceiptClaimSettlement::getFormalSettlementId, ids)
.eq(ReceiptClaimSettlement::getStatus, 1)).stream()
.eq(ReceiptClaimSettlement::getStatus, 1)
.eq(ReceiptClaimSettlement::getIsDeleted, 0)).stream()
.collect(Collectors.groupingBy(ReceiptClaimSettlement::getFormalSettlementId,
Collectors.reducing(BigDecimal.ZERO,
item -> money(item.getAllocatedReceiptAmount()), BigDecimal::add)));
@@ -640,12 +714,42 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl<KingdeeReceiptFlowMa
private BigDecimal flowClaimedAmount(Long flowId) {
return claimSettlementMapper.selectList(Wrappers.<ReceiptClaimSettlement>lambdaQuery()
.eq(ReceiptClaimSettlement::getReceiptFlowId, flowId)
.eq(ReceiptClaimSettlement::getStatus, 1)).stream()
.eq(ReceiptClaimSettlement::getStatus, 1)
.eq(ReceiptClaimSettlement::getIsDeleted, 0)).stream()
.map(ReceiptClaimSettlement::getAllocatedReceiptAmount)
.map(this::money)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
/**
* 可认领上限:公告未认领金额与收款金额取较小正值。
* 主口径仍为 A301-2 公告未认领金额;与收款金额取 min,避免未认领金额异常偏大导致无法进入「认领完成」。
*/
private BigDecimal claimCeiling(KingdeeReceiptFlow flow) {
BigDecimal unclaimed = money(flow.getUnclaimedAmount());
BigDecimal receipt = money(flow.getReceiptAmount());
if (receipt.compareTo(BigDecimal.ZERO) <= 0) {
return unclaimed;
}
if (unclaimed.compareTo(BigDecimal.ZERO) <= 0) {
return unclaimed;
}
return unclaimed.min(receipt);
}
private Long toLong(Object value) {
if (value == null) {
return null;
}
if (value instanceof Long longValue) {
return longValue;
}
if (value instanceof Number number) {
return number.longValue();
}
return Func.toLong(String.valueOf(value));
}
private KingdeeReceiptFlow lockedFlow(Long flowId) {
KingdeeReceiptFlow flow = baseMapper.selectOne(Wrappers.<KingdeeReceiptFlow>lambdaQuery()
.eq(KingdeeReceiptFlow::getId, flowId)
@@ -824,18 +928,20 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl<KingdeeReceiptFlowMa
flow.setClaimMe(announce.getClaimme());
BigDecimal claimedTotal = flowClaimedAmount(flow.getId());
flow.setClaimedAmount(claimedTotal);
String claimStatus = amountClaimStatus(claimedTotal, flow.getUnclaimedAmount());
BigDecimal claimCeiling = claimCeiling(flow);
String claimStatus = amountClaimStatus(claimedTotal, claimCeiling);
flow.setClaimStatus(claimStatus);
updateById(flow);
if (!CLAIMED.equals(claimStatus)) {
return ReceiptClaimPushResult.fail("累计认领金额(" + claimedTotal + ")与公告未认领金额("
+ money(flow.getUnclaimedAmount()) + ")不一致,不满足推送条件");
return ReceiptClaimPushResult.fail("累计认领金额(" + claimedTotal + ")与可认领上限("
+ claimCeiling + ")不一致,不满足推送条件");
}
// 分录取该流水全部有效分摊行(结算单id升序,跨认领主单归集)
List<ReceiptClaimSettlement> allocations = claimSettlementMapper.selectList(
Wrappers.<ReceiptClaimSettlement>lambdaQuery()
.eq(ReceiptClaimSettlement::getReceiptFlowId, flow.getId())
.eq(ReceiptClaimSettlement::getStatus, 1)
.eq(ReceiptClaimSettlement::getIsDeleted, 0)
.orderByAsc(ReceiptClaimSettlement::getFormalSettlementId));
if (allocations.isEmpty()) {
return ReceiptClaimPushResult.fail("该流水无有效认领分摊,无法推送");
@@ -1075,13 +1181,13 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl<KingdeeReceiptFlowMa
}
/**
* 累计认领金额 → 流水认领状态:对照公告未认领金额判定(claimed 即「认领完成」,唯一可推送状态)
* 累计认领金额 → 流水认领状态:对照可认领上限判定(claimed 即「认领完成」,唯一可推送状态)
*/
private String amountClaimStatus(BigDecimal claimedAmount, BigDecimal unclaimedAmount) {
private String amountClaimStatus(BigDecimal claimedAmount, BigDecimal claimCeiling) {
if (claimedAmount.compareTo(BigDecimal.ZERO) <= 0) {
return UNCLAIMED;
}
return claimedAmount.compareTo(money(unclaimedAmount)) >= 0 ? CLAIMED : PARTIAL;
return claimedAmount.compareTo(money(claimCeiling)) >= 0 ? CLAIMED : PARTIAL;
}
private String amountStatus(BigDecimal claimedAmount, BigDecimal settlementAmount) {
@@ -246,10 +246,8 @@ public class TemporaryCreditLimitServiceImpl extends BaseServiceImpl<TemporaryCr
TransportBusinessSupport.validateRequired(temporaryCreditLimit.getProjectCode(), "项目编号不能为空");
TransportBusinessSupport.validateRequired(temporaryCreditLimit.getUndertakeDeptName(), "承办部门不能为空");
TransportBusinessSupport.validateRequired(temporaryCreditLimit.getValidUntil() == null ? null : temporaryCreditLimit.getValidUntil().toString(), "请选择申请有效期至");
if (temporaryCreditLimit.getProjectFundLimit() == null || temporaryCreditLimit.getUsedFundLimit() == null || temporaryCreditLimit.getRemainingFundLimit() == null) {
throw new ServiceException("项目额度数据异常,请重新选择项目");
}
if (temporaryCreditLimit.getRemainingFundLimit().compareTo(BigDecimal.ZERO) < 0) {
if (temporaryCreditLimit.getRemainingFundLimit() != null
&& temporaryCreditLimit.getRemainingFundLimit().compareTo(BigDecimal.ZERO) < 0) {
throw new ServiceException("项目剩余额度不能小于0");
}
}
@@ -70,10 +70,15 @@ public class ReceiptFlowWrapper extends BaseEntityWrapper<KingdeeReceiptFlow, Re
case "failed" -> "处理失败";
default -> "未推送";
});
// 剩余可认领金额 = 公告未认领金额 − 累计认领金额(认领上限为公告口径)
// 剩余可认领金额 = 可认领上限 − 累计认领金额(上限取公告未认领与收款金额较小值)
BigDecimal unclaimedAmount = money(entity.getUnclaimedAmount());
BigDecimal receiptAmount = money(entity.getReceiptAmount());
BigDecimal claimCeiling = unclaimedAmount;
if (receiptAmount.compareTo(BigDecimal.ZERO) > 0 && unclaimedAmount.compareTo(BigDecimal.ZERO) > 0) {
claimCeiling = unclaimedAmount.min(receiptAmount);
}
BigDecimal claimedAmount = money(entity.getClaimedAmount());
vo.setRemainingAmount(unclaimedAmount.subtract(claimedAmount).max(BigDecimal.ZERO));
vo.setRemainingAmount(claimCeiling.subtract(claimedAmount).max(BigDecimal.ZERO));
vo.setCreateUserName(UserCache.getUserRealName(entity.getCreateUser()));
vo.setUpdateUserName(UserCache.getUserRealName(entity.getUpdateUser()));
return vo;
@@ -0,0 +1,18 @@
-- 用户表:新增 OA 登录名,支持用户管理后台维护
-- 排序规则 utf8mb4_general_ci
ALTER TABLE `blade_user`
ADD COLUMN `oa_login_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'OA登录名' AFTER `is_oa`;
-- 从用户部门关联表回填已有 OA 登录账号
UPDATE `blade_user` bu
INNER JOIN (
SELECT `user_id`, MAX(`oa_login_id`) AS `oa_login_id`
FROM `blade_user_dept`
WHERE `is_deleted` = 0
AND `oa_login_id` IS NOT NULL
AND `oa_login_id` <> ''
GROUP BY `user_id`
) bud ON bud.`user_id` = bu.`id`
SET bu.`oa_login_id` = bud.`oa_login_id`
WHERE bu.`oa_login_id` IS NULL OR bu.`oa_login_id` = '';