talent-card-server/src/main/java/com/talentcard/server/service/DependentCardApplyAdminService.java
@@ -2,6 +2,7 @@
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -50,18 +51,27 @@
         Integer status,
         LocalDate applyStart,
         LocalDate applyEnd,
         boolean management,
         int page,
         int size
   ) {
      validateStatusFilter(status);
      if (management && status != null
            && status != DependentCardApply.STATUS_APPROVED
            && status != DependentCardApply.STATUS_CANCELLED) {
         throw new BusinessException(ErrorCode.BAD_REQUEST, "家属卡管理状态只能为正常或已解绑");
      }
      if (applyStart != null && applyEnd != null && applyStart.isAfter(applyEnd)) {
         throw new BusinessException(ErrorCode.BAD_REQUEST, "申请开始时间不能晚于结束时间");
         throw new BusinessException(ErrorCode.BAD_REQUEST,
               management ? "注册开始时间不能晚于结束时间" : "申请开始时间不能晚于结束时间");
      }
      int safePage = Math.max(page, 1);
      int safeSize = Math.min(Math.max(size, 1), 100);
      LambdaQueryWrapper<DependentCardApply> query = Wrappers.<DependentCardApply>lambdaQuery()
            .eq(talentCardId != null, DependentCardApply::getTalentCardId, talentCardId)
            .eq(status != null, DependentCardApply::getStatus, status)
            .in(management && status == null, DependentCardApply::getStatus,
                  DependentCardApply.STATUS_APPROVED, DependentCardApply.STATUS_CANCELLED)
            .eq(cardLevel != null && !cardLevel.isBlank(), DependentCardApply::getCardLevel, trimToNull(cardLevel))
            .and(familyKeyword != null && !familyKeyword.isBlank(), wrapper -> wrapper
                  .like(DependentCardApply::getDependentName, familyKeyword.trim())
@@ -74,16 +84,25 @@
                  .or()
                  .like(DependentCardApply::getTalentMobile, talentKeyword.trim())
                  .or()
                  .like(DependentCardApply::getTalentCardNo, talentKeyword.trim()))
            .ge(applyStart != null, DependentCardApply::getApplyTime, applyStart == null ? null : applyStart.atStartOfDay())
            .lt(applyEnd != null, DependentCardApply::getApplyTime, applyEnd == null ? null : applyEnd.plusDays(1).atStartOfDay());
                  .like(DependentCardApply::getTalentCardNo, talentKeyword.trim())
                  .or()
                  .apply("exists (select 1 from talent_user u where u.id = dependent_card_apply.talent_id and u.deleted = 0 and u.id_card_no like concat('%', {0}, '%'))", talentKeyword.trim()))
            .ge(!management && applyStart != null, DependentCardApply::getApplyTime,
                  applyStart == null ? null : applyStart.atStartOfDay())
            .lt(!management && applyEnd != null, DependentCardApply::getApplyTime,
                  applyEnd == null ? null : applyEnd.plusDays(1).atStartOfDay())
            .ge(management && applyStart != null, DependentCardApply::getAuditTime,
                  applyStart == null ? null : applyStart.atStartOfDay())
            .lt(management && applyEnd != null, DependentCardApply::getAuditTime,
                  applyEnd == null ? null : applyEnd.plusDays(1).atStartOfDay());
      long total = mapper.selectCount(query);
      if (total == 0) {
         return new PageResult<>(List.of(), safePage, safeSize, 0);
      }
      long offset = (long) (safePage - 1) * safeSize;
      List<DependentCardApplyAdminResponse> records = mapper.selectList(
                  query.orderByDesc(DependentCardApply::getApplyTime)
                  query.orderByDesc(management, DependentCardApply::getAuditTime)
                        .orderByDesc(!management, DependentCardApply::getApplyTime)
                        .orderByDesc(DependentCardApply::getId)
                        .last("limit " + safeSize + " offset " + offset)
            ).stream()
