|
@@ -48,10 +48,13 @@ import java.math.BigDecimal;
|
|
|
import java.time.OffsetDateTime;
|
|
import java.time.OffsetDateTime;
|
|
|
import java.time.ZoneId;
|
|
import java.time.ZoneId;
|
|
|
import java.time.format.DateTimeFormatter;
|
|
import java.time.format.DateTimeFormatter;
|
|
|
|
|
+import java.time.format.DateTimeParseException;
|
|
|
import java.util.ArrayList;
|
|
import java.util.ArrayList;
|
|
|
|
|
+import java.util.HashSet;
|
|
|
import java.util.LinkedHashMap;
|
|
import java.util.LinkedHashMap;
|
|
|
import java.util.List;
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
import java.util.Map;
|
|
|
|
|
+import java.util.Set;
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 批量付款到户有密 — 制单授权
|
|
* 批量付款到户有密 — 制单授权
|
|
@@ -174,15 +177,25 @@ public class AlipayBatchPayService {
|
|
|
public Map<String, Object> batchCreate(BatchCreateDTO dto) {
|
|
public Map<String, Object> batchCreate(BatchCreateDTO dto) {
|
|
|
if (dto.getOutBatchNo() == null || dto.getOutBatchNo().isBlank())
|
|
if (dto.getOutBatchNo() == null || dto.getOutBatchNo().isBlank())
|
|
|
dto.setOutBatchNo(SnowflakeIdGenerator.nextIdStr());
|
|
dto.setOutBatchNo(SnowflakeIdGenerator.nextIdStr());
|
|
|
- // 幂等保护: 同单号已存在则拒绝,避免重复支付
|
|
|
|
|
|
|
+ // 幂等统一: 本地命中已有单号时与 UNIQUE_VIOLATION 兜底一致 — 查库返回已受理批次信息,不抛 400
|
|
|
if (batchOrderMapper.selectCount(
|
|
if (batchOrderMapper.selectCount(
|
|
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<BatchOrderEntity>()
|
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<BatchOrderEntity>()
|
|
|
- .eq(BatchOrderEntity::getOutBatchNo, dto.getOutBatchNo())) > 0)
|
|
|
|
|
|
|
+ .eq(BatchOrderEntity::getOutBatchNo, dto.getOutBatchNo())) > 0) {
|
|
|
|
|
+ BatchOrderEntity existing = batchOrderMapper.selectOne(
|
|
|
|
|
+ new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<BatchOrderEntity>()
|
|
|
|
|
+ .eq(BatchOrderEntity::getOutBatchNo, dto.getOutBatchNo()));
|
|
|
|
|
+ if (existing != null)
|
|
|
|
|
+ return acceptedBatchResult(existing);
|
|
|
throw new BusinessException(400, "批次外部单号已存在,请勿重复创建");
|
|
throw new BusinessException(400, "批次外部单号已存在,请勿重复创建");
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
BigDecimal total = BigDecimal.ZERO;
|
|
BigDecimal total = BigDecimal.ZERO;
|
|
|
List<TransOrderDetail> transList = new ArrayList<>();
|
|
List<TransOrderDetail> transList = new ArrayList<>();
|
|
|
|
|
+ Set<String> seenOutBizNos = new HashSet<>();
|
|
|
for (BatchCreateDTO.BatchDetailDTO d : dto.getDetails()) {
|
|
for (BatchCreateDTO.BatchDetailDTO d : dto.getDetails()) {
|
|
|
|
|
+ // 每笔 out_biz_no 批内唯一
|
|
|
|
|
+ if (!seenOutBizNos.add(d.getOutBizNo()))
|
|
|
|
|
+ throw new BusinessException(400, "批次内明细外部单号重复:" + d.getOutBizNo());
|
|
|
if (d.getAmount().compareTo(new BigDecimal("1")) < 0)
|
|
if (d.getAmount().compareTo(new BigDecimal("1")) < 0)
|
|
|
throw new BusinessException(400, "明细金额最低 1 元: " + d.getOutBizNo());
|
|
throw new BusinessException(400, "明细金额最低 1 元: " + d.getOutBizNo());
|
|
|
total = total.add(d.getAmount());
|
|
total = total.add(d.getAmount());
|
|
@@ -244,9 +257,7 @@ public class AlipayBatchPayService {
|
|
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<BatchOrderEntity>()
|
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<BatchOrderEntity>()
|
|
|
.eq(BatchOrderEntity::getOutBatchNo, dto.getOutBatchNo()));
|
|
.eq(BatchOrderEntity::getOutBatchNo, dto.getOutBatchNo()));
|
|
|
if (existing != null)
|
|
if (existing != null)
|
|
|
- return Map.of("out_batch_no", existing.getOutBatchNo(),
|
|
|
|
|
- "batch_trans_id", existing.getBatchTransId() != null ? existing.getBatchTransId() : "",
|
|
|
|
|
- "status", existing.getStatus());
|
|
|
|
|
|
|
+ return acceptedBatchResult(existing);
|
|
|
}
|
|
}
|
|
|
throw new BusinessException(400, "创建批次失败: " + response.getMsg() + " (" + response.getSubCode() + ")");
|
|
throw new BusinessException(400, "创建批次失败: " + response.getMsg() + " (" + response.getSubCode() + ")");
|
|
|
}
|
|
}
|
|
@@ -291,6 +302,13 @@ public class AlipayBatchPayService {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /** 幂等返回体: 已受理批次信息(本地 selectCount 命中与 UNIQUE_VIOLATION 兜底共用) */
|
|
|
|
|
+ private Map<String, Object> acceptedBatchResult(BatchOrderEntity existing) {
|
|
|
|
|
+ return Map.of("out_batch_no", existing.getOutBatchNo(),
|
|
|
|
|
+ "batch_trans_id", existing.getBatchTransId() != null ? existing.getBatchTransId() : "",
|
|
|
|
|
+ "status", existing.getStatus());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/** alipay.fund.trans.render.pay — 生成 PC 支付页链接 */
|
|
/** alipay.fund.trans.render.pay — 生成 PC 支付页链接 */
|
|
|
public Map<String, String> renderPay(String enterpriseId, String outBatchNo) {
|
|
public Map<String, String> renderPay(String enterpriseId, String outBatchNo) {
|
|
|
BatchOrderEntity order = batchOrderMapper.selectOne(
|
|
BatchOrderEntity order = batchOrderMapper.selectOne(
|
|
@@ -405,23 +423,31 @@ public class AlipayBatchPayService {
|
|
|
.eq(status != null && !status.isBlank(), BatchOrderEntity::getStatus, status)
|
|
.eq(status != null && !status.isBlank(), BatchOrderEntity::getStatus, status)
|
|
|
.orderByDesc(BatchOrderEntity::getId);
|
|
.orderByDesc(BatchOrderEntity::getId);
|
|
|
if (startTime != null && !startTime.isBlank()) {
|
|
if (startTime != null && !startTime.isBlank()) {
|
|
|
- // "yyyy-MM-dd HH:mm" → "yyyy-MM-ddTHH:mm:00+08:00"(ISO 默认模式,无需自定义格式器)
|
|
|
|
|
- w.ge(BatchOrderEntity::getCreatedTime,
|
|
|
|
|
- OffsetDateTime.parse(startTime.replace(' ', 'T') + ":00+08:00"));
|
|
|
|
|
|
|
+ w.ge(BatchOrderEntity::getCreatedTime, parseTimeFilter(startTime, ":00+08:00"));
|
|
|
}
|
|
}
|
|
|
if (endTime != null && !endTime.isBlank()) {
|
|
if (endTime != null && !endTime.isBlank()) {
|
|
|
- w.le(BatchOrderEntity::getCreatedTime,
|
|
|
|
|
- OffsetDateTime.parse(endTime.replace(' ', 'T') + ":59+08:00"));
|
|
|
|
|
|
|
+ w.le(BatchOrderEntity::getCreatedTime, parseTimeFilter(endTime, ":59+08:00"));
|
|
|
}
|
|
}
|
|
|
var r = batchOrderMapper.selectPage(new Page<>(pageNo, pageSize), w);
|
|
var r = batchOrderMapper.selectPage(new Page<>(pageNo, pageSize), w);
|
|
|
return PageResult.of(pageNo, pageSize, r.getTotal(), r.getRecords());
|
|
return PageResult.of(pageNo, pageSize, r.getTotal(), r.getRecords());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** 批次详情 + 明细分页 */
|
|
|
|
|
- public Map<String, Object> batchDetail(String outBatchNo, int pageNo, int pageSize) {
|
|
|
|
|
|
|
+ /** 时间参数解析: "yyyy-MM-dd HH:mm" → OffsetDateTime;非法格式抛 400(而非 DateTimeParseException→500) */
|
|
|
|
|
+ private static OffsetDateTime parseTimeFilter(String time, String suffix) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // "yyyy-MM-dd HH:mm" → "yyyy-MM-ddTHH:mm:00+08:00"(ISO 默认模式,无需自定义格式器)
|
|
|
|
|
+ return OffsetDateTime.parse(time.replace(' ', 'T') + suffix);
|
|
|
|
|
+ } catch (DateTimeParseException e) {
|
|
|
|
|
+ throw new BusinessException(400, "时间参数格式错误:应形如 yyyy-MM-dd HH:mm");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 批次详情 + 明细分页(enterprise_id 租户隔离,参照 batchList 过滤写法) */
|
|
|
|
|
+ public Map<String, Object> batchDetail(String enterpriseId, String outBatchNo, int pageNo, int pageSize) {
|
|
|
BatchOrderEntity order = batchOrderMapper.selectOne(
|
|
BatchOrderEntity order = batchOrderMapper.selectOne(
|
|
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<BatchOrderEntity>()
|
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<BatchOrderEntity>()
|
|
|
- .eq(BatchOrderEntity::getOutBatchNo, outBatchNo));
|
|
|
|
|
|
|
+ .eq(BatchOrderEntity::getOutBatchNo, outBatchNo)
|
|
|
|
|
+ .eq(enterpriseId != null && !enterpriseId.isBlank(), BatchOrderEntity::getEnterpriseId, enterpriseId));
|
|
|
if (order == null) throw new BusinessException(404, "批次不存在");
|
|
if (order == null) throw new BusinessException(404, "批次不存在");
|
|
|
Page<BatchDetailEntity> detailPage = batchDetailMapper.selectPage(new Page<>(pageNo, pageSize),
|
|
Page<BatchDetailEntity> detailPage = batchDetailMapper.selectPage(new Page<>(pageNo, pageSize),
|
|
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<BatchDetailEntity>()
|
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<BatchDetailEntity>()
|
|
@@ -487,6 +513,9 @@ public class AlipayBatchPayService {
|
|
|
log.info("导出批次报表: {} 条", listData.size());
|
|
log.info("导出批次报表: {} 条", listData.size());
|
|
|
return ExcelUtil.exportToExcel(listData, mappingDict);
|
|
return ExcelUtil.exportToExcel(listData, mappingDict);
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
|
|
|
+ // 非法时间输入应抛 400,而非被兜底包装成 500
|
|
|
|
|
+ if (e instanceof DateTimeParseException)
|
|
|
|
|
+ throw new BusinessException(400, "时间参数格式错误:应形如 yyyy-MM-dd HH:mm");
|
|
|
log.error("导出批次报表失败", e);
|
|
log.error("导出批次报表失败", e);
|
|
|
throw new RuntimeException("导出批次报表失败: " + e.getMessage());
|
|
throw new RuntimeException("导出批次报表失败: " + e.getMessage());
|
|
|
}
|
|
}
|