4、系统架构设计:前端、后端、数据库、消息推送等模块划分与交互流程
账单日还款日提醒系统的架构设计,决定了系统的稳定性、扩展性以及后续维护成本。一个合理的架构应当将前端展示、后端业务逻辑、数据持久化和消息推送四个核心模块清晰解耦,同时定义好它们之间的交互契约。本章将围绕模块划分原则、核心交互流程、关键数据模型以及消息推送的技术选型展开,帮助您构建一个高内聚、低耦合的提醒系统骨架。
4.1 核心模块划分与职责边界
系统架构的第一要务是明确边界。我们将提醒系统划分为四个核心模块,每个模块只关注自己的领域,通过标准接口对外提供服务。
| 模块名称 | 核心职责 | 关键技术选型 | 典型接口/协议 |
|---|---|---|---|
| 前端展示层 | 账单录入、日历展示、提醒偏好设置、历史记录查询 | Vue 3 / React Native / 微信小程序 | RESTful API / WebSocket |
| 后端业务层 | 账单CRUD、提醒规则计算、用户鉴权、设备管理 | Spring Boot / Node.js (NestJS) | JWT / RBAC权限 |
| 数据持久层 | 结构化数据存储、缓存热点数据、消息队列持久化 | MySQL 8.0 + Redis 7.0 | MyBatis-Plus / Spring Data Redis |
| 消息推送层 | 定时扫描到期账单、组装模板消息、调用渠道网关 | XXL-Job + 微信模板消息 + 阿里云短信 | HTTP回调 / 消息队列 |
4.2 关键交互流程设计
系统的核心交互流程包括三个:账单创建与提醒规则绑定、定时扫描与推送触发、用户反馈与状态回写。下面逐一拆解。
流程一:账单创建与提醒规则绑定
- 前端提交:用户录入账单名称、金额、账单日(如每月5日)、还款日(如每月20日),并选择提醒提前量(如提前3天)。
- 后端校验:验证日期合法性、金额范围,并计算下一个提醒时间点。例如当前日期为4月10日,账单日为5月5日,还款日为5月20日,则下一个提醒时间为5月17日(提前3天)。
- 持久化存储:将账单记录写入MySQL的
bill表,同时将下一个提醒时间戳写入Redis的ZSet(用于定时扫描)。 - 返回结果:后端返回完整的账单对象(含生成的提醒计划),前端刷新日历视图。
流程二:定时扫描与推送触发
// 伪代码:定时任务核心逻辑(每天凌晨2点执行)
List<ReminderTask> tasks = reminderTaskMapper.findDueTasks(LocalDate.now());
for (ReminderTask task : tasks) {
// 1. 组装模板消息
String content = String.format("您的账单【%s】将于%s到期,金额%.2f元",
task.getBillName(), task.getDueDate(), task.getAmount());
// 2. 获取用户设备
String = deviceTokenMapper.getByUserId(task.getUserId());
// 3. 调用推送渠道
pushService.sendWechatTemplate(task.getOpenId(), content);
// 4. 记录推送日志
pushLogMapper.insert(new PushLog(task.getId(), "SUCCESS", content));
// 5. 更新下次提醒时间(如还款日当天再次提醒)
task.setNextRemindTime(task.getDueDate().plusDays(1));
reminderTaskMapper.updateById(task);
}
流程三:用户反馈与状态回写
用户收到提醒后,可能点击“已还款”按钮。此时前端调用POST /api/bill/{id}/repay接口,后端更新账单状态为PAID,同时删除Redis中对应的定时任务,避免重复提醒。若用户未操作,系统在还款日当天再次推送一次强提醒,之后标记为OVERDUE。
4.3 数据库设计要点
数据库设计直接影响查询效率和扩展性。建议至少包含以下四张核心表:
| 表名 | 关键字段 | 索引策略 | 说明 |
|---|---|---|---|
| user | id, open_id, phone, notify_pref | uk_open_id | 存储用户基础信息与通知偏好(微信/短信/App) |
| bill | id, user_id, name, amount, bill_day, due_day, status | idx_user_id, idx_status | 账单主表,status枚举:ACTIVE/PAID/OVERDUE |
| reminder_task | id, bill_id, remind_time, channel, status | idx_remind_time, idx_status | 提醒任务表,由定时任务扫描 |
| push_log | id, task_id, channel, content, result, create_time | idx_task_id | 推送日志,用于排障和统计 |
reminder_task表单独管理。原因在于:一个账单可能产生多条提醒(提前3天、提前1天、当天),如果冗余在bill表会导致字段膨胀且难以维护。独立表结构支持灵活的提醒策略调整。
4.4 消息推送模块的工程化实现
消息推送是系统的“最后一公里”,也是最容易出问题的环节。推送模块需要解决三个核心问题:可靠性(消息不丢失)、时效性(准时到达)、可观测性(推送失败可追溯)。
推送渠道适配器模式
建议采用策略模式封装不同渠道的推送逻辑。定义一个统一的PushChannel接口,微信、短信、App推送分别实现该接口。当需要新增渠道时,只需添加一个实现类并注册到Spring容器中。
public interface PushChannel {
PushResult send(PushRequest request);
boolean supports(ChannelType type);
}
@Component
public class WechatChannel implements PushChannel {
@Override
public PushResult send(PushRequest request) {
// 调用微信模板消息API
String accessToken = wechatTokenManager.getAccessToken();
// ... 组装消息体并发送
}
}
失败重试与补偿机制
推送失败是常态(如微信过期、用户取消关注)。必须设计重试机制:
- 立即重试:网络超时或5xx错误,间隔1秒、5秒、30秒重试3次。
- 延迟补偿:超过重试次数后,将任务写入死信队列,由补偿任务每小时扫描一次,尝试补发。
- 人工介入:对于连续失败超过3次的用户,标记为
PUSH_DISABLED,并在管理后台展示告警。
4.5 实战案例:某信用卡代还App的架构优化
某创业团队开发了一款信用卡账单管理App,初期架构将所有逻辑写在一个单体应用中,推送任务使用Spring的@Scheduled注解在应用内执行。上线后遇到三个问题:
- 推送延迟:用户量增长到5万后,定时任务扫描全表耗时超过10分钟,导致部分用户错过提醒时间。
- 重复推送:应用部署多个实例时,每个实例都会执行定时任务,用户收到重复消息。
- 排障困难:推送失败后没有日志记录,用户投诉时无法定位原因。
优化方案:
- 将定时任务迁移至独立的XXL-Job调度中心,支持分片广播,解决重复执行问题。
- 引入Redis ZSet存储待提醒任务,score为提醒时间戳,扫描时使用
ZRANGEBYSCORE命令,查询效率从O(n)降为O(log n)。 - 增加
push_log表,记录每次推送的请求参数、响应结果和耗时,配合ELK日志系统实现全链路追踪。
优化效果:推送准时率从87%提升至99.2%,重复推送完全消除,排障时间从平均2小时缩短至15分钟。代价是需要额外维护调度中心和Redis集群,但相比用户流失的损失,这笔投入是值得的。
4.6 常见误区与设计复盘
在架构设计中,以下误区需要特别警惕:
| 误区 | 后果 | 正确做法 |
|---|---|---|
| 前端直接调用第三方推送API | 暴露密钥、无法统一管理推送策略 | 所有推送请求必须经过后端,由后端统一签名和限流 |
| 数据库表设计时未考虑时间字段索引 | 定时扫描全表,性能急剧下降 | 对remind_time、status等查询字段建立联合索引 |
| 推送渠道耦合在业务代码中 | 新增渠道需要修改核心业务逻辑 | 使用策略模式+工厂模式,业务层只依赖抽象接口 |
| 忽略幂等性设计 | 网络重试导致用户收到重复消息 | 每次推送生成唯一message_id,渠道端做去重 |
复盘要点:架构设计的核心是识别变化点。在本系统中,变化最频繁的是推送渠道和提醒规则。因此,将这两个部分设计为可插拔的模块,是架构成功的关键。同时,要建立完善的监控体系,对推送成功率、任务积压量、接口响应时间等指标设置告警阈值,确保问题早发现、早处理。