@@ -165,6 +184,40 @@
      return DependentCardApplyAdminResponse.from(updatedApply, applicationExtra(updatedApply));
   }
   @Transactional
   public DependentCardApplyAdminResponse unbind(Long id, Long operatorId) {
      DependentCardApply apply = mapper.selectByIdForUpdate(id);
      if (apply == null) {
         throw new BusinessException(ErrorCode.NOT_FOUND, "家属卡不存在");
      }
      if (!Integer.valueOf(DependentCardApply.STATUS_APPROVED).equals(apply.getStatus())) {
         throw new BusinessException(ErrorCode.CONFLICT, "仅正常状态的家属卡可以解除绑定");
      }
      int updated = mapper.update(
            null,
            Wrappers.<DependentCardApply>lambdaUpdate()
                  .eq(DependentCardApply::getId, id)
                  .eq(DependentCardApply::getStatus, DependentCardApply.STATUS_APPROVED)
                  .set(DependentCardApply::getStatus, DependentCardApply.STATUS_CANCELLED)
      );
      if (updated != 1) {
         throw new BusinessException(ErrorCode.CONFLICT, "家属卡状态已变化,请刷新后重试");
      }
      if (statusLogMapper.insert(new DependentCardApplyStatusLog(
            apply.getId(),
            apply.getApplyNo(),
            DependentCardApply.STATUS_APPROVED,
            DependentCardApply.STATUS_CANCELLED,
            "ADMIN",
            operatorId,
            "管理后台解除绑定"
      )) != 1) {
         throw new BusinessException(ErrorCode.INTERNAL_ERROR, "解绑状态记录保存失败");
      }
      DependentCardApply updatedApply = mapper.selectById(id);
      return DependentCardApplyAdminResponse.from(updatedApply, applicationExtra(updatedApply));
   }
   private Map<String, Object> applicationExtra(DependentCardApply apply) {
      if (apply == null || apply.getTalentCardId() == null) {
         return Map.of();
@@ -178,7 +231,7 @@
                   c.valid_start as validStart,
                   c.valid_end as validEnd,
                   c.issue_time as issueTime,
                   c.family_card_limit as familyCardLimit,
                   c.card_level as cardLevel,
                   (select count(*) from dependent_card_apply approved
                      where approved.deleted = 0
                        and approved.talent_card_id = a.talent_card_id
@@ -190,7 +243,26 @@
            where a.id = ? and a.deleted = 0
            limit 1
            """, apply.getId());
      return rows.isEmpty() ? Map.of() : rows.get(0);
      if (rows.isEmpty()) {
         return Map.of();
      }
      Map<String, Object> extra = new LinkedHashMap<>(rows.get(0));
      // 名额按真正生效的规则算:审核/申请侧也是这个口径。之前直接读 talent_card.family_card_limit,
      // 那一列在库里基本是 0(按卡配置那条路没启用),页面就成了「1/0」。
      extra.put("familyCardLimit", familyCardLimit(string(extra.get("cardLevel"))));
      return extra;
   }
   /**
    * 家属附属卡名额:卓越人才卡 2 个({@link #MAX_FAMILY_CARD_COUNT}),其它等级没有名额。
    * 展示与限流都用这一个口径,避免「页面显示 0、实际能绑 2 个」。
    */
   private int familyCardLimit(String cardLevel) {
      return cardLevel != null && ELITE_CARD_LEVEL.equals(cardLevel.trim()) ? MAX_FAMILY_CARD_COUNT : 0;
   }
   private String string(Object value) {
      return value == null ? "" : String.valueOf(value).trim();
   }
   private void ensureApprovalSlot(DependentCardApply apply) {
@@ -205,7 +277,7 @@
//      if (configuredLimit == null || configuredLimit <= 0) {
//         throw new BusinessException(ErrorCode.CONFLICT, "该主卡未配置家属附属卡名额");
//      }
      int familyCardLimit = Math.min(2, MAX_FAMILY_CARD_COUNT);
      int familyCardLimit = familyCardLimit(ownerCard.getCardLevel());
      long approvedCount = mapper.selectCount(
            Wrappers.<DependentCardApply>lambdaQuery()
                  .eq(DependentCardApply::getTalentCardId, apply.getTalentCardId())