1. 项目方案选型:为什么是Spring Boot + 微信小程序
做健康管理类系统之前,先别急着动手写代码。这个业务最大的特点不是并发量高,而是数据维度杂、时间跨度长、用户角色多。我这次选的组合是Spring Boot 2.x + 微信小程序原生开发 + MySQL + Redis,整体属于“有点传统但非常皮实”的路线,虽然不算新潮,但落地过程几乎没遇到什么框架层面的大坑。
Spring Boot在这个项目里主要承担三块职责:对外提供RESTful API,处理小程序端的所有请求;做业务逻辑编排,比如健康数据录入后的指标计算、异常判断、提醒推送;再就是对接MySQL做持久化,对接Redis做缓存和登录态管理。为什么不用Spring Cloud那套微服务?说实话,健康管理这种业务,一个中等体量的单体应用完全扛得住,拆成微服务只会引入分布式事务、服务治理这些不必要的复杂度。单体应用配合模块化包结构,后面真要拆,也比一开始就硬上微服务容易得多。
小程序端选原生开发而不是uni-app或者Taro,核心原因是这个项目需要用到的原生能力比较多。微信授权登录、订阅消息、canvas生成健康报告分享图、手机号快速验证,这些功能用原生框架去调微信API最直接,中间没有框架层转译的损耗。如果说要给Android和iOS各出一套App,我肯定会选跨平台方案,但既然业务场景明确是“微信里打开就用”,那小程序原生就是最优解,不需要为了假想的未来需求提前增加复杂度。
整体架构我分成了这么几块:小程序前端负责展示和交互,后端按业务模块拆包——用户模块、健康档案模块、数据录入模块、报告生成模块、消息通知模块。每个模块对内是独立的Service,对外通过Controller暴露接口。另外单独拎出来一个common包,统一处理返回值、异常、权限校验和工具方法。这套划分方式在项目初期看着有点繁琐,但等业务开始叠加需求的时候你就知道好处了:新功能只需要在自己的模块里加代码,几乎不影响其他模块。
技术栈定下来之后,还有几个容易被忽略的选型细节。比如HTTP工具用OkHttp还是RestTemplate,我选了OkHttp,因为性能好而且超时控制灵活;JSON序列化用Jackson,Spring Boot默认集成,不需要额外引入fastjson;权限认证用JWT而不是传统的Session,因为小程序端没有Cookie机制,JWT放在请求头里,配合Redis做黑名单控制,既简单又安全。这些选型本身没有绝对的对错,但组合在一起,整个系统的开发体验非常顺畅。
有人可能会问,为什么不用Spring Boot 3?这里有个现实原因:Spring Boot 3要求JDK 17以上,很多服务器上的JDK版本还停留在8或者11,升级成本不高但没必要。而且Spring Boot 2.x的生态太成熟了,市面上能找到的资料、踩坑记录都是现成的,遇到问题基本上一搜就有答案。对业务交付来说,减少意外比追逐版本新特性重要得多。
2. 数据库设计与核心需求拆解
2.1 健康管理系统的数据对象有哪些
健康管理这个业务,最关键的是想清楚“用户想要记录什么、系统要帮他分析什么”。我做需求调研的时候发现,大部分人对健康的诉求就几个维度:身体基本指标(体重、身高、BMI)、血压血糖、运动步数、睡眠时长、饮食摄入,以及这些数据在时间轴上的变化趋势。
围绕这些维度,我定义了几个核心数据实体:用户基本信息、健康指标记录、运动记录、饮食记录、健康档案(汇总视图)、预警规则、预警记录。用户基本信息除了昵称头像,还要存身高、出生年份、性别,因为后面计算BMI、基础代谢率、理想体重区间都需要这些基础数据。
这里有个容易犯的错:一开始只想着存“用户输入的原始值”,忽略了对派生数据的处理。比如BMI,虽然可以通过身高体重算出来,但你如果每次查询都实时计算,后面统计报表的时候会很尴尬——历史体重变了,历史BMI也会跟着变。所以我的方案是在指标记录表里直接冗余一个BMI字段,录入的时候就算好存进去。牺牲一点存储冗余,换取查询和统计的绝对稳定,这笔账怎么算都划算。
2.2 表结构设计与关键字段
用户表没什么特别的,标准的小程序用户模型。核心的健康指标表长这样:
CREATE TABLE health_metric_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT '用户ID', metric_type VARCHAR(32) NOT NULL COMMENT '指标类型: weight/blood_pressure/blood_sugar/heart_rate', metric_value DECIMAL(10,2) NOT NULL COMMENT '指标数值,血压存收缩压,血糖存mmol/L', metric_value_extra DECIMAL(10,2) DEFAULT NULL COMMENT '扩展值,血压存舒张压', measure_time DATETIME NOT NULL COMMENT '测量时间', source VARCHAR(16) DEFAULT 'manual' COMMENT '数据来源: manual/device/import', remark VARCHAR(255) DEFAULT NULL COMMENT '备注', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_user_time (user_id, measure_time), KEY idx_type_time (metric_type, measure_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;血压数据有点特殊,同时有收缩压和舒张压两个值,我用了一个metric_value_extra字段来存舒张压。血糖相对简单,一个数值就行,但要注意单位,血糖有mmol/L和mg/dL两种表示方法,国内用mmol/L,数据录入的时候要做输入限制,免得用户填出个三位数的血糖值。
运动记录表和饮食记录表结构类似,核心都是“时间 + 数值 + 备注”的模式。运动表多一个运动类型字段,是跑步、走路还是骑行;饮食表多一个热量字段,单位是千卡。有个设计细节是,饮食表的食物名称没有做成字典表,而是直接存字符串。因为每个人的饮食描述差异太大了,硬做字典反而增加录入成本,用户不愿意填,数据就废了。放弃一部分结构化程度,换取用户的持续使用意愿,值得。
健康档案表本质上是冗余表,存的是用户各项指标在某个时间段内的汇总统计值。比如最近7天平均体重、最近30天平均血压、这段时间的运动总步数。这种表不需要每次查询的时候现算,通过定时任务每天凌晨汇总更新一次,查询响应速度能快一个数量级。
2.3 数据写入的幂等与防重
健康数据录入不是高频操作,但存在一个真实场景:用户录完一条记录,网络抖动导致小程序端发起了重试请求,结果数据库里出现两条重复记录。解决这个问题,我在指标记录表上做了一个业务唯一键——user_id + metric_type + measure_time + source。小程序端每次录入的时候先查一下有没有同类型、同时间的数据,有就让用户确认是否覆盖,没有就直接插入。用业务逻辑做防重,比在数据库加唯一索引要灵活,因为用户可以主动选择“覆盖更新”还是“保留原先数据”。
另一个容易被忽略的点:测量时间应该是用户在界面上选择的时间,而不能直接取服务器当前时间。用户完全有可能在晚上补录早上的血压数据,如果系统自动用请求时间覆盖,后面的日报统计全部错位。我们在这个问题上被用户投诉过,后来改成了默认当前时间、但允许用户手动修改的交互方式,数据准确性大幅提升。
3. 小程序端关键功能落地细节
3.1 微信登录与Token认证链路
小程序端的登录流程是这套系统的地基。我用的不是普通的wx.login换取openid再自己生成session,而是完整的静默登录+手机号绑定方案:用户第一次打开小程序,通过wx.login拿到code,后端用code换openid和session_key,同时生成自定义JWT token返回给前端,前端把token存到wx.setStorageSync。首次登录时跳转到个人资料完善页,要求填写身高、性别、出生年份;后续启动直接校验token有效性,有效就静默登录跳首页,无效再走授权流程。
这里有个真实的坑:小程序的基础库版本不同,wx.getUserProfile的授权弹窗行为会变化。新版基础库下,用户主动点击才能触发授权,不能再像以前那样一进页面就弹窗。所以我在代码里把授权按钮和正常功能按钮做了统一的事件处理,用户没授权的时候点击任何需要身份的操作,都会跳转到引导授权页面。这个改动虽然小,但是对用户体验影响很大,很多人第一版就挂在这里。
// 小程序端登录逻辑简化版 wx.login({ success: (res) => { wx.request({ url: 'https://api.example.com/auth/login', method: 'POST', data: { code: res.code }, success: (resp) => { const { token, needProfile } = resp.data; wx.setStorageSync('token', token); if (needProfile) { wx.redirectTo({ url: '/pages/profile/edit' }); } else { wx.switchTab({ url: '/pages/index/index' }); } } }); } });3.2 健康数据录入与表单校验
数据录入页面是整个小程序里交互最频繁的模块,包括四类表单:基础指标(体重、身高、BMI)、血压血糖、运动记录、饮食记录。我在设计表单时遵循一个原则:默认值要智能,用户的操作成本越低,越愿意坚持记录。
体重输入框会显示上次记录的值作为占位提示,血压输入框自动聚焦到收缩压,血糖输入框做了单位换算提示。表单校验方面,除了前端必填校验,后端也要做一次严格的参数校验。因为小程序端可以被人抓包、篡改请求体,你永远不能相信前端传过来的数据。
后端校验我用的是Hibernate Validator,在DTO上直接加注解:
public class MetricRecordDTO { @NotNull(message = "指标类型不能为空") private String metricType; @NotNull(message = "指标数值不能为空") @DecimalMin(value = "0.1", message = "指标数值格式不正确") private BigDecimal metricValue; @PastOrPresent(message = "测量时间不能晚于当前时间") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime measureTime; }参数校验通过后进入业务逻辑层,先做防重查询,再计算BMI等派生指标,最后写库。这里还要处理一个边界情况:用户输入极端异常值怎么办?比如体重填了500公斤。我的做法是前端做范围限制,后端再兜底校验。体重范围设在20到300公斤,血压收缩压设在50到250mmHg,血糖设在0.5到40mmol/L,超出范围的直接提示“请检查输入的数据是否合理”。
3.3 趋势图表的渲染方案
健康数据如果只展示表格,用户坚持记录的动力会大打折扣。图表才是让枯燥数字变直观的关键。小程序端没有官方图表组件,我试过几个方案:wx-charts、echarts-for-weixin、手写canvas,最后还是选了echarts-for-weixin,因为它的图表类型丰富,折线图、柱状图、饼图都很成熟,而且有完整的官方适配方案。
实际使用时需要注意,echarts的包体积不小,如果直接放小程序包里,主包会膨胀很多。我把图表相关组件和页面都放在分包里,这样首屏加载速度不受影响。趋势图的数据加载做了三点优化:首次进入时显示一年内的月度汇总,下拉刷新时再请求半年内的周数据,同时提供时间范围筛选器,用户可以选择查看最近7天、30天、90天的数据趋势。
还有一个图表性能优化的细节:数据点超过100个时,折线图会开启采样机制。echarts的sampling参数可以设置为lttb,这种算法能在保留曲线趋势特征的前提下去除掉冗余点,渲染性能提升非常明显。我在实践后确定了一个平衡状态:查询最近一年的每日体重数据,大约365个数据点,开启lttb采样后渲染耗时从1.2秒降到了300毫秒左右,对用户来说基本无感知。
4. 后端核心接口与业务逻辑实现
4.1 统一返回体与全局异常处理
后端接口设计看似简单,实际上一堆细节决定了好坏。我从一开始就定了一个统一返回模型:
{ "code": 0, "message": "success", "data": { } }code为0表示成功,非0表示业务错误。这样做的好处是小程序端可以写一个统一的请求封装,对响应先判断code,非0再弹出错误提示,业务代码里就不用到处写try-catch了。
全局异常处理我用@RestControllerAdvice实现。业务异常、参数校验异常、系统异常分开处理,参数校验异常返回400,业务异常返回具体的业务错误码,系统异常返回500并记录完整日志。这里有个细节:不要把异常堆栈直接返回给前端,用户看到的是“系统繁忙,请稍后重试”这种友好提示,堆栈只打到服务端日志里,方便排查问题。
还有个我自己总结的经验:全局异常处理里一定要给每个业务异常定义一个明确的错误码,而不是笼统地返回“操作失败”。比如指标类型不存在返回10001,测量时间格式错误返回10002,权限不足返回20001。错误码体系一开始可能觉得麻烦,但上线后对接调试、排查问题时,价值非常大。
4.2 多条件查询与分页
健康记录查询是一个典型的多条件组合筛选场景:按指标类型、按时间范围、按数据来源、按用户。如果每个条件都单独写一个if判断,代码会越来越臃肿。我用MyBatis-Plus的QueryWrapper配合动态条件构造,代码非常简洁:
public PageResult<MetricRecordVO> pageQuery(MetricRecordQueryDTO dto) { LambdaQueryWrapper<HealthMetricRecord> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(StringUtils.isNotBlank(dto.getMetricType()), HealthMetricRecord::getMetricType, dto.getMetricType()); wrapper.eq(dto.getUserId() != null, HealthMetricRecord::getUserId, dto.getUserId()); wrapper.between(dto.getStartTime() != null && dto.getEndTime() != null, HealthMetricRecord::getMeasureTime, dto.getStartTime(), dto.getEndTime()); wrapper.orderByDesc(HealthMetricRecord::getMeasureTime); Page<HealthMetricRecord> page = new Page<>(dto.getPageNum(), dto.getPageSize()); IPage<HealthMetricRecord> result = mapper.selectPage(page, wrapper); return PageResult.from(result); }分页参数这里有个约定:pageNum从1开始,pageSize默认10、最大不超过50。防止用户一次拉取全量数据,把接口拖垮。排序默认按测量时间倒序,这样最新数据永远在最前面。
MyBatis-Plus的selectPage有个小坑:如果需要联表查询,比如查用户记录时关联出用户昵称和头像,就不能直接用简单的SelectMapper,需要写自定义SQL。这个项目里有几个统计报表接口就是联表查询,我直接写了@Select注解的Mapper方法,关联查询返回自定义VO,既保证性能,又能展示更多前端需要的信息。
4.3 健康预警与提醒规则
健康预警是健康管理系统里很有价值的功能,也是最有业务深度的部分。根据医学常识,我定义了几个预警规则引擎:BMI超出正常范围(18.5-24)、血压超过140/90mmHg、血糖空腹超过6.1mmol/L、连续7天运动步数低于3000。每一条规则的判定逻辑都独立封装成Rule类,实现统一的HealthRule接口,后续新增预警规则只需要实现接口并注册到规则引擎就行,不需要改动已有代码。
public interface HealthRule { String getRuleCode(); String getRuleName(); HealthAlert check(HealthContext context); }规则引擎执行的逻辑:定时任务每天凌晨扫描前一天的指标数据,逐条判断是否命中预警,命中就生成预警记录,同时触发微信订阅消息推送给用户。这里有个需要特别注意的细节:订阅消息的发送需要用户主动授权,且一次性订阅只能发送一条消息。所以我把预警推送设计成“一天最多推送一条聚合消息”,把当天所有异常指标合并成一条内容,而不是每个异常指标都发一条。这样既避免了骚扰用户,又能在授权次数有限的情况下尽可能完成有效触达。
4.4 健康周报定时任务
系统还有一个周报功能,每周一早上生成一份上周的健康数据汇总报告,通过订阅消息推送给用户。报告内容包括:上周平均体重、血压变化趋势、运动步数汇总、饮食热量摄入对比、异常指标提醒。这个功能的实现核心是@Scheduled定时任务,配合模板消息渲染。
定时任务的代码要注意运行环境的时钟同步问题。如果服务器时区设置不对,定时任务可能提前或延后几小时执行。我吃过大亏,当时服务器时区没设成Asia/Shanghai,周报发布始终在午夜12点而不是早上8点。后来在启动脚本里显式加了-Duser.timezone=GMT+8,问题彻底解决。另外,定时任务最好设计成幂等的——即使重复执行也不影响数据正确性。我的方案是周报生成前先查有没有已存在的记录,存在就直接返回,确保不会给用户推送两遍周报。
5. 安全策略与接口性能保障
5.1 接口防刷与请求限流
健康管理接口虽然没有电商秒杀那么高的并发,但也要防刷。有段时间我们某个数据统计分析接口被人用脚本不断调用,单机QPS一度被拉到峰值好几倍,虽然服务器没宕机,但正常用户的请求延迟明显变高了。排查后确认是接口没有做调用频率限制。
我引入了简单的计数器限流逻辑:基于Redis的INCR命令,在固定时间窗口内限制同一用户的接口调用次数。比如数据报告查询接口,限制为每人每10秒最多调用两次。实现方式不复杂:
public boolean tryAcquire(String userId, String apiKey, int maxCount, int windowSeconds) { String key = "rate:" + apiKey + ":" + userId; Long count = redisTemplate.opsForValue().increment(key); if (count != null && count == 1) { redisTemplate.expire(key, windowSeconds, TimeUnit.SECONDS); } return count != null && count <= maxCount; }防刷策略也不止限流一种。我还在网关层加了一个简单的黑名单机制,同一IP在短时间内如果请求失败次数过多,直接拉黑一段时间。这种粗颗粒度的防护对业务系统来说完全够用了,不需要上复杂的验证码系统,更没必要为了防刷专门引入一套风控系统。
5.2 个人信息与健康数据的加密存储
健康数据属于比较敏感的个人信息,数据库直存明文风险很大。在存储层面我对手机号等核心信息做了AES加密。用户手机号在库里存的是密文,查询时按需解密,但加密后的密文没法直接做模糊搜索,所以我在用户表里额外存了一个手机号后4位明文字段,专门用于搜索匹配。
这里有一个权衡:加密字段无法使用普通索引,所以手机号相关的查询性能会受影响。用户量不大的阶段无所谓,但如果用户量上来了,更合理的方案是引入分布式ID生成器,把用户ID作为对外主键,手机号只做登录校验用途,不参与业务关联查询。
另一个容易忽略的隐私保护点是日志。我在日志打印时统一做了脱敏处理,一行配置就能过滤用户敏感信息。如果你不想因为日志泄露用户数据而失眠,这个细节一定要配置上。
5.3 MySQL慢查询优化与Redis缓存策略
健康档案首页的统计汇总接口是整个系统访问最频繁的接口之一。用户打开小程序首页,就要拉取最近7天、30天、一年的数据汇总。如果每次实时查MySQL,几个表联查下来SQL会变得非常重,响应时间很容易超过500毫秒。
我的优化方案是分层缓存:首页统计结果先查Redis,缓存命中直接返回;缓存未命中再查MySQL,拿到结果后写入Redis并设置5分钟过期。缓存淘汰策略用LRU,保证热点数据一直在内存里。这里要注意缓存穿透的问题:如果某个用户没有任何健康记录,查询结果为空,Redis缓存的是空对象,但依然要设置过期时间,防止每次都穿透到数据库。
慢查询日志也必须盯着。上线后我定期用mysqldumpslow分析慢查询日志,发现过几个索引缺失的问题。比如按时间范围查指标数据时,索引如果只有user_id没有measure_time,数据量大了以后扫描行数会飙升。后来加了联合索引(user_id, measure_time),查询响应从800毫秒降到了100毫秒以内。索引设计是门学问,但核心原则就一条——根据实际where条件和排序条件来建,不要盲目加索引。
MySQL本身的参数调优也做了几项:innodb_buffer_pool_size设置为物理内存的60%,max_connections调到500,tmp_table_size和max_heap_table_size都设置为64MB。这些参数都是根据实际压测调整出来的,不要盲目照搬别人的配置。
6. 实操踩坑记录与排查工具清单
6.1 微信小程序真机请求失败排查
开发工具里接口请求一切正常,一到真机上就报request:fail,这是小程序开发最容易遇到的问题。微信官方要求所有请求域名必须是HTTPS,且必须在后台配置request合法域名。我在前几次开发时反复在这个问题上踩坑,后来总结出完整的排查思路。
打开小程序开发者工具右上角的“详情”,勾选“不校验合法域名”,本地开发就能绕过域名限制,但上线前一定要去掉这个选项。真机调试时遇到请求失败,优先按顺序检查:HTTPS证书是否有效、后台request域名是否包含当前请求的域名、域名后面是否带端口号(如果带了端口,后台也要配上)、请求方法是否在后台配置成允许。有一次我们排查了半天,发现是后台只配置了GET方法,小程序发的是POST请求,导致所有提交操作全部失败。这种低级错误,往往是最耗时间的排查过程。
6.2 Token过期与页面跳转循环
JWT token设计时设置了2小时的过期时间,但小程序端没有主动刷新token的机制,导致用户用着用着突然跳回登录页。更麻烦的是,登录页判断用户没有token就自动登录,登录成功后又跳回首页,首页再请求接口又发现token过期,形成了死循环。
解决方案是在小程序端封装统一的请求层,拦截器在收到401响应时静默调用刷新token接口。刷新token接口使用一个额外的refresh_token换取新的access_token,这个refresh_token的过期时间延长到30天。如果refresh_token也失效,才真正跳转到登录页。这个方案在实现时要注意并发问题:多个请求同时返回401时,只能有一个去调刷新接口,其他请求要排队等待新token返回后重新发起。可以用一个全局的pending队列来实现,避免重复刷新。
6.3 时区问题导致数据统计偏差
这是所有做移动端应用开发都会遇到的典型问题。小程序端提交身体数据时附带的时间是用户本地时间,但后端如果直接用LocalDateTime.now()获取服务器时间,两者就会产生偏差。
解决方法是:小程序端在请求数据时统一带一个X-Client-Time请求头,值为用户本地的ISO时间字符串;后端解析时统一转为Asia/Shanghai时区再存储。展示端再根据用户当前时区把时间转换回去。这里要注意的是,MySQL的DATETIME和TIMESTAMP类型行为不同,DATETIME不涉及时区转换,TIMESTAMP会,用哪种类型取决于业务需求。健康数据统计用的是时间轴分析,我选了DATETIME,因为记录的是用户实际测量那一刻的时刻,不需要被数据库时区影响。
6.4 Canvas生成分享图在真机上偶发白屏
小程序有个很经典的场景:用户生成健康周报分享图,保存到相册发给家人。我最初用canvas绘制分享图,开发工具里一切正常,真机上却偶发白屏。
这个问题本质上是canvas绘制的异步时序问题——绘制图片素材和绘制文本是并行的,图片还没加载完成就调用了canvasToTempFilePath,画出来的东西自然是空白。解决方案是在绘制前用wx.getImageInfo预先获取所有图片资源,全部加载完成后再执行canvas的绘制操作。有点像是先把食材准备齐了再开火炒菜,而不是边炒边洗菜。另外,canvas的type参数在真机上必须设置为2d,否则部分机型会因性能限制放弃渲染。还有一个细节,生成图片时要设置一个合适的画布尺寸,太小的画布在真机上保存出来会非常模糊,我们后来统一用750×1334的尺寸,正好适配大部分手机屏幕。
6.5 数据库连接池与连接泄漏排查
系统上线运行大概三周后,开始间歇性出现Connection is not available的报错。排查过程是这样的:先看数据库连接池监控,发现连接数持续上涨后无法释放,直到连接池被打满,所有新请求全部排队等待。
这是典型的连接泄漏问题——有部分代码在执行数据库操作时抛出了异常,导致连接没有通过finally块归还给连接池。我写的是MyBatis-Plus的Mapper调用,按理说会自动管理连接,但有个自定义链路追踪的拦截器在异常路径上主动关闭了连接,跟连接池的管理机制起了冲突。后来用HikariCP自带的leakDetectionThreshold参数,设置连接超过10秒未归还就打印完整堆栈,精确定位到泄漏的代码位置。这个参数强烈建议开启,它就像个摄像头,盯着每个连接是否按时归还。
7. 测试与上线过程中的经验沉淀
系统交付前,我做了一轮比较完整的接口测试和性能摸底。这里说的不是单元测试覆盖率,而是更实际的业务场景验证。比如用户从录入体重数据,到查看趋势曲线,再到触发体重异常预警,整个链路模拟用户真实操作走一遍,前后端各环节的数据准确性、权限校验都要实际验证。
接口测试我用了Postman搭好了一套完整的测试集合,每个接口都预设了成功、参数异常、权限不足、数据不存在四类用例。后续开发新接口时,只需要复制这套模板并修改参数,很快就能完成新增接口的冒烟测试。这套方法对个人开发者也同样适用,成本很低但收益很直接,上线前能让问题暴露一大半。
性能摸底方面,重点压测的是首页统计汇总接口和数据批量查询接口。用模拟工具发单接口请求并逐渐加大并发,观察响应时间是否符合预期。健康管理系统的目标并发并不高,我设定的是200并发的情况下,核心接口响应时间不超过300毫秒。实测下来首页统计和趋势图两个接口勉强达标,数据查询接口在索引优化后轻松通过。
上线部署用的是Docker Compose方式:Spring Boot应用打成一个镜像,MySQL和Redis分别用官方镜像跑,通过docker-compose.yml统一编排。环境变量统一管理了数据库连接串、Redis地址、小程序AppSecret等敏感配置,不硬编码在代码里。整个部署过程在云服务器上完成,nginx反向代理担当了HTTPS证书配置和静态资源缓存的角色。
上线不等于事停,监控体系也要跟上。我在服务端接入了简单的健康检查接口和日志监控策略,发现接口响应时间异常、错误率攀升时能第一时间看到。这套系统后来持续运行了很久,稳定性一直不错,除了偶尔的小程序端依赖API调整需要适配,后端几乎没怎么动过。
做这套健康管理系统的最大体会是:业务系统的技术复杂度并不在于用了多新的框架,而在于细节是否考虑周全。数据库字段要不要冗余、token失效怎么处理、图表加载怎么优化、接口被人刷了怎么办,每一个点看着都不大,但串起来却决定了整个项目是“能用”还是“好用”。如果你也在做类似的小程序后端项目,这套结构完全可以作为起点,照着这个思路去设计你自己的模块,能少走不少弯路。