针对会员卡顺延、暂停、续卡业务的思路

文章介绍了如何设计会员卡管理系统,包括会员卡的状态字段(如正常、暂停、终止),操作记录表来跟踪暂停、续卡和顺延等操作,以及通过定时任务自动化处理过期卡的状态更新。此设计便于查询统计并优化工作流程。

个人理解的大概思路

  1. 设计会员卡表,包含会员卡信息以及卡状态字段,比如卡状态为1表示正常,0表示暂停,2表示终止,等等。

  2. 设计会员卡操作记录表,用于记录卡的操作记录,比如暂停、续卡、顺延等操作,记录操作时间、操作人等信息。

  3. 在程序中,可以通过修改会员卡表的卡状态字段实现暂停和续卡操作,修改完成后,需要在会员卡操作记录表中新增一条操作记录,记录操作类型为暂停或续卡,同时记录操作人和操作时间。

  4. 对于顺延操作,可以通过修改会员卡的有效期字段实现,比如将有效期延长一定的天数。同样需要在会员卡操作记录表中新增一条操作记录,记录操作类型为顺延,同时记录操作人和操作时间。

  5. 在程序中,可以添加定时任务,定时检查会员卡的有效期,如果过期了则自动将卡状态改为2表示终止,并在会员卡操作记录表中新增一条操作记录,记录操作类型为到期终止。

这样设计可以方便地记录卡的操作历史,方便后续查询和统计,同时也可以实现自动终止过期会员卡,减轻工作负担。具体实现可以结合自己的业务需求进行调整和优化。

只是提供思路,如果各位老鸟发现博文有问题,欢迎指点一二

