|
|
@@ -0,0 +1,174 @@
|
|
|
+package com.etotem.num.service;
|
|
|
+
|
|
|
+import com.etotem.num.common.BizException;
|
|
|
+import com.etotem.num.entity.User;
|
|
|
+import com.etotem.num.entity.UserConsultationQuota;
|
|
|
+import com.etotem.num.repository.UserConsultationQuotaRepository;
|
|
|
+import com.etotem.num.repository.UserRepository;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+import java.time.LocalDate;
|
|
|
+import java.util.Optional;
|
|
|
+
|
|
|
+/**
|
|
|
+ * Consultation quota enforcement service.
|
|
|
+ * <p>
|
|
|
+ * Manages per-user, per-person daily chat quotas based on the user's
|
|
|
+ * role (vipType). All limits are read dynamically from sys_config.
|
|
|
+ * <p>
|
|
|
+ * Cross-day reset is handled atomically within the UPDATE statement,
|
|
|
+ * and a batch {@link #resetDailyQuota()} method is provided for
|
|
|
+ * {@code @Scheduled} cron jobs.
|
|
|
+ */
|
|
|
+@Service
|
|
|
+public class QuotaService {
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private UserConsultationQuotaRepository quotaRepository;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private ConfigService configService;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private UserRepository userRepository;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Check consultation quota and atomically consume one chat count.
|
|
|
+ * <p>
|
|
|
+ * Validates both person limit (max distinct persons) and daily chat
|
|
|
+ * limit (max chats per person per day) based on the user's role.
|
|
|
+ * Limits are read from sys_config dynamically via {@link ConfigService}.
|
|
|
+ * Cross-day reset is handled automatically within the atomic UPDATE
|
|
|
+ * (CASE WHEN in SET clause).
|
|
|
+ *
|
|
|
+ * @param userId the consulting user's ID
|
|
|
+ * @param personName the name of the person being consulted
|
|
|
+ * @throws BizException with code 403 if person limit or daily chat limit is exceeded
|
|
|
+ */
|
|
|
+ public void checkAndConsume(Long userId, String personName) {
|
|
|
+ User user = userRepository.findById(userId)
|
|
|
+ .orElseThrow(() -> new BizException(1002, "User not found"));
|
|
|
+ String roleKey = getRoleKey(user);
|
|
|
+ int personLimit = getPersonLimit(roleKey);
|
|
|
+ int chatLimit = getChatLimit(roleKey);
|
|
|
+
|
|
|
+ Optional<UserConsultationQuota> existing =
|
|
|
+ quotaRepository.findByUserIdAndPersonName(userId, personName);
|
|
|
+
|
|
|
+ if (existing.isEmpty()) {
|
|
|
+ // --- New person: enforce person limit ---
|
|
|
+ if (personLimit > 0) {
|
|
|
+ long currentPersonCount = quotaRepository.countByUserId(userId);
|
|
|
+ if (currentPersonCount >= personLimit) {
|
|
|
+ throw new BizException(403, "已达咨询人数上限,最多可咨询" + personLimit + "人");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // --- Existing person: enforce daily chat limit ---
|
|
|
+ if (chatLimit > 0) {
|
|
|
+ UserConsultationQuota quota = existing.get();
|
|
|
+ // If same day and count already at-or-above limit, block
|
|
|
+ if (quota.getLastChatDate() != null
|
|
|
+ && LocalDate.now().equals(quota.getLastChatDate())
|
|
|
+ && quota.getChatCountToday() >= chatLimit) {
|
|
|
+ throw new BizException(403, "今日咨询次数已用完");
|
|
|
+ }
|
|
|
+ // Cross-day: treat as 0 (will be reset to 1 by the UPDATE)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // INSERT IGNORE — safe for both new and existing persons
|
|
|
+ quotaRepository.insertIgnore(userId, personName);
|
|
|
+
|
|
|
+ // Atomic increment with cross-day awareness
|
|
|
+ int updated = quotaRepository.incrementChatCount(userId, personName);
|
|
|
+ if (updated == 0) {
|
|
|
+ throw new BizException(500, "配额更新失败");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Returns the number of distinct persons the user has ever consulted.
|
|
|
+ *
|
|
|
+ * @param userId the user's ID
|
|
|
+ * @return distinct person count
|
|
|
+ */
|
|
|
+ public int getPersonCount(Long userId) {
|
|
|
+ return (int) quotaRepository.countByUserId(userId);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Returns today's chat count for a specific person consulted by the user.
|
|
|
+ * <p>
|
|
|
+ * Returns 0 if no record exists (new person) or if the last chat was on
|
|
|
+ * a previous day (cross-day auto-reset).
|
|
|
+ *
|
|
|
+ * @param userId the user's ID
|
|
|
+ * @param personName the person's name
|
|
|
+ * @return today's chat count (0 if new person or cross-day)
|
|
|
+ */
|
|
|
+ public int getTodayChatCount(Long userId, String personName) {
|
|
|
+ return quotaRepository.findByUserIdAndPersonName(userId, personName)
|
|
|
+ .filter(q -> q.getLastChatDate() != null
|
|
|
+ && LocalDate.now().equals(q.getLastChatDate()))
|
|
|
+ .map(UserConsultationQuota::getChatCountToday)
|
|
|
+ .orElse(0);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Batch-reset all quota records whose last_chat_date is before today.
|
|
|
+ * <p>
|
|
|
+ * Sets {@code chat_count_today = 0} and {@code last_chat_date = CURDATE()}.
|
|
|
+ * Intended for use with {@code @Scheduled(cron = "...")} at midnight.
|
|
|
+ *
|
|
|
+ * @return number of affected rows
|
|
|
+ */
|
|
|
+ public int resetDailyQuota() {
|
|
|
+ return quotaRepository.resetAllDailyQuota();
|
|
|
+ }
|
|
|
+
|
|
|
+ // -- Private helpers --
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Maps the user's vipType to a role key for looking up quota limits in sys_config.
|
|
|
+ * <pre>
|
|
|
+ * null → "normal"
|
|
|
+ * "annual" → "annual"
|
|
|
+ * "family_plan" → "family"
|
|
|
+ * "super_annual" → "super"
|
|
|
+ * "practitioner" → "practitioner"
|
|
|
+ * </pre>
|
|
|
+ */
|
|
|
+ private String getRoleKey(User user) {
|
|
|
+ String vipType = user.getVipType();
|
|
|
+ if (vipType == null) return "normal";
|
|
|
+ switch (vipType) {
|
|
|
+ case "annual": return "annual";
|
|
|
+ case "family_plan": return "family";
|
|
|
+ case "super_annual": return "super";
|
|
|
+ case "practitioner": return "practitioner";
|
|
|
+ default: return "normal";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Reads the person limit for a given role key from sys_config.
|
|
|
+ * <p>
|
|
|
+ * Config key: {@code quota.person_limit.{roleKey}}<br>
|
|
|
+ * Default: 3 (standard normal-user limit).
|
|
|
+ */
|
|
|
+ private int getPersonLimit(String roleKey) {
|
|
|
+ return configService.getInt("quota.person_limit." + roleKey, 3);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Reads the daily chat limit for a given role key from sys_config.
|
|
|
+ * <p>
|
|
|
+ * Config key: {@code quota.chat_limit.{roleKey}}<br>
|
|
|
+ * Default: 3 (standard normal-user limit).
|
|
|
+ */
|
|
|
+ private int getChatLimit(String roleKey) {
|
|
|
+ return configService.getInt("quota.chat_limit." + roleKey, 3);
|
|
|
+ }
|
|
|
+}
|