“mitao”
2026-04-17 d699da385dda6257677ee93bef47388c0635f64e
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/points/service/PointsSettlementService.java
@@ -17,6 +17,8 @@
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@@ -29,6 +31,9 @@
public class PointsSettlementService {
    private static final DateTimeFormatter ORDER_NO_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
    private static final String SETTLEMENT_CLEAR_SOURCE_PREFIX = "config-clear-";
    private static final String SETTLEMENT_CLEAR_REFERENCE_PREFIX = "POINTS_CONFIG-";
    private static final Logger log = LoggerFactory.getLogger(PointsSettlementService.class);
    private final PointsConfigMapper pointsConfigMapper;
    private final UserPointsAccountMapper userPointsAccountMapper;
@@ -62,6 +67,9 @@
                .eq(PointsConfig::getSettlementStatus, 0)
                .le(PointsConfig::getActivityEndAt, now)
                .orderByAsc(PointsConfig::getId));
        if (!configs.isEmpty()) {
            log.info("points settlement scan found {} expired pending configs", configs.size());
        }
        for (PointsConfig config : configs) {
            settleConfig(config.getId());
        }
@@ -80,9 +88,13 @@
            return;
        }
        log.info("points settlement start, configId={}, activityEndAt={}, requiredPoints={}",
                config.getId(), config.getActivityEndAt(), config.getRequiredPoints());
        List<PointsSettlementCandidateRow> candidates = userPointsAccountMapper.selectSettlementCandidates();
        List<PointsExchangeOrderItem> createdItems = new ArrayList<PointsExchangeOrderItem>();
        LocalDateTime now = LocalDateTime.now();
        int qualifiedCount = 0;
        int clearedOnlyCount = 0;
        for (PointsSettlementCandidateRow candidate : candidates) {
            int balance = candidate.getPointsBalance() == null ? 0 : candidate.getPointsBalance();
            if (balance <= 0) {
@@ -92,6 +104,28 @@
                PointsExchangeOrderItem item = buildOrderItem(config, candidate, now);
                pointsExchangeOrderItemMapper.insert(item);
                createdItems.add(item);
                qualifiedCount++;
            } else {
                pointsLedgerAppService.createSettlementClearLedger(
                        buildSettlementClearSourceId(config.getId(), candidate.getAppUserId()),
                        candidate.getAppUserId(),
                        candidate.getUserNickname(),
                        candidate.getUserPhone(),
                        candidate.getClinicId(),
                        candidate.getClinicName(),
                        candidate.getProvinceCode(),
                        candidate.getProvinceName(),
                        candidate.getCityCode(),
                        candidate.getCityName(),
                        candidate.getDistrictCode(),
                        candidate.getDistrictName(),
                        config.getGiftName(),
                        config.getId(),
                        buildSettlementReferenceNo(config.getId()),
                        balance,
                        now
                );
                clearedOnlyCount++;
            }
            UserPointsAccount account = userPointsAccountMapper.selectById(candidate.getAccountId());
            if (account != null) {
@@ -105,6 +139,8 @@
        config.setSettlementStatus(1);
        config.setSettledAt(now);
        pointsConfigMapper.updateById(config);
        log.info("points settlement completed, configId={}, qualifiedCount={}, clearedOnlyCount={}, createdItemCount={}, settledAt={}",
                config.getId(), qualifiedCount, clearedOnlyCount, createdItems.size(), now);
    }
    private PointsExchangeOrderItem buildOrderItem(PointsConfig config, PointsSettlementCandidateRow candidate, LocalDateTime now) {
@@ -275,4 +311,20 @@
        }
        return "-";
    }
    /**
     * 构建活动结束清零流水的唯一来源标识,确保重复调度不会重复入账。
     */
    private Long buildSettlementClearSourceId(Long configId, Long appUserId) {
        String compositeId = SETTLEMENT_CLEAR_SOURCE_PREFIX + (configId == null ? "0" : configId)
                + "-" + (appUserId == null ? "0" : appUserId);
        return Long.valueOf(Math.abs((long) compositeId.hashCode()));
    }
    /**
     * 构建活动结算流水引用编号,便于后台追溯到积分活动。
     */
    private String buildSettlementReferenceNo(Long configId) {
        return SETTLEMENT_CLEAR_REFERENCE_PREFIX + (configId == null ? 0L : configId.longValue());
    }
}