package com.tcm.training.admin.points.service;
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.tcm.training.admin.member.entity.AppUser;
|
import com.tcm.training.admin.member.mapper.UserMapper;
|
import com.tcm.training.admin.live.entity.Clinic;
|
import com.tcm.training.admin.live.mapper.ClinicMapper;
|
import com.tcm.training.admin.points.entity.PointsConfig;
|
import com.tcm.training.admin.points.entity.PointsExchangeOrder;
|
import com.tcm.training.admin.points.entity.PointsExchangeOrderItem;
|
import com.tcm.training.admin.points.entity.UserPointsAccount;
|
import com.tcm.training.admin.points.mapper.PointsConfigMapper;
|
import com.tcm.training.admin.points.mapper.PointsExchangeOrderItemMapper;
|
import com.tcm.training.admin.points.mapper.PointsExchangeOrderMapper;
|
import com.tcm.training.admin.points.mapper.UserPointsAccountMapper;
|
import com.tcm.training.admin.points.row.PointsSettlementCandidateRow;
|
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;
|
import java.util.ArrayList;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
|
@Service
|
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;
|
private final PointsExchangeOrderMapper pointsExchangeOrderMapper;
|
private final PointsExchangeOrderItemMapper pointsExchangeOrderItemMapper;
|
private final UserMapper userMapper;
|
private final ClinicMapper clinicMapper;
|
private final PointsLedgerAppService pointsLedgerAppService;
|
|
public PointsSettlementService(PointsConfigMapper pointsConfigMapper,
|
UserPointsAccountMapper userPointsAccountMapper,
|
PointsExchangeOrderMapper pointsExchangeOrderMapper,
|
PointsExchangeOrderItemMapper pointsExchangeOrderItemMapper,
|
UserMapper userMapper,
|
ClinicMapper clinicMapper,
|
PointsLedgerAppService pointsLedgerAppService) {
|
this.pointsConfigMapper = pointsConfigMapper;
|
this.userPointsAccountMapper = userPointsAccountMapper;
|
this.pointsExchangeOrderMapper = pointsExchangeOrderMapper;
|
this.pointsExchangeOrderItemMapper = pointsExchangeOrderItemMapper;
|
this.userMapper = userMapper;
|
this.clinicMapper = clinicMapper;
|
this.pointsLedgerAppService = pointsLedgerAppService;
|
}
|
|
@Scheduled(fixedDelayString = "${app.points.settlement-interval-ms:60000}")
|
public void settleExpiredConfigs() {
|
LocalDateTime now = LocalDateTime.now();
|
List<PointsConfig> configs = pointsConfigMapper.selectList(new LambdaQueryWrapper<PointsConfig>()
|
.eq(PointsConfig::getDeleted, 0)
|
.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());
|
}
|
}
|
|
@Transactional
|
public void settleConfig(Long configId) {
|
PointsConfig config = pointsConfigMapper.selectById(configId);
|
if (config == null || config.getDeleted() != null && config.getDeleted() == 1) {
|
return;
|
}
|
if (config.getSettlementStatus() != null && config.getSettlementStatus() == 1) {
|
return;
|
}
|
if (config.getActivityEndAt() == null || config.getActivityEndAt().isAfter(LocalDateTime.now())) {
|
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) {
|
continue;
|
}
|
if (balance >= config.getRequiredPoints()) {
|
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) {
|
account.setPointsBalance(0);
|
account.setLastSettledConfigId(config.getId());
|
account.setLastClearedAt(now);
|
userPointsAccountMapper.updateById(account);
|
}
|
}
|
buildSummaryOrders(config, createdItems, now);
|
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) {
|
PointsExchangeOrderItem item = new PointsExchangeOrderItem();
|
item.setOrderNo(buildOrderNo("PXD", candidate.getAppUserId(), now));
|
item.setAppUserId(candidate.getAppUserId());
|
item.setUserNickname(candidate.getUserNickname());
|
item.setUserPhone(candidate.getUserPhone());
|
item.setClinicId(candidate.getClinicId());
|
item.setClinicName(candidate.getClinicName());
|
item.setProvinceCode(candidate.getProvinceCode());
|
item.setProvinceName(candidate.getProvinceName());
|
item.setCityCode(candidate.getCityCode());
|
item.setCityName(candidate.getCityName());
|
item.setDistrictCode(candidate.getDistrictCode());
|
item.setDistrictName(candidate.getDistrictName());
|
item.setSettledPoints(candidate.getPointsBalance());
|
item.setStatus(0);
|
item.setCreatedAt(now);
|
return item;
|
}
|
|
private void buildSummaryOrders(PointsConfig config, List<PointsExchangeOrderItem> items, LocalDateTime now) {
|
if (items.isEmpty()) {
|
return;
|
}
|
Map<String, List<PointsExchangeOrderItem>> grouped = new LinkedHashMap<String, List<PointsExchangeOrderItem>>();
|
for (PointsExchangeOrderItem item : items) {
|
String key = buildGroupKey(item);
|
List<PointsExchangeOrderItem> group = grouped.get(key);
|
if (group == null) {
|
group = new ArrayList<PointsExchangeOrderItem>();
|
grouped.put(key, group);
|
}
|
group.add(item);
|
}
|
|
for (Map.Entry<String, List<PointsExchangeOrderItem>> entry : grouped.entrySet()) {
|
List<PointsExchangeOrderItem> group = entry.getValue();
|
PointsExchangeOrderItem first = group.get(0);
|
PointsExchangeOrder order = new PointsExchangeOrder();
|
order.setOrderNo(buildOrderNo("PXS", first.getClinicId() == null ? first.getAppUserId() : first.getClinicId(), now));
|
order.setConfigId(config.getId());
|
order.setGiftCover(config.getGiftCover());
|
order.setGiftName(config.getGiftName());
|
order.setRequiredPoints(config.getRequiredPoints());
|
order.setActivityStartAt(config.getActivityStartAt());
|
order.setActivityEndAt(config.getActivityEndAt());
|
order.setClinicId(first.getClinicId());
|
order.setClinicAppUserId(resolveClinicAppUserId(first.getClinicId()));
|
order.setClinicName(first.getClinicName());
|
order.setReceiverName(defaultReceiverName(first));
|
order.setReceiverPhone(defaultReceiverPhone(first));
|
order.setReceiverAddress(defaultReceiverAddress(first));
|
order.setItemCount(group.size());
|
order.setTotalPoints(sumPoints(group));
|
order.setStatus(0);
|
pointsExchangeOrderMapper.insert(order);
|
|
for (PointsExchangeOrderItem item : group) {
|
item.setOrderId(order.getId());
|
pointsExchangeOrderItemMapper.updateById(item);
|
pointsLedgerAppService.createExchangeExpenseLedger(
|
item.getId(),
|
item.getAppUserId(),
|
item.getUserNickname(),
|
item.getUserPhone(),
|
item.getClinicId(),
|
item.getClinicName(),
|
item.getProvinceCode(),
|
item.getProvinceName(),
|
item.getCityCode(),
|
item.getCityName(),
|
item.getDistrictCode(),
|
item.getDistrictName(),
|
config.getGiftName(),
|
item.getId(),
|
item.getOrderNo(),
|
item.getSettledPoints(),
|
item.getCreatedAt() == null ? now : item.getCreatedAt()
|
);
|
}
|
}
|
}
|
|
private String buildGroupKey(PointsExchangeOrderItem item) {
|
if (item.getClinicId() != null) {
|
return "clinic:" + item.getClinicId();
|
}
|
return "user:" + item.getAppUserId();
|
}
|
|
private int sumPoints(List<PointsExchangeOrderItem> items) {
|
int total = 0;
|
for (PointsExchangeOrderItem item : items) {
|
total += item.getSettledPoints() == null ? 0 : item.getSettledPoints();
|
}
|
return total;
|
}
|
|
private Long resolveClinicAppUserId(Long clinicId) {
|
if (clinicId == null) {
|
return null;
|
}
|
AppUser clinicUser = userMapper.selectOne(new LambdaQueryWrapper<AppUser>()
|
.eq(AppUser::getClinicId, clinicId)
|
.eq(AppUser::getType, 1)
|
.eq(AppUser::getDeleted, 0)
|
.last("limit 1"));
|
return clinicUser == null ? null : clinicUser.getId();
|
}
|
|
private String buildOrderNo(String prefix, Long seed, LocalDateTime now) {
|
long suffix = seed == null ? System.nanoTime() % 100000 : seed % 100000;
|
return prefix + ORDER_NO_FORMATTER.format(now) + String.format("%05d", suffix);
|
}
|
|
private String defaultReceiverName(PointsSettlementCandidateRow candidate) {
|
if (candidate.getClinicId() != null) {
|
Clinic clinic = clinicMapper.selectById(candidate.getClinicId());
|
if (clinic != null && clinic.getContactName() != null && !clinic.getContactName().trim().isEmpty()) {
|
return clinic.getContactName();
|
}
|
}
|
return candidate.getUserNickname();
|
}
|
|
private String defaultReceiverPhone(PointsSettlementCandidateRow candidate) {
|
if (candidate.getClinicContactPhone() != null && !candidate.getClinicContactPhone().trim().isEmpty()) {
|
return candidate.getClinicContactPhone();
|
}
|
return candidate.getUserPhone();
|
}
|
|
private String defaultReceiverAddress(PointsSettlementCandidateRow candidate) {
|
if (candidate.getClinicAddress() != null && !candidate.getClinicAddress().trim().isEmpty()) {
|
return candidate.getClinicAddress();
|
}
|
return "-";
|
}
|
|
private String defaultReceiverName(PointsExchangeOrderItem item) {
|
if (item.getClinicId() != null) {
|
Clinic clinic = clinicMapper.selectById(item.getClinicId());
|
if (clinic != null && clinic.getContactName() != null && !clinic.getContactName().trim().isEmpty()) {
|
return clinic.getContactName();
|
}
|
}
|
return item.getUserNickname();
|
}
|
|
private String defaultReceiverPhone(PointsExchangeOrderItem item) {
|
if (item.getClinicId() != null) {
|
Clinic clinic = clinicMapper.selectById(item.getClinicId());
|
if (clinic != null && clinic.getContactPhone() != null && !clinic.getContactPhone().trim().isEmpty()) {
|
return clinic.getContactPhone();
|
}
|
}
|
return item.getUserPhone();
|
}
|
|
private String defaultReceiverAddress(PointsExchangeOrderItem item) {
|
if (item.getClinicId() != null) {
|
Clinic clinic = clinicMapper.selectById(item.getClinicId());
|
if (clinic != null && clinic.getAddress() != null && !clinic.getAddress().trim().isEmpty()) {
|
return clinic.getAddress();
|
}
|
}
|
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());
|
}
|
}
|