mirror of
https://codeup.aliyun.com/67c68d4e484ca2f0a13ac3c1/ydc/jsowell-charger-web.git
synced 2026-06-24 00:59:43 +08:00
update
This commit is contained in:
@@ -0,0 +1,42 @@
|
|||||||
|
package com.jsowell.web.controller.pile;
|
||||||
|
|
||||||
|
import com.jsowell.common.core.controller.BaseController;
|
||||||
|
import com.jsowell.common.core.domain.AjaxResult;
|
||||||
|
import com.jsowell.pile.service.OrderBasicInfoService;
|
||||||
|
import com.jsowell.pile.vo.UnsplitOrderFieldsVO;
|
||||||
|
import org.apache.commons.collections4.CollectionUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
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.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内部查询接口(仅供 Task/脚本 调用,不对外暴露鉴权)
|
||||||
|
* 用于 PRE 环境向本地数据库查询订单补齐字段
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/internal/order")
|
||||||
|
public class InternalOrderController extends BaseController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private OrderBasicInfoService orderBasicInfoService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量查询未分账订单补齐所需字段
|
||||||
|
* POST /internal/order/unsplit-fields
|
||||||
|
* Body: ["C202401010001", "C202401010002", ...]
|
||||||
|
* 返回:order_code, pile_sn, settle_amount, refund_amount
|
||||||
|
*/
|
||||||
|
@PostMapping("/unsplit-fields")
|
||||||
|
public AjaxResult getUnsplitOrderFields(@RequestBody Set<String> orderCodes) {
|
||||||
|
if (CollectionUtils.isEmpty(orderCodes)) {
|
||||||
|
return AjaxResult.error("orderCodes 不能为空");
|
||||||
|
}
|
||||||
|
List<UnsplitOrderFieldsVO> list = orderBasicInfoService.selectUnsplitOrderFields(orderCodes);
|
||||||
|
return AjaxResult.success(list);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -299,3 +299,7 @@ dubbo:
|
|||||||
port: -1
|
port: -1
|
||||||
consumer:
|
consumer:
|
||||||
check: false # 关键配置:启动时不检查提供者
|
check: false # 关键配置:启动时不检查提供者
|
||||||
|
|
||||||
|
# PRE 环境自身地址(供 Task 调用内部接口补齐订单字段)
|
||||||
|
pre:
|
||||||
|
base-url: http://localhost:8080
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.jsowell.pile.dto.*;
|
|||||||
import com.jsowell.pile.dto.nanrui.NRQueryOrderDTO;
|
import com.jsowell.pile.dto.nanrui.NRQueryOrderDTO;
|
||||||
import com.jsowell.pile.dto.ningxiajiaotou.NXJTQueryOrdersInfoDTO;
|
import com.jsowell.pile.dto.ningxiajiaotou.NXJTQueryOrdersInfoDTO;
|
||||||
import com.jsowell.pile.vo.SupStationStatsVO;
|
import com.jsowell.pile.vo.SupStationStatsVO;
|
||||||
|
import com.jsowell.pile.vo.UnsplitOrderFieldsVO;
|
||||||
import com.jsowell.pile.vo.base.MerchantOrderInfoVO;
|
import com.jsowell.pile.vo.base.MerchantOrderInfoVO;
|
||||||
import com.jsowell.pile.vo.lianlian.AccumulativeInfoVO;
|
import com.jsowell.pile.vo.lianlian.AccumulativeInfoVO;
|
||||||
import com.jsowell.pile.vo.nanrui.JiangSuOrderInfoVO;
|
import com.jsowell.pile.vo.nanrui.JiangSuOrderInfoVO;
|
||||||
@@ -452,6 +453,8 @@ public interface OrderBasicInfoMapper {
|
|||||||
|
|
||||||
List<OrderBasicInfo> selectOrderTemp(@Param("orderCodes") Set<String> orderCodes);
|
List<OrderBasicInfo> selectOrderTemp(@Param("orderCodes") Set<String> orderCodes);
|
||||||
|
|
||||||
|
List<UnsplitOrderFieldsVO> selectUnsplitOrderFields(@Param("orderCodes") Set<String> orderCodes);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取保险收入
|
* 获取保险收入
|
||||||
* @param dto
|
* @param dto
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import com.jsowell.pile.dto.nanrui.NRQueryOrderDTO;
|
|||||||
import com.jsowell.pile.dto.ningxiajiaotou.NXJTQueryOrdersInfoDTO;
|
import com.jsowell.pile.dto.ningxiajiaotou.NXJTQueryOrdersInfoDTO;
|
||||||
import com.jsowell.pile.vo.OrderInfoDetailVO;
|
import com.jsowell.pile.vo.OrderInfoDetailVO;
|
||||||
import com.jsowell.pile.vo.SupStationStatsVO;
|
import com.jsowell.pile.vo.SupStationStatsVO;
|
||||||
|
import com.jsowell.pile.vo.UnsplitOrderFieldsVO;
|
||||||
import com.jsowell.pile.vo.base.MerchantOrderInfoVO;
|
import com.jsowell.pile.vo.base.MerchantOrderInfoVO;
|
||||||
import com.jsowell.pile.vo.base.OrderAmountDetailVO;
|
import com.jsowell.pile.vo.base.OrderAmountDetailVO;
|
||||||
import com.jsowell.pile.vo.base.OrderPeriodAmountVO;
|
import com.jsowell.pile.vo.base.OrderPeriodAmountVO;
|
||||||
@@ -662,6 +663,9 @@ public interface OrderBasicInfoService{
|
|||||||
// 临时接口, 查询订单信息
|
// 临时接口, 查询订单信息
|
||||||
List<OrderBasicInfo> selectOrderTemp(Set<String> orderCodes);
|
List<OrderBasicInfo> selectOrderTemp(Set<String> orderCodes);
|
||||||
|
|
||||||
|
// 查询未分账订单补齐所需字段(轻量接口,仅返回 pileSn/settleAmount/refundAmount)
|
||||||
|
List<UnsplitOrderFieldsVO> selectUnsplitOrderFields(Set<String> orderCodes);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取保险金额
|
* 获取保险金额
|
||||||
* @param dto
|
* @param dto
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ import com.jsowell.pile.util.UserUtils;
|
|||||||
import com.jsowell.pile.vo.OrderInfoDetailVO;
|
import com.jsowell.pile.vo.OrderInfoDetailVO;
|
||||||
import com.jsowell.pile.vo.OrderPayRecordVO;
|
import com.jsowell.pile.vo.OrderPayRecordVO;
|
||||||
import com.jsowell.pile.vo.SupStationStatsVO;
|
import com.jsowell.pile.vo.SupStationStatsVO;
|
||||||
|
import com.jsowell.pile.vo.UnsplitOrderFieldsVO;
|
||||||
import com.jsowell.pile.vo.base.*;
|
import com.jsowell.pile.vo.base.*;
|
||||||
import com.jsowell.pile.vo.base.PileInfoVO;
|
import com.jsowell.pile.vo.base.PileInfoVO;
|
||||||
import com.jsowell.pile.vo.lianlian.AccumulativeInfoVO;
|
import com.jsowell.pile.vo.lianlian.AccumulativeInfoVO;
|
||||||
@@ -6463,6 +6464,11 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService {
|
|||||||
return orderBasicInfoMapper.selectOrderTemp(orderCodes);
|
return orderBasicInfoMapper.selectOrderTemp(orderCodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<UnsplitOrderFieldsVO> selectUnsplitOrderFields(Set<String> orderCodes) {
|
||||||
|
return orderBasicInfoMapper.selectUnsplitOrderFields(orderCodes);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询每天的保险金额
|
* 查询每天的保险金额
|
||||||
* @param dto
|
* @param dto
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.jsowell.pile.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 未分账订单补齐字段 VO(仅包含 Task 补齐所需的最小字段集)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class UnsplitOrderFieldsVO {
|
||||||
|
|
||||||
|
/** 订单编号 */
|
||||||
|
private String orderCode;
|
||||||
|
|
||||||
|
/** 充电桩SN(用于判断桩类型 EV/eBike) */
|
||||||
|
private String pileSn;
|
||||||
|
|
||||||
|
/** 结算金额(应分账金额) */
|
||||||
|
private BigDecimal settleAmount;
|
||||||
|
|
||||||
|
/** 退款金额(已退款或应退款金额) */
|
||||||
|
private BigDecimal refundAmount;
|
||||||
|
}
|
||||||
@@ -3599,6 +3599,15 @@
|
|||||||
</foreach>
|
</foreach>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectUnsplitOrderFields" resultType="com.jsowell.pile.vo.UnsplitOrderFieldsVO">
|
||||||
|
select order_code, pile_sn, settle_amount, refund_amount
|
||||||
|
from order_basic_info
|
||||||
|
where order_code in
|
||||||
|
<foreach item="item" collection="orderCodes" separator="," open="(" close=")">
|
||||||
|
#{item}
|
||||||
|
</foreach>
|
||||||
|
</select>
|
||||||
|
|
||||||
<select id="getInsuranceAmount" resultType="com.jsowell.pile.vo.web.IndexPlatformProfitVO">
|
<select id="getInsuranceAmount" resultType="com.jsowell.pile.vo.web.IndexPlatformProfitVO">
|
||||||
select
|
select
|
||||||
DATE_FORMAT(settlement_time, '%Y-%m-%d') as tradeDate,
|
DATE_FORMAT(settlement_time, '%Y-%m-%d') as tradeDate,
|
||||||
|
|||||||
@@ -0,0 +1,567 @@
|
|||||||
|
package com.jsowell.quartz.task;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
|
import com.jsowell.adapay.common.DivMember;
|
||||||
|
import com.jsowell.adapay.dto.PaymentConfirmParam;
|
||||||
|
import com.jsowell.adapay.response.PaymentConfirmResponse;
|
||||||
|
import com.jsowell.adapay.response.RefundResponse;
|
||||||
|
import com.jsowell.adapay.service.AdapayService;
|
||||||
|
import com.jsowell.common.YouDianUtils;
|
||||||
|
import com.jsowell.common.constant.CacheConstants;
|
||||||
|
import com.jsowell.common.constant.Constants;
|
||||||
|
import com.jsowell.common.core.redis.RedisCache;
|
||||||
|
import com.jsowell.common.util.DateUtils;
|
||||||
|
import com.jsowell.common.util.StringUtils;
|
||||||
|
import com.jsowell.common.util.http.HttpUtils;
|
||||||
|
import com.jsowell.pile.domain.AdapayUnsplitRecord;
|
||||||
|
import com.jsowell.pile.service.AdapayUnsplitRecordService;
|
||||||
|
import com.jsowell.pile.service.OrderBasicInfoService;
|
||||||
|
import com.jsowell.pile.vo.AdapayUnsplitRecordVO;
|
||||||
|
import com.jsowell.pile.vo.UnsplitOrderFieldsVO;
|
||||||
|
import org.apache.commons.collections4.CollectionUtils;
|
||||||
|
import org.apache.poi.ss.usermodel.*;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ForkJoinPool;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 汇付未分账订单处理 Task
|
||||||
|
*
|
||||||
|
* 三步流程(可独立调用,也可按顺序执行):
|
||||||
|
* Step1: adapayUnsplitTask.step1ImportExcel('文件路径') — 导入 Excel 到 adapay_unsplit_record
|
||||||
|
* Step2: adapayUnsplitTask.step2CompleteFields('startTime','endTime') — 调 PRE 接口补齐订单字段
|
||||||
|
* Step3: adapayUnsplitTask.step3ProcessSplit() — 分账 + 退款处理
|
||||||
|
*/
|
||||||
|
@Component("adapayUnsplitTask")
|
||||||
|
public class AdapayUnsplitTask {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(AdapayUnsplitTask.class);
|
||||||
|
|
||||||
|
/** 批量导入每批大小 */
|
||||||
|
private static final int IMPORT_BATCH_SIZE = 500;
|
||||||
|
/** 补齐字段分页大小 */
|
||||||
|
private static final int COMPLETE_PAGE_SIZE = 200;
|
||||||
|
/** 调 PRE 接口每批 orderCode 数量(避免 URL/Body 过长) */
|
||||||
|
private static final int PRE_QUERY_BATCH_SIZE = 100;
|
||||||
|
|
||||||
|
@Value("${pre.base-url:http://localhost:8080}")
|
||||||
|
private String preBaseUrl;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private AdapayUnsplitRecordService adapayUnsplitRecordService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private AdapayService adapayService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RedisCache redisCache;
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// Step 1: 导入 Excel → adapay_unsplit_record
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从默认路径导入 Excel
|
||||||
|
* adapayUnsplitTask.step1ImportExcel()
|
||||||
|
*/
|
||||||
|
public void step1ImportExcel() {
|
||||||
|
step1ImportExcel("doc/万车充小程序-未分账明细.xlsx");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从指定路径导入 Excel 到 adapay_unsplit_record 表
|
||||||
|
* adapayUnsplitTask.step1ImportExcel('doc/万车充小程序-未分账明细.xlsx')
|
||||||
|
*/
|
||||||
|
public void step1ImportExcel(String filePath) {
|
||||||
|
Path path = Paths.get(filePath);
|
||||||
|
if (!path.isAbsolute()) {
|
||||||
|
path = Paths.get(System.getProperty("user.dir"), filePath);
|
||||||
|
}
|
||||||
|
if (!Files.exists(path)) {
|
||||||
|
log.error("[step1ImportExcel] 文件不存在: {}", path.toAbsolutePath());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int total = 0, success = 0, skipped = 0, failed = 0;
|
||||||
|
DataFormatter formatter = new DataFormatter();
|
||||||
|
|
||||||
|
try (InputStream is = Files.newInputStream(path);
|
||||||
|
Workbook workbook = WorkbookFactory.create(is)) {
|
||||||
|
|
||||||
|
Sheet sheet = workbook.getSheetAt(0);
|
||||||
|
if (sheet == null) {
|
||||||
|
log.error("[step1ImportExcel] Excel 无 Sheet, file:{}", path.toAbsolutePath());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Row headerRow = sheet.getRow(sheet.getFirstRowNum());
|
||||||
|
if (headerRow == null) {
|
||||||
|
log.error("[step1ImportExcel] Excel 无表头, file:{}", path.toAbsolutePath());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Integer> headerMap = buildHeaderMap(headerRow, formatter);
|
||||||
|
List<String> required = Arrays.asList(
|
||||||
|
"商户号", "支付时间", "交易流水号", "交易订单号",
|
||||||
|
"交易订单金额", "已确认分账金额", "已撤销金额", "支付确认撤销金额", "剩余未分账金额"
|
||||||
|
);
|
||||||
|
for (String h : required) {
|
||||||
|
if (!headerMap.containsKey(normalize(h))) {
|
||||||
|
log.error("[step1ImportExcel] 缺少必要列: {}", h);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<AdapayUnsplitRecord> batch = new ArrayList<>(IMPORT_BATCH_SIZE);
|
||||||
|
int firstData = sheet.getFirstRowNum() + 1;
|
||||||
|
int lastData = sheet.getLastRowNum();
|
||||||
|
|
||||||
|
for (int rowNum = firstData; rowNum <= lastData; rowNum++) {
|
||||||
|
Row row = sheet.getRow(rowNum);
|
||||||
|
if (row == null || isRowEmpty(row)) continue;
|
||||||
|
total++;
|
||||||
|
|
||||||
|
try {
|
||||||
|
AdapayUnsplitRecord record = rowToRecord(row, headerMap, formatter);
|
||||||
|
if (record == null) { skipped++; continue; }
|
||||||
|
batch.add(record);
|
||||||
|
|
||||||
|
if (batch.size() >= IMPORT_BATCH_SIZE) {
|
||||||
|
adapayUnsplitRecordService.batchInsertOrUpdateSelective(batch);
|
||||||
|
success += batch.size();
|
||||||
|
batch.clear();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
failed++;
|
||||||
|
log.error("[step1ImportExcel] 行解析失败, rowNum:{}", rowNum + 1, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!batch.isEmpty()) {
|
||||||
|
adapayUnsplitRecordService.batchInsertOrUpdateSelective(batch);
|
||||||
|
success += batch.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[step1ImportExcel] 读取 Excel 失败, file:{}", path.toAbsolutePath(), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("[step1ImportExcel] 完成, total:{}, success:{}, skipped:{}, failed:{}", total, success, skipped, failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// Step 2: 调 PRE 接口补齐 order_code/pile_type/settle_amount/due_refund_amount
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 补齐指定时间范围内的未分账记录字段
|
||||||
|
* adapayUnsplitTask.step2CompleteFields('2024-01-01 00:00:00', '2025-12-31 23:59:59')
|
||||||
|
*/
|
||||||
|
public void step2CompleteFields(String startTime, String endTime) {
|
||||||
|
int pageNum = 1;
|
||||||
|
int updatedTotal = 0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
// 分页查询该时间段内的记录
|
||||||
|
com.jsowell.common.util.PageUtils.startPage(pageNum, COMPLETE_PAGE_SIZE);
|
||||||
|
List<AdapayUnsplitRecord> list = adapayUnsplitRecordService.queryUnsplitOrders(startTime, endTime);
|
||||||
|
if (CollectionUtils.isEmpty(list)) break;
|
||||||
|
|
||||||
|
// 1. 从 orderNo 中提取 orderCode(C 开头,下划线前的部分)
|
||||||
|
for (AdapayUnsplitRecord r : list) {
|
||||||
|
if (StringUtils.isBlank(r.getOrderCode())) {
|
||||||
|
r.setOrderCode(extractOrderCode(r.getOrderNo()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 收集有效 orderCode,批量调 PRE 接口查询订单字段
|
||||||
|
Set<String> orderCodes = list.stream()
|
||||||
|
.map(AdapayUnsplitRecord::getOrderCode)
|
||||||
|
.filter(c -> StringUtils.isNotBlank(c) && c.startsWith("C"))
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
Map<String, UnsplitOrderFieldsVO> orderFieldMap = new HashMap<>();
|
||||||
|
if (CollectionUtils.isNotEmpty(orderCodes)) {
|
||||||
|
orderFieldMap = queryOrderFieldsFromPre(orderCodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 逐条比对,有变化才加入更新列表
|
||||||
|
List<AdapayUnsplitRecord> updateList = new ArrayList<>();
|
||||||
|
Date now = DateUtils.getNowDate();
|
||||||
|
for (AdapayUnsplitRecord r : list) {
|
||||||
|
boolean changed = false;
|
||||||
|
|
||||||
|
// 补 orderCode
|
||||||
|
String orderCode = r.getOrderCode();
|
||||||
|
if (StringUtils.isBlank(orderCode)) {
|
||||||
|
orderCode = extractOrderCode(r.getOrderNo());
|
||||||
|
if (StringUtils.isNotBlank(orderCode)) {
|
||||||
|
r.setOrderCode(orderCode);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用 PRE 接口数据补齐其余字段
|
||||||
|
if (StringUtils.isNotBlank(orderCode)) {
|
||||||
|
UnsplitOrderFieldsVO fields = orderFieldMap.get(orderCode);
|
||||||
|
if (fields != null) {
|
||||||
|
// pile_type
|
||||||
|
String pileType = YouDianUtils.isEBikePileSn(fields.getPileSn()) ? "eBike" : "EV";
|
||||||
|
if (!StringUtils.equals(r.getPileType(), pileType)) {
|
||||||
|
r.setPileType(pileType);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
// settle_amount
|
||||||
|
if (!isSameAmount(r.getSettleAmount(), fields.getSettleAmount())) {
|
||||||
|
r.setSettleAmount(fields.getSettleAmount());
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
// due_refund_amount
|
||||||
|
if (!isSameAmount(r.getDueRefundAmount(), fields.getRefundAmount())) {
|
||||||
|
r.setDueRefundAmount(fields.getRefundAmount());
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
r.setUpdateTime(now);
|
||||||
|
updateList.add(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CollectionUtils.isNotEmpty(updateList)) {
|
||||||
|
adapayUnsplitRecordService.updateBatchSelective(updateList);
|
||||||
|
updatedTotal += updateList.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (list.size() < COMPLETE_PAGE_SIZE) break;
|
||||||
|
pageNum++;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("[step2CompleteFields] 完成, startTime:{}, endTime:{}, 更新:{}条", startTime, endTime, updatedTotal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量调 PRE 环境 /internal/order/unsplit-fields 接口查询订单字段
|
||||||
|
* 每批最多 PRE_QUERY_BATCH_SIZE 个 orderCode,并行发起请求
|
||||||
|
*/
|
||||||
|
private Map<String, UnsplitOrderFieldsVO> queryOrderFieldsFromPre(Set<String> orderCodes) {
|
||||||
|
Map<String, UnsplitOrderFieldsVO> result = new HashMap<>();
|
||||||
|
List<String> codeList = new ArrayList<>(orderCodes);
|
||||||
|
String url = preBaseUrl + "/internal/order/unsplit-fields";
|
||||||
|
|
||||||
|
// 分批请求,每批 PRE_QUERY_BATCH_SIZE 条
|
||||||
|
List<List<String>> partitions = new ArrayList<>();
|
||||||
|
for (int i = 0; i < codeList.size(); i += PRE_QUERY_BATCH_SIZE) {
|
||||||
|
partitions.add(codeList.subList(i, Math.min(i + PRE_QUERY_BATCH_SIZE, codeList.size())));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (List<String> batch : partitions) {
|
||||||
|
try {
|
||||||
|
String body = JSON.toJSONString(batch);
|
||||||
|
String resp = HttpUtils.sendPostContentType(url, body, "application/json");
|
||||||
|
if (StringUtils.isBlank(resp)) continue;
|
||||||
|
|
||||||
|
JSONObject json = JSONObject.parseObject(resp);
|
||||||
|
if (json == null || json.getIntValue("code") != 200) {
|
||||||
|
log.warn("[step2CompleteFields] PRE 接口返回异常: {}", resp);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<UnsplitOrderFieldsVO> list = json.getJSONArray("data")
|
||||||
|
.toJavaList(UnsplitOrderFieldsVO.class);
|
||||||
|
if (CollectionUtils.isNotEmpty(list)) {
|
||||||
|
list.forEach(v -> result.put(v.getOrderCode(), v));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[step2CompleteFields] 调 PRE 接口失败, batch size:{}", batch.size(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// Step 3: 分账 + 退款处理
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理所有待分账记录(默认 appId,10 线程并行)
|
||||||
|
* adapayUnsplitTask.step3ProcessSplit()
|
||||||
|
*/
|
||||||
|
public void step3ProcessSplit() {
|
||||||
|
step3ProcessSplit(Constants.DEFAULT_APP_ID, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理所有待分账记录
|
||||||
|
* 逻辑:
|
||||||
|
* - remaining_split_amount > 0 时需要处理
|
||||||
|
* - due_refund_amount > refund_amount:差额部分走退款
|
||||||
|
* - settle_amount > confirmed_split_amount:差额部分走分账
|
||||||
|
* adapayUnsplitTask.step3ProcessSplit('wx_app_id', 10)
|
||||||
|
*/
|
||||||
|
public void step3ProcessSplit(String wechatAppId, Integer parallelism) {
|
||||||
|
int threads = (parallelism == null || parallelism <= 0) ? 10 : parallelism;
|
||||||
|
|
||||||
|
Boolean acquired = redisCache.setnx(CacheConstants.PROCESS_UNSPLIT_ORDERS, Constants.ONE, 120L);
|
||||||
|
if (Boolean.FALSE.equals(acquired)) {
|
||||||
|
log.info("[step3ProcessSplit] 上一批次仍在执行中,跳过");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<AdapayUnsplitRecordVO> list = adapayUnsplitRecordService.queryList();
|
||||||
|
if (CollectionUtils.isEmpty(list)) {
|
||||||
|
log.info("[step3ProcessSplit] 无待处理记录");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log.info("[step3ProcessSplit] 共{}条待处理记录,并行线程数:{}", list.size(), threads);
|
||||||
|
|
||||||
|
AtomicInteger splitSuccess = new AtomicInteger();
|
||||||
|
AtomicInteger splitFailed = new AtomicInteger();
|
||||||
|
AtomicInteger refundSuccess = new AtomicInteger();
|
||||||
|
AtomicInteger refundFailed = new AtomicInteger();
|
||||||
|
AtomicInteger skipped = new AtomicInteger();
|
||||||
|
|
||||||
|
ForkJoinPool pool = new ForkJoinPool(threads);
|
||||||
|
try {
|
||||||
|
pool.submit(() ->
|
||||||
|
list.parallelStream().forEach(item -> {
|
||||||
|
String paymentId = item.getPaymentId();
|
||||||
|
String orderCode = item.getOrderCode();
|
||||||
|
if (StringUtils.isBlank(paymentId)) {
|
||||||
|
skipped.incrementAndGet();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 退款部分 ---
|
||||||
|
// due_refund_amount - refund_amount = 应退但未退的金额
|
||||||
|
BigDecimal dueRefund = nvl(item.getRefundAmount()); // due_refund_amount
|
||||||
|
BigDecimal paidRefund = nvl(item.getPaidAmount()); // refund_amount(已退)
|
||||||
|
BigDecimal pendingRefund = dueRefund.subtract(paidRefund);
|
||||||
|
if (pendingRefund.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
doRefund(paymentId, orderCode, pendingRefund, wechatAppId, refundSuccess, refundFailed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 分账部分 ---
|
||||||
|
// waitSplitAmount = settle_amount - confirmed_split_amount - payment_revoke_amount
|
||||||
|
BigDecimal waitSplit = nvl(item.getWaitSplitAmount());
|
||||||
|
if (waitSplit.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
doSplit(item, paymentId, orderCode, waitSplit, wechatAppId, splitSuccess, splitFailed);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingRefund.compareTo(BigDecimal.ZERO) <= 0 && waitSplit.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
|
skipped.incrementAndGet();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).get();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[step3ProcessSplit] 并行执行异常", e);
|
||||||
|
} finally {
|
||||||
|
pool.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("[step3ProcessSplit] 完成, splitSuccess:{}, splitFailed:{}, refundSuccess:{}, refundFailed:{}, skipped:{}",
|
||||||
|
splitSuccess.get(), splitFailed.get(), refundSuccess.get(), refundFailed.get(), skipped.get());
|
||||||
|
} finally {
|
||||||
|
redisCache.deleteObject(CacheConstants.PROCESS_UNSPLIT_ORDERS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// 私有方法
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
private void doRefund(String paymentId, String orderCode, BigDecimal refundAmt,
|
||||||
|
String wechatAppId, AtomicInteger success, AtomicInteger failed) {
|
||||||
|
try {
|
||||||
|
RefundResponse resp = adapayService.createRefundRequest(
|
||||||
|
paymentId, refundAmt.setScale(2, BigDecimal.ROUND_HALF_UP),
|
||||||
|
wechatAppId, Constants.ZERO, "SPLIT", orderCode);
|
||||||
|
|
||||||
|
if (resp != null && resp.isSuccess()) {
|
||||||
|
success.incrementAndGet();
|
||||||
|
updateSplitResult(paymentId, "REFUND_SUCCESS",
|
||||||
|
"退款金额: " + refundAmt.toPlainString());
|
||||||
|
log.info("[step3ProcessSplit] 退款成功, paymentId:{}, orderCode:{}, refundAmt:{}", paymentId, orderCode, refundAmt);
|
||||||
|
} else {
|
||||||
|
failed.incrementAndGet();
|
||||||
|
String errCode = resp == null ? "null" : resp.getError_code();
|
||||||
|
String errMsg = resp == null ? "response_is_null" : resp.getError_msg();
|
||||||
|
updateSplitResult(paymentId, "REFUND_FAILED", errCode + ": " + errMsg);
|
||||||
|
log.error("[step3ProcessSplit] 退款失败, paymentId:{}, orderCode:{}, errCode:{}, errMsg:{}", paymentId, orderCode, errCode, errMsg);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
failed.incrementAndGet();
|
||||||
|
updateSplitResult(paymentId, "REFUND_FAILED", "异常: " + e.getMessage());
|
||||||
|
log.error("[step3ProcessSplit] 退款异常, paymentId:{}, orderCode:{}", paymentId, orderCode, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doSplit(AdapayUnsplitRecordVO item, String paymentId, String orderCode,
|
||||||
|
BigDecimal waitSplit, String wechatAppId,
|
||||||
|
AtomicInteger success, AtomicInteger failed) {
|
||||||
|
try {
|
||||||
|
DivMember divMember = new DivMember();
|
||||||
|
divMember.setMemberId(Constants.ZERO);
|
||||||
|
divMember.setAmount(waitSplit.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString());
|
||||||
|
divMember.setFeeFlag(Constants.Y);
|
||||||
|
|
||||||
|
PaymentConfirmParam param = PaymentConfirmParam.builder()
|
||||||
|
.paymentId(paymentId)
|
||||||
|
.divMemberList(Lists.newArrayList(divMember))
|
||||||
|
.confirmAmt(waitSplit)
|
||||||
|
.orderCode(orderCode)
|
||||||
|
.wechatAppId(wechatAppId)
|
||||||
|
.build();
|
||||||
|
PaymentConfirmResponse resp = adapayService.createPaymentConfirmRequest(param);
|
||||||
|
|
||||||
|
if (resp != null && resp.isSuccess()) {
|
||||||
|
success.incrementAndGet();
|
||||||
|
// 累加 confirmed_split_amount
|
||||||
|
BigDecimal newConfirmed = nvl(item.getConfirmedSplitAmount())
|
||||||
|
.add(waitSplit).setScale(2, BigDecimal.ROUND_HALF_UP);
|
||||||
|
AdapayUnsplitRecord update = new AdapayUnsplitRecord();
|
||||||
|
update.setPaymentId(paymentId);
|
||||||
|
update.setConfirmedSplitAmount(newConfirmed);
|
||||||
|
update.setSplitFlag("SUCCESS");
|
||||||
|
update.setSplitRemark("分账金额: " + waitSplit.toPlainString());
|
||||||
|
update.setUpdateTime(DateUtils.getNowDate());
|
||||||
|
adapayUnsplitRecordService.updateByPaymentIdSelective(update);
|
||||||
|
log.info("[step3ProcessSplit] 分账成功, paymentId:{}, orderCode:{}, amt:{}", paymentId, orderCode, waitSplit);
|
||||||
|
} else {
|
||||||
|
failed.incrementAndGet();
|
||||||
|
String errCode = resp == null ? "null" : resp.getError_code();
|
||||||
|
String errMsg = resp == null ? "response_is_null" : resp.getError_msg();
|
||||||
|
updateSplitResult(paymentId, "FAILED", errCode + ": " + errMsg);
|
||||||
|
log.error("[step3ProcessSplit] 分账失败, paymentId:{}, orderCode:{}, errCode:{}, errMsg:{}", paymentId, orderCode, errCode, errMsg);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
failed.incrementAndGet();
|
||||||
|
updateSplitResult(paymentId, "FAILED", "异常: " + e.getMessage());
|
||||||
|
log.error("[step3ProcessSplit] 分账异常, paymentId:{}, orderCode:{}", paymentId, orderCode, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSplitResult(String paymentId, String splitFlag, String splitRemark) {
|
||||||
|
AdapayUnsplitRecord r = new AdapayUnsplitRecord();
|
||||||
|
r.setPaymentId(paymentId);
|
||||||
|
r.setSplitFlag(splitFlag);
|
||||||
|
r.setSplitRemark(splitRemark);
|
||||||
|
r.setUpdateTime(DateUtils.getNowDate());
|
||||||
|
adapayUnsplitRecordService.updateByPaymentIdSelective(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AdapayUnsplitRecord rowToRecord(Row row, Map<String, Integer> headerMap, DataFormatter formatter) {
|
||||||
|
String paymentId = cellStr(row, headerMap.get(normalize("交易流水号")), formatter);
|
||||||
|
if (StringUtils.isBlank(paymentId)) return null;
|
||||||
|
|
||||||
|
String orderNo = cellStr(row, headerMap.get(normalize("交易订单号")), formatter);
|
||||||
|
String orderCode = extractOrderCode(orderNo);
|
||||||
|
|
||||||
|
AdapayUnsplitRecord r = new AdapayUnsplitRecord();
|
||||||
|
r.setMerchantCode(cellStr(row, headerMap.get(normalize("商户号")), formatter));
|
||||||
|
r.setPayTime(parsePayTime(getCell(row, headerMap.get(normalize("支付时间"))), formatter));
|
||||||
|
r.setPaymentId(paymentId);
|
||||||
|
r.setOrderNo(orderNo);
|
||||||
|
r.setOrderCode(orderCode);
|
||||||
|
r.setPayAmount(cellDecimal(row, headerMap.get(normalize("交易订单金额")), formatter));
|
||||||
|
r.setConfirmedSplitAmount(cellDecimal(row, headerMap.get(normalize("已确认分账金额")), formatter));
|
||||||
|
r.setRefundAmount(cellDecimal(row, headerMap.get(normalize("已撤销金额")), formatter));
|
||||||
|
r.setPaymentRevokeAmount(cellDecimal(row, headerMap.get(normalize("支付确认撤销金额")), formatter));
|
||||||
|
r.setRemainingSplitAmount(cellDecimal(row, headerMap.get(normalize("剩余未分账金额")), formatter));
|
||||||
|
r.setUpdateTime(DateUtils.getNowDate());
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractOrderCode(String orderNo) {
|
||||||
|
if (StringUtils.isBlank(orderNo)) return null;
|
||||||
|
int idx = orderNo.indexOf("_");
|
||||||
|
String code = idx > 0 ? orderNo.substring(0, idx) : orderNo;
|
||||||
|
return code.length() <= 16 ? code : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Integer> buildHeaderMap(Row headerRow, DataFormatter formatter) {
|
||||||
|
Map<String, Integer> map = new HashMap<>();
|
||||||
|
for (int i = headerRow.getFirstCellNum(); i < headerRow.getLastCellNum(); i++) {
|
||||||
|
Cell cell = headerRow.getCell(i, Row.MissingCellPolicy.RETURN_BLANK_AS_NULL);
|
||||||
|
if (cell == null) continue;
|
||||||
|
String h = normalize(formatter.formatCellValue(cell));
|
||||||
|
if (StringUtils.isNotBlank(h)) map.put(h, i);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String cellStr(Row row, Integer col, DataFormatter formatter) {
|
||||||
|
Cell cell = getCell(row, col);
|
||||||
|
if (cell == null) return null;
|
||||||
|
String v = formatter.formatCellValue(cell);
|
||||||
|
return StringUtils.isBlank(v) ? null : v.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Cell getCell(Row row, Integer col) {
|
||||||
|
if (row == null || col == null) return null;
|
||||||
|
return row.getCell(col, Row.MissingCellPolicy.RETURN_BLANK_AS_NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal cellDecimal(Row row, Integer col, DataFormatter formatter) {
|
||||||
|
String v = cellStr(row, col, formatter);
|
||||||
|
if (StringUtils.isBlank(v)) return BigDecimal.ZERO;
|
||||||
|
try { return new BigDecimal(v.replace(",", "").trim()); }
|
||||||
|
catch (NumberFormatException e) { return BigDecimal.ZERO; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private Date parsePayTime(Cell cell, DataFormatter formatter) {
|
||||||
|
if (cell == null) return null;
|
||||||
|
if (cell.getCellType() == CellType.NUMERIC) return DateUtil.getJavaDate(cell.getNumericCellValue());
|
||||||
|
if (cell.getCellType() == CellType.FORMULA
|
||||||
|
&& cell.getCachedFormulaResultType() == CellType.NUMERIC)
|
||||||
|
return DateUtil.getJavaDate(cell.getNumericCellValue());
|
||||||
|
String v = formatter.formatCellValue(cell).trim();
|
||||||
|
if (StringUtils.isBlank(v)) return null;
|
||||||
|
Date d = DateUtils.parseDate(v);
|
||||||
|
if (d != null) return d;
|
||||||
|
try { return DateUtil.getJavaDate(Double.parseDouble(v)); }
|
||||||
|
catch (Exception e) { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isRowEmpty(Row row) {
|
||||||
|
for (int i = row.getFirstCellNum(); i < row.getLastCellNum(); i++) {
|
||||||
|
Cell c = row.getCell(i, Row.MissingCellPolicy.RETURN_BLANK_AS_NULL);
|
||||||
|
if (c != null && c.getCellType() != CellType.BLANK) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalize(String s) {
|
||||||
|
return s == null ? "" : s.replace(" ", "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isSameAmount(BigDecimal a, BigDecimal b) {
|
||||||
|
if (a == null && b == null) return true;
|
||||||
|
if (a == null || b == null) return false;
|
||||||
|
return a.compareTo(b) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal nvl(BigDecimal v) {
|
||||||
|
return v == null ? BigDecimal.ZERO : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal nvl(String v) {
|
||||||
|
if (StringUtils.isBlank(v)) return BigDecimal.ZERO;
|
||||||
|
try { return new BigDecimal(v.replace(",", "").trim()); }
|
||||||
|
catch (Exception e) { return BigDecimal.ZERO; }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user