wenminjie
2026-08-12 7361c250cee0a13724bd4ac613a39e57b8576ec5
talent-card-server/src/main/java/com/talentcard/server/service/TicketPackageAdminService.java
@@ -5,12 +5,14 @@
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
@@ -22,11 +24,16 @@
import com.talentcard.server.entity.TicketPackage;
import com.talentcard.server.entity.TicketPackageClaim;
import com.talentcard.server.entity.TicketPackageCode;
import com.talentcard.server.entity.TicketPackageLevelRule;
import com.talentcard.server.entity.TicketPackageMerchant;
import com.talentcard.server.mapper.BenefitMerchantMapper;
import com.talentcard.server.mapper.TicketPackageClaimMapper;
import com.talentcard.server.mapper.TicketPackageCodeMapper;
import com.talentcard.server.mapper.TicketPackageLevelRuleMapper;
import com.talentcard.server.mapper.TicketPackageMapper;
import com.talentcard.server.mapper.TicketPackageMerchantMapper;
import com.talentcard.server.request.TicketPackageCodeImportRequest;
import com.talentcard.server.request.TicketPackageCodeGenerateRequest;
import com.talentcard.server.request.TicketPackageCodeLockRequest;
import com.talentcard.server.request.TicketPackageCodeStatusRequest;
import com.talentcard.server.request.TicketPackageSupplementRequest;
@@ -50,26 +57,32 @@
    private final TicketPackageCodeMapper codeMapper;
    private final TicketPackageClaimMapper claimMapper;
    private final BenefitMerchantMapper merchantMapper;
    private final TicketPackageMerchantMapper packageMerchantMapper;
    private final TicketPackageLevelRuleMapper levelRuleMapper;
    public TicketPackageAdminService(
            TicketPackageMapper packageMapper,
            TicketPackageCodeMapper codeMapper,
            TicketPackageClaimMapper claimMapper,
            BenefitMerchantMapper merchantMapper
            BenefitMerchantMapper merchantMapper,
            TicketPackageMerchantMapper packageMerchantMapper,
            TicketPackageLevelRuleMapper levelRuleMapper
    ) {
        this.packageMapper = packageMapper;
        this.codeMapper = codeMapper;
        this.claimMapper = claimMapper;
        this.merchantMapper = merchantMapper;
        this.packageMerchantMapper = packageMerchantMapper;
        this.levelRuleMapper = levelRuleMapper;
    }
    @Transactional(readOnly = true)
    public PageResult<Map<String, Object>> packages(Long merchantId, Integer status, String keyword, int page, int size) {
    public PageResult<Map<String, Object>> packages(Long merchantId, Integer status, String keyword, String merchantKeyword, int page, int size) {
        int safePage = Math.max(page, 1);
        int safeSize = Math.min(Math.max(size, 1), 100);
        Integer normalizedStatus = normalizeStatusFilter(status);
        Long total = packageMapper.countAdminPage(merchantId, normalizedStatus, clean(keyword));
        List<Map<String, Object>> records = packageMapper.selectAdminPage(merchantId, normalizedStatus, clean(keyword), safeSize, (safePage - 1) * safeSize)
        Long total = packageMapper.countAdminPage(merchantId, normalizedStatus, clean(keyword), clean(merchantKeyword));
        List<Map<String, Object>> records = packageMapper.selectAdminPage(merchantId, normalizedStatus, clean(keyword), clean(merchantKeyword), safeSize, (safePage - 1) * safeSize)
                .stream()
                .map(this::packageView)
                .toList();
@@ -83,19 +96,21 @@
    @Transactional
    public Map<String, Object> createPackage(TicketPackageSaveRequest request) {
        validateMerchant(request.merchantId());
        List<Long> merchantIds = validateMerchants(request);
        TicketPackage entity = new TicketPackage();
        entity.setPackageNo(BusinessIdGenerator.next("TP"));
        apply(entity, request);
        entity.setDeleted(0);
        packageMapper.insert(entity);
        savePackageMerchants(entity.getId(), merchantIds);
        saveLevelRules(entity.getId(), request);
        return packageDetail(entity.getId());
    }
    @Transactional
    public Map<String, Object> updatePackage(Long id, TicketPackageSaveRequest request) {
        Long packageId = requireId(id, "票包ID不能为空");
        validateMerchant(request.merchantId());
        List<Long> merchantIds = validateMerchants(request);
        TicketPackage current = packageMapper.selectByIdForUpdate(packageId);
        if (current == null) {
            throw new BusinessException(ErrorCode.NOT_FOUND, "票包不存在");
@@ -105,6 +120,8 @@
        if (updated != 1) {
            throw new BusinessException(ErrorCode.CONFLICT, "票包配置已变化,请刷新后重试");
        }
        savePackageMerchants(packageId, merchantIds);
        saveLevelRules(packageId, request);
        return packageDetail(packageId);
    }
@@ -166,6 +183,57 @@
    }
    @Transactional
    public Map<String, Object> generateCodes(Long packageId, TicketPackageCodeGenerateRequest request) {
        TicketPackage ticketPackage = requirePackage(packageId);
        int target = Math.max(request.codeCount() == null ? 0 : request.codeCount(), 0);
        if (target < 1) {
            throw new BusinessException(ErrorCode.BAD_REQUEST, "生成数量不能为空");
        }
        String batchNo = clean(request.batchNo()).isBlank() ? BusinessIdGenerator.next("TPC") : clean(request.batchNo());
        String remark = clean(request.remark());
        String codePrefix = normalizedGeneratedCodePrefix(request.codePrefix());
        Set<String> generatedCodes = new LinkedHashSet<>();
        int imported = 0;
        int duplicate = 0;
        int attempts = 0;
        int maxAttempts = Math.max(target * 20, 100);
        while (imported < target && attempts < maxAttempts) {
            attempts++;
            String exchangeCode = buildGeneratedExchangeCode(codePrefix);
            if (!generatedCodes.add(exchangeCode)) {
                duplicate++;
                continue;
            }
            TicketPackageCode entity = new TicketPackageCode();
            entity.setPackageId(ticketPackage.getId());
            entity.setExchangeCode(exchangeCode);
            entity.setBatchNo(batchNo);
            entity.setStatus("AVAILABLE");
            entity.setRemark(remark);
            entity.setDeleted(0);
            try {
                codeMapper.insert(entity);
                imported++;
            } catch (DuplicateKeyException exception) {
                duplicate++;
                generatedCodes.remove(exchangeCode);
            }
        }
        if (imported < target) {
            throw new BusinessException(ErrorCode.CONFLICT, "兑换码生成失败,请重试");
        }
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("packageId", ticketPackage.getId());
        result.put("batchNo", batchNo);
        result.put("submittedCount", target);
        result.put("distinctCount", generatedCodes.size());
        result.put("importedCount", imported);
        result.put("duplicateCount", duplicate);
        result.put("stock", stock(ticketPackage.getId()));
        return result;
    }
    @Transactional
    public Map<String, Object> importCodeFile(Long packageId, MultipartFile file, String batchNo, String remark) {
        if (file == null || file.isEmpty()) {
            throw new BusinessException(ErrorCode.BAD_REQUEST, "兑换码文件不能为空");
@@ -180,12 +248,12 @@
    }
    @Transactional(readOnly = true)
    public PageResult<Map<String, Object>> codes(Long packageId, String status, String batchNo, String keyword, int page, int size) {
    public PageResult<Map<String, Object>> codes(Long packageId, String status, String batchNo, String keyword, String packageKeyword, String merchantKeyword, int page, int size) {
        int safePage = Math.max(page, 1);
        int safeSize = Math.min(Math.max(size, 1), 100);
        String normalizedStatus = normalizeCodeStatusFilter(status);
        Long total = codeMapper.countAdminPage(packageId, normalizedStatus, clean(batchNo), clean(keyword));
        List<Map<String, Object>> records = codeMapper.selectAdminPage(packageId, normalizedStatus, clean(batchNo), clean(keyword), safeSize, (safePage - 1) * safeSize)
        Long total = codeMapper.countAdminPage(packageId, normalizedStatus, clean(batchNo), clean(keyword), clean(packageKeyword), clean(merchantKeyword));
        List<Map<String, Object>> records = codeMapper.selectAdminPage(packageId, normalizedStatus, clean(batchNo), clean(keyword), clean(packageKeyword), clean(merchantKeyword), safeSize, (safePage - 1) * safeSize)
                .stream()
                .map(this::codeView)
                .toList();
@@ -225,16 +293,16 @@
                    .eq("id", current.getIssuedClaimId())
                    .eq("deleted", 0));
        }
        return codeView(codeMapper.selectAdminPage(current.getPackageId(), "", "", current.getExchangeCode(), 1, 0).stream().findFirst().orElse(Map.of()));
        return codeView(codeMapper.selectAdminPage(current.getPackageId(), "", "", current.getExchangeCode(), "", "", 1, 0).stream().findFirst().orElse(Map.of()));
    }
    @Transactional(readOnly = true)
    public PageResult<Map<String, Object>> claims(Long packageId, Long talentId, String status, String keyword, int page, int size) {
    public PageResult<Map<String, Object>> claims(Long packageId, Long talentId, String status, String keyword, String packageKeyword, String talentKeyword, String cardLevel, LocalDate claimedStart, LocalDate claimedEnd, int page, int size) {
        int safePage = Math.max(page, 1);
        int safeSize = Math.min(Math.max(size, 1), 100);
        String normalizedStatus = normalizeClaimStatusFilter(status);
        Long total = claimMapper.countAdminPage(packageId, talentId, normalizedStatus, clean(keyword));
        List<Map<String, Object>> records = claimMapper.selectAdminPage(packageId, talentId, normalizedStatus, clean(keyword), safeSize, (safePage - 1) * safeSize)
        Long total = claimMapper.countAdminPage(packageId, talentId, normalizedStatus, clean(keyword), clean(packageKeyword), clean(talentKeyword), clean(cardLevel), claimedStart, claimedEnd);
        List<Map<String, Object>> records = claimMapper.selectAdminPage(packageId, talentId, normalizedStatus, clean(keyword), clean(packageKeyword), clean(talentKeyword), clean(cardLevel), claimedStart, claimedEnd, safeSize, (safePage - 1) * safeSize)
                .stream()
                .map(this::claimView)
                .toList();
@@ -478,13 +546,28 @@
        String text = clean(value).toUpperCase(Locale.ROOT);
        return List.of("兑换码", "券码", "码", "EXCHANGE_CODE", "CODE").contains(text);
    }
    private String normalizedGeneratedCodePrefix(String codePrefix) {
        String prefix = clean(codePrefix).replaceAll("[^A-Za-z0-9]", "").toUpperCase(Locale.ROOT);
        if (prefix.isBlank()) {
            return "EX";
        }
        return prefix.length() > 16 ? prefix.substring(0, 16) : prefix;
    }
    private String buildGeneratedExchangeCode(String codePrefix) {
        return normalizedGeneratedCodePrefix(codePrefix) + UUID.randomUUID().toString().replace("-", "").toUpperCase(Locale.ROOT);
    }
    private void apply(TicketPackage entity, TicketPackageSaveRequest request) {
        entity.setPackageName(cleanRequired(request.packageName(), "票包名称不能为空"));
        entity.setMerchantId(request.merchantId());
        List<Long> merchantIds = normalizedMerchantIds(request);
        entity.setTotalQuantity(Math.max(request.totalQuantity() == null ? 0 : request.totalQuantity(), 0));
        entity.setMerchantId(merchantIds.isEmpty() ? null : merchantIds.get(0));
        entity.setCoverUrl(clean(request.coverUrl()));
        entity.setTalentLevels(joinTalentLevels(request.talentLevels()));
        entity.setTalentLevels(joinTalentLevels(levelsFromRules(request)));
        entity.setClaimPrice(request.claimPrice() == null ? BigDecimal.ZERO : request.claimPrice());
        entity.setPerUserLimit(request.perUserLimit() == null ? 1 : request.perUserLimit());
        entity.setPerUserLimit(defaultPerUserLimit(request));
        entity.setValidStart(request.validStart());
        entity.setValidEnd(request.validEnd());
        if (entity.getValidStart() != null && entity.getValidEnd() != null && entity.getValidEnd().isBefore(entity.getValidStart())) {
@@ -502,10 +585,19 @@
        result.put("id", value(row, "id"));
        result.put("packageNo", text(row, "packageNo", "package_no"));
        result.put("packageName", text(row, "packageName", "package_name"));
        result.put("totalQuantity", intValue(value(row, "totalQuantity", "total_quantity")));
        result.put("merchantId", value(row, "merchantId", "merchant_id"));
        result.put("merchantName", text(row, "merchantName", "merchant_name"));
        List<Long> merchantIds = splitLongs(text(row, "merchantIdsText", "merchant_ids_text"));
        Long legacyMerchantId = longValue(row, "merchantId", "merchant_id");
        if (merchantIds.isEmpty() && legacyMerchantId != null) {
            merchantIds = List.of(legacyMerchantId);
        }
        result.put("merchantIds", merchantIds);
        result.put("merchantNames", text(row, "merchantNames", "merchant_names"));
        result.put("coverUrl", text(row, "coverUrl", "cover_url"));
        result.put("talentLevels", splitLevels(text(row, "talentLevels", "talent_levels")));
        result.put("levelRules", parseLevelRules(text(row, "levelRulesText", "level_rules_text")));
        result.put("claimPrice", value(row, "claimPrice", "claim_price"));
        result.put("perUserLimit", intValue(value(row, "perUserLimit", "per_user_limit")));
        result.put("validStart", value(row, "validStart", "valid_start"));
@@ -521,6 +613,8 @@
        result.put("issuedCodes", intValue(value(row, "issuedCodes", "issued_codes")));
        result.put("usedCodes", intValue(value(row, "usedCodes", "used_codes")));
        result.put("voidCodes", intValue(value(row, "voidCodes", "void_codes")));
        result.put("claimedCount", intValue(value(row, "claimedCount", "claimed_count")));
        result.put("remainingCount", Math.max(intValue(value(row, "totalQuantity", "total_quantity")) - intValue(result.get("claimedCount")), 0));
        result.put("createdAt", value(row, "createdAt", "created_at"));
        result.put("updatedAt", value(row, "updatedAt", "updated_at"));
        return result;
@@ -530,13 +624,19 @@
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("id", value(row, "id"));
        result.put("packageId", value(row, "packageId", "package_id"));
        result.put("codeId", value(row, "id"));
        result.put("packageNo", text(row, "packageNo", "package_no"));
        result.put("packageName", text(row, "packageName", "package_name"));
        result.put("merchantName", text(row, "merchantName", "merchant_name"));
        result.put("merchantNames", text(row, "merchantNames", "merchant_names"));
        result.put("exchangeCode", text(row, "exchangeCode", "exchange_code"));
        result.put("batchNo", text(row, "batchNo", "batch_no"));
        result.put("status", text(row, "status"));
        result.put("statusName", codeStatusName(text(row, "status")));
        result.put("lockedByTalentId", value(row, "lockedByTalentId", "locked_by_talent_id"));
        result.put("lockedByCardId", value(row, "lockedByCardId", "locked_by_card_id"));
        result.put("lockedTalentName", text(row, "lockedTalentName", "locked_talent_name"));
        result.put("lockedTalentMobile", text(row, "lockedTalentMobile", "locked_talent_mobile"));
        result.put("lockedAt", value(row, "lockedAt", "locked_at"));
        result.put("issuedClaimId", value(row, "issuedClaimId", "issued_claim_id"));
        result.put("issuedAt", value(row, "issuedAt", "issued_at"));
@@ -555,6 +655,7 @@
        result.put("packageName", text(row, "packageName", "package_name"));
        result.put("merchantId", value(row, "merchantId", "merchant_id"));
        result.put("merchantName", text(row, "merchantName", "merchant_name"));
        result.put("merchantNames", text(row, "merchantNames", "merchant_names"));
        result.put("talentId", value(row, "talentId", "talent_id"));
        result.put("cardId", value(row, "cardId", "card_id"));
        result.put("cardNo", text(row, "cardNo", "card_no"));
@@ -564,6 +665,11 @@
        result.put("exchangeCode", text(row, "exchangeCode", "exchange_code"));
        result.put("status", text(row, "status"));
        result.put("statusName", claimStatusName(text(row, "status")));
        result.put("claimStatus", text(row, "status"));
        result.put("claimStatusName", claimStatusName(text(row, "status")));
        result.put("useStatus", "USED".equalsIgnoreCase(text(row, "status")) ? "USED" : "ACTIVE");
        result.put("useStatusName", "USED".equalsIgnoreCase(text(row, "status")) ? "已使用" : "未使用");
        result.put("claimedAt", value(row, "createdAt", "created_at"));
        result.put("validEnd", value(row, "validEnd", "valid_end"));
        result.put("source", claimSource(text(row, "idempotencyKey")));
        result.put("sourceName", "MANUAL".equals(claimSource(text(row, "idempotencyKey"))) ? "后台补录" : "线上领取");
@@ -594,17 +700,149 @@
        return ticketPackage;
    }
    private void validateMerchant(Long merchantId) {
        if (merchantId == null) {
            return;
    private List<Long> validateMerchants(TicketPackageSaveRequest request) {
        List<Long> merchantIds = normalizedMerchantIds(request);
        for (Long merchantId : merchantIds) {
            BenefitMerchant merchant = merchantMapper.selectOne(new QueryWrapper<BenefitMerchant>()
                    .eq("id", merchantId)
                    .eq("deleted", 0)
                    .last("limit 1"));
            if (merchant == null) {
                throw new BusinessException(ErrorCode.NOT_FOUND, "指定商家不存在");
            }
        }
        BenefitMerchant merchant = merchantMapper.selectOne(new QueryWrapper<BenefitMerchant>()
                .eq("id", merchantId)
                .eq("deleted", 0)
                .last("limit 1"));
        if (merchant == null) {
            throw new BusinessException(ErrorCode.NOT_FOUND, "指定商家不存在");
        return merchantIds;
    }
    private List<Long> normalizedMerchantIds(TicketPackageSaveRequest request) {
        List<Long> result = new ArrayList<>();
        if (request.merchantIds() != null) {
            for (Long merchantId : request.merchantIds()) {
                if (merchantId != null && merchantId > 0 && !result.contains(merchantId)) {
                    result.add(merchantId);
                }
            }
        }
        if (result.isEmpty() && request.merchantId() != null && request.merchantId() > 0) {
            result.add(request.merchantId());
        }
        return result;
    }
    private void savePackageMerchants(Long packageId, List<Long> merchantIds) {
        UpdateWrapper<TicketPackageMerchant> clear = new UpdateWrapper<TicketPackageMerchant>()
                .set("deleted", 1)
                .eq("package_id", packageId)
                .eq("deleted", 0);
        if (!merchantIds.isEmpty()) {
            clear.notIn("merchant_id", merchantIds);
        }
        packageMerchantMapper.update(null, clear);
        int sortNo = 0;
        for (Long merchantId : merchantIds) {
            TicketPackageMerchant current = packageMerchantMapper.selectOne(new QueryWrapper<TicketPackageMerchant>()
                    .eq("package_id", packageId)
                    .eq("merchant_id", merchantId)
                    .last("limit 1"));
            if (current == null) {
                TicketPackageMerchant row = new TicketPackageMerchant();
                row.setPackageId(packageId);
                row.setMerchantId(merchantId);
                row.setSortNo(sortNo++);
                row.setDeleted(0);
                packageMerchantMapper.insert(row);
            } else {
                packageMerchantMapper.update(null, new UpdateWrapper<TicketPackageMerchant>()
                        .set("sort_no", sortNo++)
                        .set("deleted", 0)
                        .eq("id", current.getId()));
            }
        }
    }
    private void saveLevelRules(Long packageId, TicketPackageSaveRequest request) {
        List<TicketPackageSaveRequest.TicketPackageLevelRuleRequest> rules = levelRules(request);
        List<String> ruleLevels = rules.stream()
                .map(rule -> normalizeCardLevel(rule.talentLevel()))
                .filter(level -> !level.isBlank())
                .distinct()
                .toList();
        UpdateWrapper<TicketPackageLevelRule> clear = new UpdateWrapper<TicketPackageLevelRule>()
                .set("deleted", 1)
                .eq("package_id", packageId)
                .eq("deleted", 0);
        if (!ruleLevels.isEmpty()) {
            clear.notIn("talent_level", ruleLevels);
        }
        levelRuleMapper.update(null, clear);
        for (TicketPackageSaveRequest.TicketPackageLevelRuleRequest rule : rules) {
            String level = normalizeCardLevel(rule.talentLevel());
            if (!CARD_LEVELS.contains(level)) {
                throw new BusinessException(ErrorCode.BAD_REQUEST, "适用人才卡等级仅支持卓越人才卡、杰出人才卡、菁英人才卡");
            }
            int enabled = rule.enabled() == null ? 1 : normalizeStatus(rule.enabled());
            int claimableCount = Math.max(rule.claimableCount() == null ? 1 : rule.claimableCount(), 1);
            String resetCycle = normalizeResetCycle(rule.resetCycle());
            TicketPackageLevelRule current = levelRuleMapper.selectOne(new QueryWrapper<TicketPackageLevelRule>()
                    .eq("package_id", packageId)
                    .eq("talent_level", level)
                    .last("limit 1"));
            if (current == null) {
                TicketPackageLevelRule entity = new TicketPackageLevelRule();
                entity.setPackageId(packageId);
                entity.setTalentLevel(level);
                entity.setEnabled(enabled);
                entity.setClaimableCount(claimableCount);
                entity.setResetCycle(resetCycle);
                entity.setDeleted(0);
                levelRuleMapper.insert(entity);
                continue;
            }
            TicketPackageLevelRule entity = new TicketPackageLevelRule();
            entity.setEnabled(enabled);
            entity.setClaimableCount(claimableCount);
            entity.setResetCycle(resetCycle);
            entity.setDeleted(0);
            levelRuleMapper.update(entity, new UpdateWrapper<TicketPackageLevelRule>().eq("id", current.getId()));
        }
    }
    private List<TicketPackageSaveRequest.TicketPackageLevelRuleRequest> levelRules(TicketPackageSaveRequest request) {
        if (request.levelRules() != null && !request.levelRules().isEmpty()) {
            return request.levelRules();
        }
        List<String> levels = request.talentLevels() == null || request.talentLevels().isEmpty() ? CARD_LEVELS : request.talentLevels();
        List<TicketPackageSaveRequest.TicketPackageLevelRuleRequest> result = new ArrayList<>();
        for (String level : CARD_LEVELS) {
            int enabled = levels.stream().anyMatch(item -> normalizeCardLevel(item).equals(level)) ? 1 : 0;
            result.add(new TicketPackageSaveRequest.TicketPackageLevelRuleRequest(level, enabled,
                    request.perUserLimit() == null ? 1 : request.perUserLimit(), "每天"));
        }
        return result;
    }
    private List<String> levelsFromRules(TicketPackageSaveRequest request) {
        List<String> result = new ArrayList<>();
        for (TicketPackageSaveRequest.TicketPackageLevelRuleRequest rule : levelRules(request)) {
            if (rule.enabled() == null || rule.enabled() == 1) {
                String level = normalizeCardLevel(rule.talentLevel());
                if (!result.contains(level)) {
                    result.add(level);
                }
            }
        }
        return result;
    }
    private int defaultPerUserLimit(TicketPackageSaveRequest request) {
        int limit = request.perUserLimit() == null ? 1 : request.perUserLimit();
        for (TicketPackageSaveRequest.TicketPackageLevelRuleRequest rule : levelRules(request)) {
            if (rule.enabled() == null || rule.enabled() == 1) {
                limit = Math.max(rule.claimableCount() == null ? limit : rule.claimableCount(), 1);
                break;
            }
        }
        return Math.max(limit, 1);
    }
    private String joinTalentLevels(List<String> levels) {
@@ -617,6 +855,7 @@
            if (value.isBlank()) {
                continue;
            }
            value = normalizeCardLevel(value);
            if (!CARD_LEVELS.contains(value)) {
                throw new BusinessException(ErrorCode.BAD_REQUEST, "适用人才卡等级仅支持卓越人才卡、杰出人才卡、菁英人才卡");
            }
@@ -633,6 +872,106 @@
            return List.of();
        }
        return List.of(text.split("[、,,]"));
    }
    private List<Long> splitLongs(String value) {
        String text = clean(value);
        if (text.isBlank()) {
            return List.of();
        }
        List<Long> result = new ArrayList<>();
        for (String part : text.split(",")) {
            try {
                Long id = Long.valueOf(part.trim());
                if (id > 0 && !result.contains(id)) {
                    result.add(id);
                }
            } catch (NumberFormatException ignored) {
                // Ignore dirty historical data in aggregate text.
            }
        }
        return result;
    }
    private List<Map<String, Object>> parseLevelRules(String value) {
        Map<String, Map<String, Object>> byLevel = new HashMap<>();
        String text = clean(value);
        if (!text.isBlank()) {
            for (String item : text.split(";;")) {
                String[] parts = item.split("\\|", -1);
                if (parts.length < 4) {
                    continue;
                }
                String level = normalizeCardLevel(parts[0]);
                if (!CARD_LEVELS.contains(level)) {
                    continue;
                }
                Map<String, Object> row = new LinkedHashMap<>();
                row.put("talentLevel", level);
                row.put("enabled", parseInt(parts[1], 1));
                row.put("claimableCount", Math.max(parseInt(parts[2], 1), 1));
                row.put("resetCycle", normalizeResetCycle(parts[3]));
                byLevel.put(level, row);
            }
        }
        List<Map<String, Object>> result = new ArrayList<>();
        for (String level : CARD_LEVELS) {
            result.add(byLevel.getOrDefault(level, defaultLevelRule(level)));
        }
        return result;
    }
    private Map<String, Object> defaultLevelRule(String level) {
        Map<String, Object> row = new LinkedHashMap<>();
        row.put("talentLevel", level);
        row.put("enabled", 1);
        row.put("claimableCount", 1);
        row.put("resetCycle", "每天");
        return row;
    }
    private int parseInt(String value, int fallback) {
        try {
            return Integer.parseInt(clean(value));
        } catch (NumberFormatException exception) {
            return fallback;
        }
    }
    private String normalizeCardLevel(String level) {
        String value = clean(level);
        String upper = value.toUpperCase(Locale.ROOT);
        if (upper.contains("EXCELLENT") || value.contains("卓越")) {
            return "卓越人才卡";
        }
        if (upper.contains("OUTSTANDING") || value.contains("杰出")) {
            return "杰出人才卡";
        }
        if (upper.contains("ELITE") || value.contains("菁英")) {
            return "菁英人才卡";
        }
        return value;
    }
    private String normalizeResetCycle(String resetCycle) {
        String value = clean(resetCycle);
        if (value.isBlank()) {
            return "每天";
        }
        String upper = value.toUpperCase(Locale.ROOT);
        if (upper.contains("DAY") || value.contains("天") || value.contains("日")) {
            return "每天";
        }
        if (upper.contains("WEEK") || value.contains("周") || value.contains("星期")) {
            return "每周";
        }
        if (upper.contains("MONTH") || value.contains("月")) {
            return "每月";
        }
        if (upper.contains("YEAR") || upper.contains("ANNUAL") || value.contains("年")) {
            return "每年";
        }
        return value;
    }
    private int normalizeStatus(Integer status) {
@@ -950,4 +1289,4 @@
    private String clean(String value) {
        return value == null ? "" : value.trim();
    }
}
}