<think>我们需要设计一个套餐续费提醒和自动续费系统,主要包含以下功能:
1. 续费提醒:在订阅到期前发送提醒(比如提前3天、1天)
2. 自动续费:用户可以选择开启自动续费,系统在到期时自动扣款续费
设计要点:
- 定时任务:用于检查即将到期的订阅并发送提醒,以及执行自动续费扣款
- 支付集成:需要与支付网关(如支付宝、微信支付、Stripe等)集成,支持自动扣款
- 用户设置:允许用户管理自动续费开关
- 失败处理:自动续费失败后的处理(如重试、通知用户等)
我们将分模块设计:
1. 数据库设计(新增自动续费相关字段)
2. 定时任务模块(续费提醒和自动续费执行)
3. 支付集成模块(封装支付网关的扣款接口)
4. 用户设置接口
5. 失败重试与通知机制
下面详细展开:</think>### 套餐续费提醒与自动续费功能设计
#### 整体架构设计
```
用户设置 → 定时任务调度 → 续费处理引擎 → 支付网关
↑ ↓ ↓ ↓
数据库 提醒服务 失败处理机制 扣款结果
```
#### 核心数据库设计
```sql
-- 用户订阅表(新增续费相关字段)
ALTER TABLE subscriptions ADD COLUMN (
auto_renew BOOLEAN DEFAULT false COMMENT '是否自动续费',
next_billing_date DATE COMMENT '下次扣款日期',
payment_token VARCHAR(255) COMMENT '支付令牌',
renewal_attempts INT DEFAULT 0 COMMENT '续费尝试次数',
renewal_status ENUM('PENDING', 'SUCCESS', 'FAILED') DEFAULT 'PENDING'
);
-- 续费记录表
CREATE TABLE renewal_records (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
subscription_id BIGINT NOT NULL,
attempt_date DATETIME NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status ENUM('SUCCESS', 'FAILED') NOT NULL,
failure_reason VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (subscription_id) REFERENCES subscriptions(id)
);
-- 提醒配置表
CREATE TABLE reminder_settings (
user_id BIGINT PRIMARY KEY,
days_before INT DEFAULT 3 COMMENT '提前几天提醒',
methods VARCHAR(100) COMMENT '提醒方式: EMAIL,SMS,PUSH',
last_reminder_sent DATE
);
```
#### 1. 续费提醒系统实现
##### 提醒服务核心代码
```java
@Service
public class RenewalReminderService {
private final SubscriptionRepository subscriptionRepo;
private final UserRepository userRepo;
private final EmailService emailService;
private final PushNotificationService pushService;
private final SmsService smsService;
@Scheduled(cron = "0 0 9 * * ?") // 每天上午9点执行
public void sendRenewalReminders() {
LocalDate today = LocalDate.now();
// 获取即将到期的订阅(3天内到期)
List<Subscription> expiringSubs = subscriptionRepo
.findSubscriptionsExpiringInDays(3);
for (Subscription sub : expiringSubs) {
User user = userRepo.findById(sub.getUserId())
.orElseThrow(() -> new UserNotFoundException(sub.getUserId()));
ReminderSetting setting = getReminderSetting(user.getId());
// 计算到期天数
long daysUntilExpire = ChronoUnit.DAYS.between(today, sub.getEndDate());
// 检查是否需要发送提醒
if (shouldSendReminder(setting, daysUntilExpire)) {
sendReminder(user, sub, setting, daysUntilExpire);
updateLastReminderSent(setting, today);
}
}
}
private void sendReminder(User user, Subscription sub,
ReminderSetting setting, long daysLeft) {
String message = String.format(
"您的%s会员将在%d天后到期。请及时续费以保证服务不中断!",
sub.getPlan().getName(), daysLeft
);
// 多渠道发送提醒
if (setting.getMethods().contains("EMAIL")) {
emailService.sendRenewalReminder(user.getEmail(), message);
}
if (setting.getMethods().contains("SMS")) {
smsService.sendSms(user.getPhone(), message);
}
if (setting.getMethods().contains("PUSH")) {
pushService.sendPush(user.getId(), "会员到期提醒", message);
}
}
private boolean shouldSendReminder(ReminderSetting setting, long daysLeft) {
// 检查是否达到配置的提醒天数
if (daysLeft > setting.getDaysBefore()) return false;
// 检查今天是否已发送过
return !today.equals(setting.getLastReminderSent());
}
}
```
##### 用户设置接口
```java
@RestController
@RequestMapping("/api/subscription")
public class SubscriptionController {
// 更新自动续费设置
@PutMapping("/{subscriptionId}/auto-renew")
public ResponseEntity<?> updateAutoRenew(
@PathVariable Long subscriptionId,
@RequestBody AutoRenewRequest request) {
Subscription sub = subscriptionRepo.findById(subscriptionId)
.orElseThrow(() -> new SubscriptionNotFoundException(subscriptionId));
// 验证用户权限
if (!sub.getUserId().equals(SecurityUtils.getCurrentUserId())) {
throw new AccessDeniedException("无权修改此订阅");
}
sub.setAutoRenew(request.isAutoRenew());
// 如果开启自动续费,需要验证支付令牌
if (request.isAutoRenew() && request.getPaymentToken() != null) {
if (!paymentService.validateToken(request.getPaymentToken())) {
throw new PaymentTokenInvalidException();
}
sub.setPaymentToken(request.getPaymentToken());
}
subscriptionRepo.save(sub);
return ResponseEntity.ok().build();
}
// 更新提醒设置
@PutMapping("/reminder-settings")
public ReminderSetting updateReminderSettings(
@RequestBody ReminderSettingRequest request) {
Long userId = SecurityUtils.getCurrentUserId();
ReminderSetting setting = settingRepo.findByUserId(userId)
.orElse(new ReminderSetting(userId));
setting.setDaysBefore(request.getDaysBefore());
setting.setMethods(String.join(",", request.getMethods()));
return settingRepo.save(setting);
}
}
```
#### 2. 自动续费系统实现
##### 自动续费引擎
```java
@Service
public class AutoRenewalEngine {
private final SubscriptionRepository subscriptionRepo;
private final PaymentService paymentService;
private final RenewalRecordRepository renewalRecordRepo;
private final NotificationService notificationService;
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
public void processAutoRenewals() {
LocalDate today = LocalDate.now();
// 获取今天需要续费的订阅
List<Subscription> renewSubs = subscriptionRepo
.findSubscriptionsForRenewal(today);
for (Subscription sub : renewSubs) {
processRenewal(sub);
}
}
private void processRenewal(Subscription sub) {
RenewalRecord record = new RenewalRecord();
record.setSubscriptionId(sub.getId());
record.setAttemptDate(LocalDateTime.now());
record.setAmount(sub.getPlan().getPrice());
try {
// 执行扣款
PaymentResult result = paymentService.charge(
sub.getPaymentToken(),
sub.getPlan().getPrice(),
"会员自动续费"
);
if (result.isSuccess()) {
// 续费成功
renewSubscription(sub);
record.setStatus("SUCCESS");
sendRenewalSuccessNotification(sub);
} else {
// 支付失败
handlePaymentFailure(sub, result.getErrorMessage());
record.setStatus("FAILED");
record.setFailureReason(result.getErrorMessage());
}
} catch (Exception e) {
// 系统异常
record.setStatus("FAILED");
record.setFailureReason("系统错误: " + e.getMessage());
logger.error("续费处理异常", e);
} finally {
renewalRecordRepo.save(record);
}
}
private void renewSubscription(Subscription sub) {
// 计算新订阅周期
LocalDate newEndDate = sub.getEndDate().plusDays(
sub.getPlan().getDurationDays()
);
// 更新订阅
sub.setStartDate(LocalDate.now());
sub.setEndDate(newEndDate);
sub.setRenewalStatus("SUCCESS");
sub.setRenewalAttempts(0);
// 设置下次扣款日期(提前3天)
sub.setNextBillingDate(newEndDate.minusDays(3));
subscriptionRepo.save(sub);
}
private void handlePaymentFailure(Subscription sub, String reason) {
// 更新尝试次数
sub.setRenewalAttempts(sub.getRenewalAttempts() + 1);
sub.setRenewalStatus("FAILED");
// 根据失败次数采取不同措施
if (sub.getRenewalAttempts() >= 3) {
// 超过最大尝试次数,关闭自动续费
sub.setAutoRenew(false);
sendAutoRenewDisabledNotification(sub, reason);
} else {
// 计划下次重试(1天后)
sub.setNextBillingDate(LocalDate.now().plusDays(1));
}
subscriptionRepo.save(sub);
sendRenewalFailureNotification(sub, reason);
}
}
```
##### 支付服务集成
```java
@Service
public class PaymentService {
// 统一支付网关接口
public PaymentResult charge(String paymentToken, BigDecimal amount, String description) {
// 根据token类型路由到不同支付渠道
if (paymentToken.startsWith("wx_")) {
return wechatPay(paymentToken, amount, description);
} else if (paymentToken.startsWith("ali_")) {
return alipay(paymentToken, amount, description);
} else if (paymentToken.startsWith("stripe_")) {
return stripePay(paymentToken, amount, description);
}
throw new UnsupportedPaymentMethodException();
}
// 微信支付实现
private PaymentResult wechatPay(String token, BigDecimal amount, String desc) {
try {
WxPayUnifiedOrderRequest request = new WxPayUnifiedOrderRequest();
request.setBody(desc);
request.setOutTradeNo(generateTradeNo());
request.setTotalFee(amount.multiply(BigDecimal.valueOf(100)).intValue());
request.setSpbillCreateIp("127.0.0.1");
request.setTradeType("MWEB");
WxPayUnifiedOrderResult result = wxPayService.unifiedOrder(request);
return new PaymentResult(true, "支付成功");
} catch (WxPayException e) {
return new PaymentResult(false, "微信支付失败: " + e.getReturnMsg());
}
}
// 支付宝实现
private PaymentResult alipay(String token, BigDecimal amount, String desc) {
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
AlipayTradePagePayModel model = new AlipayTradePagePayModel();
model.setOutTradeNo(generateTradeNo());
model.setTotalAmount(amount.toString());
model.setSubject(desc);
model.setProductCode("FAST_INSTANT_TRADE_PAY");
try {
AlipayTradePagePayResponse response = alipayClient.pageExecute(request);
if (response.isSuccess()) {
return new PaymentResult(true, "支付成功");
} else {
return new PaymentResult(false, "支付宝支付失败");
}
} catch (AlipayApiException e) {
return new PaymentResult(false, "支付宝系统错误: " + e.getMessage());
}
}
}
```
#### 3. 失败处理与通知系统
##### 失败重试机制
```java
public class RenewalRetryHandler {
// 指数退避重试策略
public void handleRetry(Subscription sub) {
int attempt = sub.getRenewalAttempts();
Duration delay = calculateDelay(attempt);
// 使用消息队列延迟处理
renewalQueue.sendDelayedMessage(
new RenewalTask(sub.getId()),
delay.toMillis()
);
}
private Duration calculateDelay(int attempt) {
// 指数退避算法:2^attempt 分钟,最大24小时
long minutes = (long) Math.pow(2, Math.min(attempt, 10));
return Duration.ofMinutes(Math.min(minutes, 24 * 60));
}
}
```
##### 通知服务
```java
@Service
public class RenewalNotificationService {
// 续费成功通知
public void sendRenewalSuccessNotification(Subscription sub) {
User user = userRepo.findById(sub.getUserId())
.orElseThrow(() -> new UserNotFoundException(sub.getUserId()));
String message = String.format(
"您的%s会员已成功续费至%s,本次扣款%s元。",
sub.getPlan().getName(),
sub.getEndDate().format(DateTimeFormatter.ISO_DATE),
sub.getPlan().getPrice()
);
notificationService.send(user, "续费成功通知", message);
}
// 续费失败通知
public void sendRenewalFailureNotification(Subscription sub, String reason) {
User user = userRepo.findById(sub.getUserId())
.orElseThrow(() -> new UserNotFoundException(sub.getUserId()));
String message = String.format(
"您的%s会员续费失败(原因:%s),系统将在%d小时后再次尝试。",
sub.getPlan().getName(),
reason,
calculateRetryHours(sub.getRenewalAttempts())
);
notificationService.send(user, "续费失败提醒", message);
}
// 自动续费关闭通知
public void sendAutoRenewDisabledNotification(Subscription sub, String reason) {
User user = userRepo.findById(sub.getUserId())
.orElseThrow(() -> new UserNotFoundException(sub.getUserId()));
String message = String.format(
"由于连续%d次续费失败(原因:%s),您的%s会员自动续费功能已被关闭。",
sub.getRenewalAttempts(),
reason,
sub.getPlan().getName()
);
notificationService.send(user, "自动续费已关闭", message);
}
}
```
### 关键设计要点
1. **支付令牌安全存储**
```java
// 使用AES加密存储支付令牌
public class TokenEncryptor {
private static final String SECRET_KEY = "your-256-bit-secret";
private static final String ALGORITHM = "AES/GCM/NoPadding";
public String encryptToken(String token) {
try {
Cipher cipher = Cipher.getInstance(ALGORITHM);
SecretKeySpec keySpec = new SecretKeySpec(SECRET_KEY.getBytes(), "AES");
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
byte[] encrypted = cipher.doFinal(token.getBytes());
return Base64.getEncoder().encodeToString(encrypted);
} catch (Exception e) {
throw new TokenEncryptionException("支付令牌加密失败", e);
}
}
public String decryptToken(String encrypted) {
try {
Cipher cipher = Cipher.getInstance(ALGORITHM);
SecretKeySpec keySpec = new SecretKeySpec(SECRET_KEY.getBytes(), "AES");
cipher.init(Cipher.DECRYPT_MODE, keySpec);
byte[] decoded = Base64.getDecoder().decode(encrypted);
return new String(cipher.doFinal(decoded));
} catch (Exception e) {
throw new TokenDecryptionException("支付令牌解密失败", e);
}
}
}
```
2. **续费状态机设计**
```java
// 续费状态机管理
public class RenewalStateMachine {
private static final Map<RenewalState, Map<RenewalEvent, RenewalState>> TRANSITIONS = Map.of(
RenewalState.PENDING, Map.of(
RenewalEvent.SCHEDULED, RenewalState.PENDING,
RenewalEvent.ATTEMPT, RenewalState.PROCESSING
),
RenewalState.PROCESSING, Map.of(
RenewalEvent.SUCCESS, RenewalState.COMPLETED,
RenewalEvent.FAILURE, RenewalState.RETRYING,
RenewalEvent.ABORT, RenewalState.ABORTED
),
RenewalState.RETRYING, Map.of(
RenewalEvent.ATTEMPT, RenewalState.PROCESSING,
RenewalEvent.ABORT, RenewalState.ABORTED
)
);
public RenewalState transition(RenewalState current, RenewalEvent event) {
Map<RenewalEvent, RenewalState> events = TRANSITIONS.get(current);
if (events == null || !events.containsKey(event)) {
throw new IllegalStateException("无效状态转换: " + current + " -> " + event);
}
return events.get(event);
}
}
enum RenewalState { PENDING, PROCESSING, RETRYING, COMPLETED, ABORTED }
enum RenewalEvent { SCHEDULED, ATTEMPT, SUCCESS, FAILURE, ABORT }
```
3. **账单记录与对账系统**
```java
// 对账服务
@Service
public class ReconciliationService {
@Scheduled(cron = "0 0 4 * * ?") // 每天凌晨4点执行
public void dailyReconciliation() {
LocalDate yesterday = LocalDate.now().minusDays(1);
// 获取系统记录的所有续费交易
List<RenewalRecord> systemRecords = renewalRecordRepo.findByDate(yesterday);
// 从支付网关获取实际交易
List<PaymentRecord> gatewayRecords = paymentGateway.getDailyReport(yesterday);
// 对账处理
ReconciliationResult result = reconcile(systemRecords, gatewayRecords);
// 处理差异
handleDiscrepancies(result.getDiscrepancies());
}
private ReconciliationResult reconcile(
List<RenewalRecord> systemRecords,
List<PaymentRecord> gatewayRecords) {
Map<String, RenewalRecord> systemMap = systemRecords.stream()
.collect(Collectors.toMap(r -> r.getTransactionId(), r -> r));
Map<String, PaymentRecord> gatewayMap = gatewayRecords.stream()
.collect(Collectors.toMap(p -> p.getTransactionId(), p -> p));
List<Discrepancy> discrepancies = new ArrayList<>();
// 检查系统有但网关没有的记录
systemMap.keySet().forEach(txId -> {
if (!gatewayMap.containsKey(txId)) {
discrepancies.add(new Discrepancy(
txId, "MISSING_IN_GATEWAY", systemMap.get(txId)
));
}
});
// 检查金额不一致的记录
gatewayMap.values().forEach(gateway -> {
RenewalRecord system = systemMap.get(gateway.getTransactionId());
if (system != null &&
system.getAmount().compareTo(gateway.getAmount()) != 0) {
discrepancies.add(new Discrepancy(
gateway.getTransactionId(),
"AMOUNT_MISMATCH",
system,
gateway
));
}
});
return new ReconciliationResult(systemRecords.size(),
gatewayRecords.size(), discrepancies);
}
}
```
### 安全与合规设计
1. **PCI DSS合规处理**
```java
// 支付令牌管理服务
@Service
public class PaymentTokenService {
// 使用第三方支付tokenization服务
public String tokenizePaymentInfo(PaymentCard card) {
TokenizationRequest request = new TokenizationRequest(
card.getNumber(),
card.getExpiry(),
card.getCvv()
);
TokenizationResponse response = thirdPartyService.tokenize(request);
return "tok_" + response.getToken();
}
// 敏感数据掩码
public String maskCardNumber(String cardNumber) {
if (cardNumber == null || cardNumber.length() < 12) return "****";
return cardNumber.substring(0, 4) + "****"
+ cardNumber.substring(cardNumber.length() - 4);
}
}
```
2. **GDPR合规设计**
```java
// 用户数据清理服务
@Service
public class DataCleanupService {
@Transactional
public void processDataDeletionRequest(Long userId) {
// 匿名化用户数据
anonymizeUserData(userId);
// 删除支付令牌
deletePaymentTokens(userId);
// 保留必要的财务记录(仅匿名化)
anonymizeFinancialRecords(userId);
}
private void anonymizeUserData(Long userId) {
userRepo.update(userId, Map.of(
"email", "deleted@" + UUID.randomUUID(),
"phone", "deleted",
"name", "Deleted User"
));
}
}
```
### 性能优化方案
1. **批量处理优化**
```java
// 批量续费处理
@Scheduled(fixedDelay = 30000) // 每30秒执行
public void processRenewalBatch() {
List<Subscription> renewSubs = subscriptionRepo
.findSubscriptionsForRenewalBatch(100); // 每次处理100条
if (!renewSubs.isEmpty()) {
// 并行处理
renewSubs.parallelStream().forEach(this::processRenewal);
}
}
```
2. **缓存优化**
```java
// 用户支付令牌缓存
@Service
public class TokenCacheService {
private final Cache<Long, String> tokenCache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.HOURS)
.maximumSize(10_000)
.build();
public String getPaymentToken(Long userId) {
return tokenCache.get(userId, key -> {
Subscription sub = subscriptionRepo.findActiveByUserId(userId);
return (sub != null) ? sub.getPaymentToken() : null;
});
}
}
```
---
###