shinnosuke
2026-08-18 69138ddcf4936098cb3b67688412badfc8f503a0
talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessService.java
@@ -2,9 +2,12 @@
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -228,21 +231,7 @@
      StringBuilder sql = new StringBuilder("""
            select m.id, m.merchant_name, m.merchant_type, m.contact_name, m.contact_mobile,
                   m.logo_url, m.cover_url, m.address, m.is_recommend, m.sort_no, m.status,
                   case
                     when m.status <> 1 then '休息中'
                     when lower(coalesce(m.business_status, 'open')) in ('closed', '休息中') then '休息中'
                     when exists (
                       select 1
                       from merchant_business_hours h
                       join merchant_store s on s.id = h.store_id and s.status = 1 and s.deleted = 0
                       where h.merchant_id = m.id
                         and (
                           (h.start_time <= h.end_time and current_time() >= h.start_time and current_time() < h.end_time)
                           or (h.start_time > h.end_time and (current_time() >= h.start_time or current_time() < h.end_time))
                         )
                     ) then '营业中'
                     else '休息中'
                   end as business_status, m.created_at,
                   m.created_at,
                   (select count(*) from merchant_product p where p.merchant_id = m.id and p.deleted = 0) as product_count,
                   (select count(*) from benefit_order o where o.merchant_id = m.id and o.deleted = 0) as order_count,
                   0 as reservation_count
@@ -290,7 +279,45 @@
         args.add(createdEnd);
      }
      sql.append(" order by m.is_recommend desc, m.sort_no desc, m.id desc");
      return page(sql.toString(), countSql.toString(), page, size, args.toArray());
      PageResult<Map<String, Object>> result = page(sql.toString(), countSql.toString(), page, size, args.toArray());
      applyBusinessStatus(result.records());
      return result;
   }
   @Transactional(readOnly = true)
   public List<Map<String, Object>> pendingNotices() {
      List<Map<String, Object>> notices = new ArrayList<>();
      addPendingNotice(notices, "product-audit", "团单审核", "您有 %d 条团单待审核", "/products",
            "select count(*) as pending_count, max(coalesce(submit_time, created_at)) as latest_time from merchant_product where deleted = 0 and audit_status = 1");
      addPendingNotice(notices, "family-card", "家属卡申请", "您有 %d 条家属卡申请待审核", "/family-card-applications",
            "select count(*) as pending_count, max(apply_time) as latest_time from family_card_application where deleted = 0 and status = '待审核'");
      addPendingNotice(notices, "upgrade", "升级申请", "您有 %d 条升级申请待审核", "/upgrade-applications",
            "select count(*) as pending_count, max(apply_time) as latest_time from upgrade_application where deleted = 0 and status = '待审核'");
      addPendingNotice(notices, "merchant-onboarding", "商户进件", "您有 %d 条商户进件待审核", "/merchant-settlement?kind=onboardings",
            "select count(*) as pending_count, max(created_at) as latest_time from merchant_onboarding_application where status = 'pending'");
      addPendingNotice(notices, "merchant-bank-card", "银行卡申请", "您有 %d 条银行卡申请待审核", "/merchant-settlement?kind=bank-cards",
            "select count(*) as pending_count, max(created_at) as latest_time from merchant_bank_card where status = 'pending'");
      addPendingNotice(notices, "merchant-withdrawal", "提现审核", "您有 %d 条提现申请待审核", "/merchant-settlement?kind=withdrawals",
            "select count(*) as pending_count, max(created_at) as latest_time from merchant_withdrawal where status = 'pending'");
      addPendingNotice(notices, "medical-reservation", "体检预约", "您有 %d 条体检预约待处理", "/medical-reservations",
            "select count(*) as pending_count, max(submitted_at) as latest_time from service_application where deleted = 0 and application_type = 'MEDICAL_RESERVATION' and status = '待处理'");
      addPendingNotice(notices, "medical-application", "就医申请", "您有 %d 条就医申请待处理", "/medical-applications",
            "select count(*) as pending_count, max(submitted_at) as latest_time from service_application where deleted = 0 and application_type = 'MEDICAL_APPLICATION' and status = '待处理'");
      notices.sort((left, right) -> String.valueOf(right.get("time")).compareTo(String.valueOf(left.get("time"))));
      return notices;
   }
   private void addPendingNotice(List<Map<String, Object>> notices, String id, String category, String template, String path, String sql) {
      Map<String, Object> summary = DatabaseRows.normalize(jdbcTemplate.queryForMap(sql));
      long count = summary.get("pendingCount") instanceof Number number ? number.longValue() : 0;
      if (count == 0) return;
      Map<String, Object> notice = new LinkedHashMap<>();
      notice.put("id", id);
      notice.put("category", category);
      notice.put("content", template.formatted(count));
      notice.put("time", summary.get("latestTime"));
      notice.put("path", path);
      notices.add(notice);
   }
   @Transactional(readOnly = true)
@@ -335,7 +362,7 @@
   @Transactional
   public Map<String, Object> createMerchant(MerchantCreateRequest request) {
      validateMerchantAddress(request.merchantType(), request.address());
      validateScenicMerchantCreate(request);
      validateRequiredMerchantCreateFields(request);
      jdbcTemplate.update("""
            insert into merchant(merchant_name, merchant_type, contact_name, contact_mobile, address,
                                    logo_url, cover_url, business_status, longitude, latitude, can_login_merchant_app, group_buying_enabled,
@@ -396,8 +423,8 @@
      return MERCHANT_APP_LOGIN_TYPES.contains(merchantType);
   }
   private void validateScenicMerchantCreate(MerchantCreateRequest request) {
      if (!"景区服务".equals(request.merchantType())) {
   private void validateRequiredMerchantCreateFields(MerchantCreateRequest request) {
      if (!Set.of("景区服务", "餐饮服务").contains(request.merchantType())) {
         return;
      }
      if (request.businessHours() == null || request.businessHours().isEmpty()
@@ -907,6 +934,41 @@
      return new PageResult<>(records, safePage, safeSize, total == null ? 0 : total);
   }
   private void applyBusinessStatus(List<Map<String, Object>> merchants) {
      if (merchants.isEmpty()) return;
      List<Long> merchantIds = merchants.stream().map(row -> Long.valueOf(String.valueOf(row.get("id")))).toList();
      String placeholders = String.join(",", java.util.Collections.nCopies(merchantIds.size(), "?"));
      List<Map<String, Object>> periods = DatabaseRows.normalize(jdbcTemplate.queryForList("""
            select h.merchant_id, h.start_time, h.end_time
            from merchant_business_hours h
            join merchant_store s on s.id = h.store_id
            where s.status = 1 and s.deleted = 0 and h.merchant_id in (""" + placeholders + ")", merchantIds.toArray()));
      LocalTime now = LocalTime.now(ZoneId.of("Asia/Shanghai"));
      Set<Long> openMerchantIds = new HashSet<>();
      for (Map<String, Object> period : periods) {
         if (isWithinBusinessPeriod(now, String.valueOf(period.get("startTime")), String.valueOf(period.get("endTime")))) {
            openMerchantIds.add(Long.valueOf(String.valueOf(period.get("merchantId"))));
         }
      }
      for (Map<String, Object> merchant : merchants) {
         Long merchantId = Long.valueOf(String.valueOf(merchant.get("id")));
         boolean enabled = "1".equals(String.valueOf(merchant.get("status")));
         merchant.put("businessStatus", enabled && openMerchantIds.contains(merchantId) ? "营业中" : "休息中");
      }
   }
   private boolean isWithinBusinessPeriod(LocalTime now, String startValue, String endValue) {
      try {
         LocalTime start = LocalTime.parse(startValue);
         LocalTime end = LocalTime.parse(endValue);
         return start.isBefore(end)
               ? !now.isBefore(start) && now.isBefore(end)
               : !now.isBefore(start) || now.isBefore(end);
      } catch (DateTimeParseException ignored) {
         return false;
      }
   }
   private void ensureUpdated(int updated) {
      if (updated == 0) {
         throw new BusinessException(ErrorCode.NOT_FOUND, "Data not found");