<think>我们需要设计一个套餐暂停功能,允许用户临时冻结会员资格,暂停期间不享受会员权益,也不计入会员有效期。暂停结束后自动恢复会员资格,并顺延有效期。 设计要点: 1. 用户界面:提供暂停申请、恢复、查看暂停记录等功能 2. 业务规则: - 暂停次数限制(如每年最多暂停3次) - 每次暂停时长限制(如7-30天) - 暂停期间不扣费,不计算有效期 - 暂停结束后,会员有效期顺延暂停天数 3. 状态管理:订阅状态增加“暂停中” 4. 定时任务:处理暂停结束和恢复 数据库设计: 在订阅表中增加暂停相关字段,并创建暂停记录表。 步骤: 1. 用户申请暂停,选择暂停天数(在允许范围内) 2. 验证是否符合暂停条件(如次数限制、当前状态等) 3. 创建暂停记录,更新订阅状态为“暂停中”,并设置暂停结束时间 4. 暂停结束时间到达时,系统自动恢复会员资格,并更新订阅有效期 下面我们详细实现:</think>### 套餐暂停功能设计与实现 套餐暂停功能允许用户临时冻结会员资格,暂停期间不享受会员权益,也不计入会员有效期。以下是完整的实现方案: #### 系统架构设计 ``` 暂停请求 → 暂停验证引擎 → 暂停执行服务 → 状态管理服务 ↑ ↓ ↓ ↓ 用户界面 规则检查 有效期调整 定时恢复任务 ``` #### 核心数据库设计 ```sql -- 订阅表新增暂停相关字段 ALTER TABLE subscriptions ADD COLUMN ( is_paused BOOLEAN DEFAULT false COMMENT '是否暂停中', pause_start_date DATE COMMENT '暂停开始日期', pause_end_date DATE COMMENT '暂停结束日期', pause_days_used INT DEFAULT 0 COMMENT '已使用暂停天数', pause_days_remaining INT DEFAULT 0 COMMENT '剩余暂停天数', pause_reason VARCHAR(255) COMMENT '暂停原因' ); -- 暂停记录表 CREATE TABLE pause_records ( id BIGINT AUTO_INCREMENT PRIMARY KEY, subscription_id BIGINT NOT NULL, start_date DATE NOT NULL, end_date DATE NOT NULL, requested_days INT NOT NULL COMMENT '请求暂停天数', actual_days INT NOT NULL COMMENT '实际暂停天数', status ENUM('ACTIVE', 'COMPLETED', 'CANCELLED') DEFAULT 'ACTIVE', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (subscription_id) REFERENCES subscriptions(id) ); -- 暂停策略表 CREATE TABLE pause_policies ( plan_id BIGINT NOT NULL, max_pause_days_per_year INT DEFAULT 30 COMMENT '每年最大暂停天数', max_pause_instances INT DEFAULT 2 COMMENT '每年最大暂停次数', min_days_before_pause INT DEFAULT 7 COMMENT '使用套餐后至少多少天才能暂停', advance_notice_days INT DEFAULT 3 COMMENT '提前通知天数', PRIMARY KEY (plan_id) ); ``` #### 1. 暂停策略服务 ```java @Service public class PausePolicyService { private final PausePolicyRepository policyRepo; // 获取套餐的暂停策略 public PausePolicy getPolicyForPlan(Long planId) { return policyRepo.findById(planId) .orElseGet(() -> new PausePolicy(planId)); // 返回默认策略 } // 验证暂停请求是否合规 public void validatePauseRequest(Subscription sub, int requestedDays) { PausePolicy policy = getPolicyForPlan(sub.getPlanId()); LocalDate today = LocalDate.now(); // 检查是否在套餐有效期内 if (today.isBefore(sub.getStartDate()) || today.isAfter(sub.getEndDate())) { throw new InvalidPauseException("套餐不在有效期内"); } // 检查最短使用时间要求 long daysUsed = ChronoUnit.DAYS.between(sub.getStartDate(), today); if (daysUsed < policy.getMinDaysBeforePause()) { throw new InvalidPauseException("使用套餐不足" + policy.getMinDaysBeforePause() + "天,无法暂停"); } // 检查年度暂停次数限制 int pausesThisYear = pauseRecordRepo.countPausesThisYear(sub.getId()); if (pausesThisYear >= policy.getMaxPauseInstances()) { throw new InvalidPauseException("已达到年度最大暂停次数"); } // 检查剩余暂停天数 int remainingDays = policy.getMaxPauseDaysPerYear() - pauseRecordRepo.getUsedPauseDaysThisYear(sub.getId()); if (requestedDays > remainingDays) { throw new InvalidPauseException("请求天数超过剩余可用暂停天数"); } } } ``` #### 2. 暂停执行服务 ```java @Service @Transactional public class PauseExecutionService { private final SubscriptionRepository subRepo; private final PauseRecordRepository pauseRecordRepo; private final BenefitService benefitService; private final NotificationService notificationService; // 处理暂停请求 public PauseRecord processPauseRequest(Long subscriptionId, int days, String reason) { Subscription sub = subRepo.findById(subscriptionId) .orElseThrow(() -> new SubscriptionNotFoundException(subscriptionId)); // 验证请求 PausePolicyService.validatePauseRequest(sub, days); // 创建暂停记录 PauseRecord record = new PauseRecord(); record.setSubscriptionId(subscriptionId); record.setStartDate(LocalDate.now()); record.setEndDate(LocalDate.now().plusDays(days)); record.setRequestedDays(days); record.setActualDays(days); record.setStatus("ACTIVE"); pauseRecordRepo.save(record); // 更新订阅状态 sub.setPaused(true); sub.setPauseStartDate(LocalDate.now()); sub.setPauseEndDate(record.getEndDate()); sub.setPauseDaysUsed(0); sub.setPauseDaysRemaining(days); sub.setPauseReason(reason); subRepo.save(sub); // 暂停用户权益 benefitService.suspendBenefits(sub.getUserId()); // 发送通知 notificationService.sendPauseConfirmation(sub, record); // 调度恢复任务 scheduleResumption(record); return record; } // 提前恢复套餐 public void resumeSubscriptionEarly(Long subscriptionId) { Subscription sub = subRepo.findById(subscriptionId) .orElseThrow(() -> new SubscriptionNotFoundException(subscriptionId)); if (!sub.isPaused()) { throw new InvalidResumeException("套餐未处于暂停状态"); } // 计算实际暂停天数 long actualPauseDays = ChronoUnit.DAYS.between( sub.getPauseStartDate(), LocalDate.now()); // 更新订阅 sub.setPaused(false); sub.setPauseDaysUsed((int) actualPauseDays); sub.setPauseDaysRemaining(0); subRepo.save(sub); // 更新暂停记录 PauseRecord record = pauseRecordRepo.findActiveBySubscription(subscriptionId); record.setActualDays((int) actualPauseDays); record.setStatus("COMPLETED"); pauseRecordRepo.save(record); // 恢复用户权益 benefitService.restoreBenefits(sub.getUserId()); // 调整套餐有效期 adjustSubscriptionEndDate(sub, actualPauseDays); // 发送通知 notificationService.sendEarlyResumeNotification(sub, record); } // 调整套餐有效期 private void adjustSubscriptionEndDate(Subscription sub, long pauseDays) { LocalDate newEndDate = sub.getEndDate().plusDays(pauseDays); sub.setEndDate(newEndDate); subRepo.save(sub); } } ``` #### 3. 定时恢复服务 ```java @Service public class SubscriptionResumptionService { private final SubscriptionRepository subRepo; private final PauseRecordRepository pauseRecordRepo; private final BenefitService benefitService; @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行 public void processScheduledResumptions() { LocalDate today = LocalDate.now(); // 查找需要恢复的订阅 List<Subscription> subscriptionsToResume = subRepo.findSubscriptionsToResume(today); for (Subscription sub : subscriptionsToResume) { try { resumeSubscription(sub); } catch (Exception e) { logger.error("恢复订阅失败: {}", sub.getId(), e); handleResumeFailure(sub, e); } } } private void resumeSubscription(Subscription sub) { // 计算实际暂停天数 long actualPauseDays = ChronoUnit.DAYS.between( sub.getPauseStartDate(), LocalDate.now()); // 更新订阅状态 sub.setPaused(false); sub.setPauseDaysUsed((int) actualPauseDays); sub.setPauseDaysRemaining(0); subRepo.save(sub); // 更新暂停记录 PauseRecord record = pauseRecordRepo.findActiveBySubscription(sub.getId()); record.setActualDays((int) actualPauseDays); record.setStatus("COMPLETED"); pauseRecordRepo.save(record); // 恢复用户权益 benefitService.restoreBenefits(sub.getUserId()); // 调整套餐有效期 adjustSubscriptionEndDate(sub, actualPauseDays); // 发送通知 notificationService.sendResumptionNotification(sub, record); } private void adjustSubscriptionEndDate(Subscription sub, long pauseDays) { LocalDate newEndDate = sub.getEndDate().plusDays(pauseDays); sub.setEndDate(newEndDate); subRepo.save(sub); } private void handleResumeFailure(Subscription sub, Exception e) { // 重试机制:使用指数退避策略 int retryCount = sub.getResumeRetryCount() != null ? sub.getResumeRetryCount() : 0; if (retryCount < 3) { // 计划重试 Duration delay = Duration.ofMinutes((long) Math.pow(2, retryCount)); resumptionRetryQueue.scheduleRetry(sub.getId(), delay); // 更新重试计数 sub.setResumeRetryCount(retryCount + 1); subRepo.save(sub); } else { // 超过最大重试次数,标记为失败 sub.setPauseStatus("RESUME_FAILED"); subRepo.save(sub); // 通知管理员 adminNotificationService.notifyResumeFailure(sub, e); } } } ``` #### 4. 权益管理服务 ```java @Service public class BenefitManagementService { private final UserAccessService accessService; private final DownloadService downloadService; private final ContentService contentService; // 暂停冻结权益 public void suspendBenefits(Long userId) { // 限制内容访问 accessService.disablePremiumAccess(userId); // 暂停离线下载 downloadService.pauseActiveDownloads(userId); // 限制设备数量 accessService.reduceConcurrentDevices(userId, 1); // 清除缓存 contentService.clearUserCache(userId); // 更新用户状态 userService.updateUserStatus(userId, UserStatus.PAUSED); } // 恢复时重新激活权益 public void restoreBenefits(Long userId) { // 恢复内容访问 accessService.restorePremiumAccess(userId); // 恢复离线下载 downloadService.resumePausedDownloads(userId); // 恢复设备限制 accessService.restoreDeviceLimit(userId); // 刷新用户缓存 contentService.refreshUserCache(userId); // 更新用户状态 userService.updateUserStatus(userId, UserStatus.ACTIVE); } } ``` ### 关键设计要点 #### 1. 暂停状态机管理 ```java public class PauseStateMachine { private static final Map<PauseState, Map<PauseEvent, PauseState>> TRANSITIONS = Map.of( PauseState.ACTIVE, Map.of( PauseEvent.EARLY_RESUME, PauseState.COMPLETED, PauseEvent.SCHEDULED_RESUME, PauseState.COMPLETED, PauseEvent.CANCEL, PauseState.CANCELLED ), PauseState.COMPLETED, Map.of( PauseEvent.REVERT, PauseState.ACTIVE ), PauseState.CANCELLED, Map.of( PauseEvent.REVERT, PauseState.ACTIVE ) ); public PauseState transition(PauseState current, PauseEvent event) { Map<PauseEvent, PauseState> events = TRANSITIONS.get(current); if (events == null || !events.containsKey(event)) { throw new IllegalStateException("无效状态转换: " + current + " -> " + event); } return events.get(event); } } enum PauseState { ACTIVE, COMPLETED, CANCELLED } enum PauseEvent { EARLY_RESUME, SCHEDULED_RESUME, CANCEL, REVERT } ``` #### 2. 有效期调整算法 ```java public class ValidityAdjustmentService { // 计算暂停后的新有效期 public LocalDate calculateNewEndDate(Subscription sub, long pauseDays) { MembershipPlan plan = planRepo.findById(sub.getPlanId()) .orElseThrow(() -> new PlanNotFoundException(sub.getPlanId())); // 根据套餐类型调整有效期 switch (plan.getBillingCycle()) { case MONTHLY: return adjustMonthlyPlan(sub, pauseDays); case ANNUAL: return adjustAnnualPlan(sub, pauseDays); case LIFETIME: // 终身会员无需调整 return sub.getEndDate(); default: // 默认按天计算 return sub.getEndDate().plusDays(pauseDays); } } private LocalDate adjustMonthlyPlan(Subscription sub, long pauseDays) { // 计算完整的计费周期 long fullCycles = pauseDays / 30; long remainingDays = pauseDays % 30; // 每月按30天计算 LocalDate newEndDate = sub.getEndDate() .plusMonths(fullCycles) .plusDays(remainingDays); return newEndDate; } private LocalDate adjustAnnualPlan(Subscription sub, long pauseDays) { // 每年按365天计算 long fullYears = pauseDays / 365; long remainingDays = pauseDays % 365; LocalDate newEndDate = sub.getEndDate() .plusYears(fullYears) .plusDays(remainingDays); return newEndDate; } } ``` #### 3. 暂停冲突检测 ```java public class PauseConflictDetector { public void checkForConflicts(Subscription sub) { // 检查是否有未完成的订单 if (orderService.hasPendingOrders(sub.getUserId())) { throw new PauseConflictException("存在未完成的订单,无法暂停套餐"); } // 检查是否在活动促销期内 if (promotionService.isInPromotionPeriod(sub)) { throw new PauseConflictException("促销期内无法暂停套餐"); } // 检查是否有未使用的权益 if (benefitService.hasPendingBenefits(sub.getUserId())) { throw new PauseConflictException("存在未使用的权益,无法暂停套餐"); } // 检查账户状态 if (userService.isAccountLocked(sub.getUserId())) { throw new PauseConflictException("账户被锁定,无法暂停套餐"); } } } ``` ### API接口设计 #### 用户接口 ```java @RestController @RequestMapping("/api/subscription") public class SubscriptionController { // 申请套餐暂停 @PostMapping("/{subscriptionId}/pause") public PauseRecord requestPause( @PathVariable Long subscriptionId, @RequestBody PauseRequest request) { return pauseService.processPauseRequest( subscriptionId, request.getDays(), request.getReason() ); } // 提前恢复套餐 @PostMapping("/{subscriptionId}/resume") public ResponseEntity<?> resumeSubscription( @PathVariable Long subscriptionId) { pauseService.resumeSubscriptionEarly(subscriptionId); return ResponseEntity.ok().build(); } // 获取暂停历史 @GetMapping("/{subscriptionId}/pause-history") public List<PauseRecord> getPauseHistory( @PathVariable Long subscriptionId) { return pauseRecordRepo.findBySubscriptionId(subscriptionId); } // 获取可用的暂停天数 @GetMapping("/{subscriptionId}/available-pause-days") public PauseAvailability getAvailablePauseDays( @PathVariable Long subscriptionId) { Subscription sub = subRepo.findById(subscriptionId).orElseThrow(); PausePolicy policy = policyService.getPolicyForPlan(sub.getPlanId()); int usedDays = pauseRecordRepo.getUsedPauseDaysThisYear(subscriptionId); int remainingDays = policy.getMaxPauseDaysPerYear() - usedDays; return new PauseAvailability( remainingDays, policy.getMaxPauseDaysPerYear(), policy.getMaxPauseInstances() - pauseRecordRepo.countPausesThisYear(subscriptionId) ); } } ``` #### 管理接口 ```java @RestController @RequestMapping("/admin/subscriptions") public class SubscriptionAdminController { // 强制暂停套餐(管理员操作) @PostMapping("/{subscriptionId}/force-pause") public PauseRecord forcePause( @PathVariable Long subscriptionId, @RequestBody ForcePauseRequest request) { // 验证管理员权限 if (!authService.isAdmin()) { throw new AccessDeniedException("需要管理员权限"); } // 执行暂停 return pauseService.processPauseRequest( subscriptionId, request.getDays(), request.getReason() ); } // 强制恢复套餐(管理员操作) @PostMapping("/{subscriptionId}/force-resume") public ResponseEntity<?> forceResume( @PathVariable Long subscriptionId) { // 验证管理员权限 if (!authService.isAdmin()) { throw new AccessDeniedException("需要管理员权限"); } pauseService.resumeSubscriptionEarly(subscriptionId); return ResponseEntity.ok().build(); } } ``` ### 安全与合规设计 #### 1. 暂停确认流程 ```java public class PauseConfirmationService { public void sendConfirmation(Subscription sub, PauseRecord record) { // 生成唯一确认令牌 String token = generateConfirmationToken(sub.getId()); // 存储到缓存(有效期24小时) redisTemplate.opsForValue().set( "pause:confirmation:" + sub.getId(), token, 24, TimeUnit.HOURS ); // 发送确认邮件 emailService.sendPauseConfirmation( sub.getUser().getEmail(), token, record ); } public boolean validateConfirmation(Long subscriptionId, String token) { String storedToken = redisTemplate.opsForValue().get( "pause:confirmation:" + subscriptionId); return token != null && token.equals(storedToken); } } ``` #### 2. 数据保留策略 ```java public class PauseDataRetention { // 清理旧的暂停记录 @Scheduled(cron = "0 0 4 * * ?") // 每天凌晨4点执行 public void cleanupOldPauseRecords() { LocalDate cutoff = LocalDate.now().minusYears(2); List<PauseRecord> oldRecords = pauseRecordRepo.findCompletedBefore(cutoff); pauseRecordRepo.deleteAll(oldRecords); } } ``` ### 性能优化方案 #### 1. 批量暂停处理 ```java public class BatchPauseProcessor { @Transactional public void processBatchPauses(List<Long> subscriptionIds, int days, String reason) { // 分批次处理(每批50个) Lists.partition(subscriptionIds, 50).forEach(batch -> { batch.parallelStream().forEach(subId -> { try { pauseService.processPauseRequest(subId, days, reason); } catch (Exception e) { logger.error("批量暂停失败: {}", subId, e); } }); }); } } ``` #### 2. 暂停状态缓存 ```java public class PauseStatusCache { private final Cache<Long, PauseStatus> statusCache = Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.MINUTES) .maximumSize(10_000) .build(); public PauseStatus getPauseStatus(Long subscriptionId) { return statusCache.get(subscriptionId, key -> { Subscription sub = subRepo.findById(key).orElseThrow(); return new PauseStatus( sub.isPaused(), sub.getPauseStartDate(), sub.getPauseEndDate(), sub.getPauseDaysRemaining() ); }); } } ``` --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hello Bug

谢谢老板,老板大气,老板硬邦邦

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值