package com.cyksj.service.relation.impl; import cn.hutool.core.date.DateUtil; import com.cyksj.common.exception.BusinessRuntimeException; import com.cyksj.mapper.TicketDurationRepairMapper; import com.cyksj.mapper.manage.cms.CmsUserMapper; import com.cyksj.mapper.manage.statistics.IncrExpiryTimeRecordMapper; import com.cyksj.model.dto.TicketDurationRepairOrder; import com.cyksj.model.dto.TicketDurationRepairRow; import com.cyksj.model.entity.CmsUser; import com.cyksj.model.entity.GroupsRelation; import com.cyksj.model.entity.IncrExpiryTimeRecord; import com.cyksj.model.request.TicketDurationRepairReq; import com.cyksj.model.response.TicketDurationRepairItemResp; import com.cyksj.model.response.TicketDurationRepairPreviewResp; import com.cyksj.service.relation.TicketDurationRepairService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @Service @RequiredArgsConstructor public class TicketDurationRepairServiceImpl implements TicketDurationRepairService { private final TicketDurationRepairMapper mapper; private final IncrExpiryTimeRecordMapper recordMapper; private final CmsUserMapper cmsUserMapper; @Override @Transactional(readOnly = true, isolation = Isolation.REPEATABLE_READ) public TicketDurationRepairPreviewResp preview(Long afterId, Integer limit, Long userId) { if (afterId == null || afterId < 0 || limit == null || limit < 1 || limit > 200) { throw BusinessRuntimeException.getInstance("afterId必须大于等于0,limit必须在1至200之间"); } if (userId != null && userId <= 0) { throw BusinessRuntimeException.getInstance("userId必须大于0"); } List fetched = mapper.selectPage(afterId, limit + 1, userId); boolean hasMore = fetched.size() > limit; List rows = hasMore ? fetched.subList(0, limit) : fetched; TicketDurationRepairPreviewResp response = new TicketDurationRepairPreviewResp(); response.setUserId(userId); response.setAfterId(afterId); response.setNextAfterId(rows.isEmpty() ? afterId : rows.get(rows.size() - 1).getRelationId()); response.setHasMore(hasMore); response.setItems(classifyAll(rows)); return response; } @Override @Transactional(rollbackFor = Exception.class) public List execute(TicketDurationRepairReq req, Long adminId) { validateRequest(req, adminId); // Lock in a stable order. The entire batch is validated before the first mutation. List snapshots = new ArrayList<>(req.getItems()); snapshots.sort(Comparator.comparing(TicketDurationRepairReq.Snapshot::getRelationId)); List rows = new ArrayList<>(); for (TicketDurationRepairReq.Snapshot snapshot : snapshots) { TicketDurationRepairRow row = mapper.selectForUpdate(snapshot.getRelationId()); if (row == null || !matches(row, snapshot)) { throw BusinessRuntimeException.getInstance("车票" + snapshot.getRelationId() + "已变化或不属于目标规格,请重新预览"); } rows.add(row); } List results = classifyAll(rows); for (TicketDurationRepairItemResp result : results) { if (!"candidate".equals(result.getDecision())) { throw BusinessRuntimeException.getInstance("车票" + result.getRelationId() + "不能自动修复:" + result.getReason()); } } CmsUser admin = cmsUserMapper.selectById(adminId); String operator = admin != null && admin.getNickname() != null ? admin.getNickname() : String.valueOf(adminId); for (int i = 0; i < rows.size(); i++) { TicketDurationRepairRow row = rows.get(i); TicketDurationRepairItemResp result = results.get(i); Date expiryTime = new Date(result.getNewExpiryTime()); if (mapper.updateExpiry(row, expiryTime) != 1) { throw BusinessRuntimeException.getInstance("车票" + row.getRelationId() + "更新失败,整批修复已回滚"); } IncrExpiryTimeRecord record = new IncrExpiryTimeRecord(); record.setRelationId(row.getRelationId()); record.setUserId(row.getUserId()); record.setExpiryTimeInitial(row.getExpiryTime()); // Existing audit schema records days; calculate the actual calendar-day difference. int days = (int) ChronoUnit.DAYS.between(DateUtil.toLocalDateTime(row.getExpiryTime()).toLocalDate(), DateUtil.toLocalDateTime(expiryTime).toLocalDate()); record.setIncrExpiryDays(days); record.setOperator(operator); record.setRemark(req.getRemark() != null && !req.getRemark().isBlank() ? req.getRemark() : "规格772/774半年付时长修复:原到期时间减6个自然月;原到期=" + DateUtil.formatDateTime(row.getExpiryTime()) + ";新到期=" + DateUtil.formatDateTime(expiryTime)); if (recordMapper.insert(record) != 1) { throw BusinessRuntimeException.getInstance("车票" + row.getRelationId() + "修复记录保存失败,整批修复已回滚"); } result.setDecision("updated"); result.setReason("已从原到期时间扣减6个自然月"); } return results; } private List classifyAll(List rows) { if (rows.isEmpty()) { return Collections.emptyList(); } List ids = rows.stream().map(TicketDurationRepairRow::getRelationId).collect(Collectors.toList()); Map> orders = new HashMap<>(); for (TicketDurationRepairOrder order : mapper.selectOrders(ids)) { Map byId = orders.computeIfAbsent(order.getRelationId(), key -> new HashMap<>()); TicketDurationRepairOrder previous = byId.putIfAbsent(order.getOrderId(), order); if (previous != null && Boolean.TRUE.equals(order.getSlot())) { previous.setSlot(true); } } Set adjusted = new HashSet<>(mapper.selectAdjustedRelationIds(ids)); List results = new ArrayList<>(); for (TicketDurationRepairRow row : rows) { results.add(classify(row, new ArrayList<>(orders.getOrDefault(row.getRelationId(), Collections.emptyMap()).values()), adjusted.contains(row.getRelationId()))); } return results; } private TicketDurationRepairItemResp classify(TicketDurationRepairRow row, List orders, boolean adjusted) { TicketDurationRepairItemResp result = new TicketDurationRepairItemResp(); result.setRelationId(row.getRelationId()); result.setGroupsId(row.getGroupsId()); result.setSkuId(row.getSkuId()); result.setUserId(row.getUserId()); result.setStartTime(millis(row.getStartTime())); result.setExpiryTime(millis(row.getExpiryTime())); result.setUpdateTime(millis(row.getUpdateTime())); result.setOrderCount(orders.size()); if (!isTarget(row.getSkuId())) { return decision(result, "skipped", "不是规格772或774"); } if (row.getStartTime() == null || row.getExpiryTime() == null) { return decision(result, "skipped", "未设置完整起止时间"); } Date expected = DateUtil.offsetMonth(row.getStartTime(), 12); long difference = row.getExpiryTime().getTime() - expected.getTime(); result.setYearDifferenceMillis(difference); if (difference != 0) { if (Math.abs(difference) <= 60_000 || DateUtil.isSameDay(expected, row.getExpiryTime())) { return decision(result, "review", "接近一年但存在时间偏差,需核对"); } return decision(result, "skipped", "已设置的时长不是12个自然月"); } List reasons = new ArrayList<>(); if (row.getUserId() == null || row.getUserId() <= 0 || (row.getStatus() != GroupsRelation.Status.validity && row.getStatus() != GroupsRelation.Status.outside)) { reasons.add("车票无当前用户或状态无效"); } if (Boolean.TRUE.equals(row.getIsFrozen()) || Boolean.TRUE.equals(row.getIsRecover()) || nonzero(row.getReservedDays())) { reasons.add("有冻结/恢复/保留天数"); } if (nonzero(row.getCompensationDays()) || adjusted) { reasons.add("有补偿或手工时长调整"); } if (Boolean.TRUE.equals(row.getIsVip()) || !Objects.equals(row.getAqType(), 1)) { reasons.add("VIP或非普通购买车票"); } if (orders.size() != 1) { reasons.add("有效关联订单不是一笔"); } else { TicketDurationRepairOrder order = orders.get(0); if (!Objects.equals(order.getOrderType(), 1)) { reasons.add("续费或未知订单类型"); } if (!Objects.equals(order.getSkuId(), row.getSkuId())) { reasons.add("订单规格与当前车票不一致"); } if (order.getNum() == null || order.getNum() < 1 || (order.getNum() > 1 && !Boolean.TRUE.equals(order.getSlot()))) { reasons.add("购买数量可能叠加时长"); } } if (!reasons.isEmpty()) { return decision(result, "review", String.join(";", reasons)); } Date newExpiry = DateUtil.offsetMonth(row.getExpiryTime(), -6); result.setNewExpiryTime(newExpiry.getTime()); result.setExpiredAfterRepair(!newExpiry.after(new Date())); return decision(result, "candidate", "可修复:原到期时间减6个自然月"); } private TicketDurationRepairItemResp decision(TicketDurationRepairItemResp result, String decision, String reason) { result.setDecision(decision); result.setReason(reason); return result; } private boolean matches(TicketDurationRepairRow row, TicketDurationRepairReq.Snapshot snapshot) { return Objects.equals(row.getRelationId(), snapshot.getRelationId()) && Objects.equals(row.getGroupsId(), snapshot.getGroupsId()) && Objects.equals(row.getSkuId(), snapshot.getSkuId()) && isTarget(row.getSkuId()) && Objects.equals(row.getUserId(), snapshot.getUserId()) && Objects.equals(millis(row.getStartTime()), snapshot.getStartTime()) && Objects.equals(millis(row.getExpiryTime()), snapshot.getExpiryTime()) && Objects.equals(millis(row.getUpdateTime()), snapshot.getUpdateTime()); } private void validateRequest(TicketDurationRepairReq req, Long adminId) { if (adminId == null || adminId <= 0 || req == null || req.getItems() == null || req.getItems().isEmpty() || req.getItems().size() > 200) { throw BusinessRuntimeException.getInstance("请以后台账号提交1至200张预览中的车票"); } Set ids = new HashSet<>(); for (TicketDurationRepairReq.Snapshot item : req.getItems()) { if (item == null || item.getRelationId() == null || item.getRelationId() <= 0 || item.getGroupsId() == null || item.getGroupsId() <= 0 || item.getUserId() == null || item.getUserId() <= 0 || !isTarget(item.getSkuId()) || item.getStartTime() == null || item.getExpiryTime() == null) { throw BusinessRuntimeException.getInstance("车票快照不完整或规格不属于772/774"); } if (!ids.add(item.getRelationId())) { throw BusinessRuntimeException.getInstance("不能重复提交同一张车票"); } } } private static Long millis(Date date) { return date == null ? null : date.getTime(); } private static boolean nonzero(Integer value) { return value != null && value != 0; } private static boolean isTarget(Long skuId) { return Objects.equals(skuId, 772L) || Objects.equals(skuId, 774L); } }