ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Java的体重记录APP源码设计:从数据模型到趋势算法

基于Java的体重记录APP源码设计:从数据模型到趋势算法 简介这是一份基于Java开发的体重记录APP完整设计源码面向移动应用开发者、Java学习者及健康管理类产品设计人员用于掌握从数据存储、界面交互到图表展示的完整实现思路。资源包共257个文件主要包括175个Java源文件、32个XML配置文件、12个PNG图片、10个字体文件及Gradle构建脚本等另附属性文件、Markdown说明与LICENSE项目结构清晰便于按模块阅读与二次开发。压缩包大小约3.9MB轻量易部署。作者xyq2024已有313人浏览学习。源码不仅覆盖体重数据的新增、历史查询与目标管理还涉及多设备数据同步思考、隐私安全设计等扩展方向通过阅读Java核心逻辑与XML布局配置可快速理解该APP项目的分层架构与工程组织方式项目对异常输入、数据校验与界面刷新等细节均有处理适合作为课程设计、毕业设计或商业产品原型的参考基础。1. 基于Java开发的专业体重记录APP设计源码先立住边界看到「基于Java开发的专业体重记录APP设计源码」这个标题第一反应容易走偏成「写个计步器」或「套个Android壳」。实际上体重记录APP的核心难度不在录入界面而在数据模型和趋势计算一个人一天可能称三次体重早晚差出两公斤直接存原始值画折线图得到的是锯齿而不是趋势不处理生理性波动提醒和目标预测全是噪音。这套方案把技术栈钉在Java上后端用Spring Boot提供HTTP接口前端只做展示数据全部落MySQL源码按「测试、接口、算法、数据表」四层拆开。适合想自己掌握全链路、或者拿来做课程设计和面试项目的人。2. 体重记录APP的Java后端选型把「记录」变成可维护的数据模型2.1 功能拆解记录、趋势、目标、提醒四件事体重记录APP的「专业」体现在这里不是把数字存下来而是让数字产生决策。功能上拆成四块。记录是唯一入口用户在任意时间点提交体重和体脂率后端做合法性校验后入库趋势是把散点变成可读信息常见做法是同时算移动平均和每日变化量目标是从「当前值」到「目标值」的路径管理需要拆成每周建议减重速率提醒基于规则触发体脂率异常、久未记录、连续N天趋势向上分别对应不同文案。这四个功能决定了两件事第一数据表必须区分「原始记录」和「统计结果」不能在业务代码里临时聚合大表第二算法要放在Service层并配单元测试因为体重数据噪音大公式错了界面上是看不出来的。多数刚起步的Java项目死在第三步以为数据库里有一张表就够了结果业务逻辑越写越厚最后Controller里全是循环和if。2.2 体重数据表设计从一天一条到一天多条先给建表语句这是整套源码的地基。以MySQL为例。CREATE TABLE user ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, nickname VARCHAR(32) NOT NULL, height_cm DECIMAL(5,2) NOT NULL COMMENT 身高单位厘米保留两位, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE measurement ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id BIGINT UNSIGNED NOT NULL, weight_kg DECIMAL(5,2) NOT NULL COMMENT 体重单位公斤, body_fat_rate DECIMAL(4,2) NULL COMMENT 体脂率可选, note VARCHAR(255) NULL COMMENT 备注如早晨空腹、运动后, measured_at DATETIME NOT NULL COMMENT 称重时间APP端传入不用服务器时间, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_user_time (user_id, measured_at), CONSTRAINT fk_measurement_user FOREIGN KEY (user_id) REFERENCES user (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT体重原始记录表;几个参数设计的理由。体重和体脂率必须用DECIMAL而不是FLOAT体重计输出一位小数DECIMAL(5,2)足够避免浮点误差在趋势计算里被放大。measured_at用APP端传入的时间而不是数据库的CURRENT_TIMESTAMP这是称重数据的特殊性用户可能补录昨天早上的体重如果统一落服务器时间趋势曲线排序就错了。联合索引(user_id, measured_at)保证查询某个用户时间范围内的原始记录走索引这是列表页和趋势计算最频繁的查询路径。这张表刻意不做「一天一条」的唯一约束同一个用户一天多次称重是合理行为去重和过滤交给算法层不在数据库层硬卡。2.3 为什么后端选Spring Boot MyBatis而不是别的技术选型直接决定源码的交接成本和面试说服力。这个项目用Spring Boot 2.x MyBatis理由有三条。Java生态里Spring Boot的自动配置能省掉大量XML一个spring-boot-starter-web起步内嵌Tomcatjava -jar就能跑这对「设计源码」类项目最重要别人拿到代码五分钟内能启动才谈得上读源码。持久层选MyBatis而不是JPA看中的是SQL可控体重趋势、周统计、连续未记录天数这类查询SQL会越来越复杂MyBatis里可以逐条调优换成JPA的自动JPQL反而不容易定位问题。第三点容易被忽略MyBatis的源码规模在Java持久层框架里相对可读Mapper代理、SqlSession、插件机制三个核心链路能单独拆出来读。面试谈项目时从「用了MyBatis」引申到「MyBatis怎么给Mapper接口生成代理类」比背八股文有说服力得多。与之相对JPA在表关系复杂的ERP系统里优势明显但体重记录只有两张主表和几个聚合查询用JPA的实体关系映射属于给自己加戏。记住这个判断标准实体关系多、写多读少用JPA聚合查询多、SQL要精调用MyBatis。3. 用Spring Boot把体重记录APP跑起来最小可运行源码3.1 项目结构与Maven依赖源码工程建议按功能分包不按技术层分包。com.example.weight ├── WeightApplication.java ├── controller │ └── MeasurementController.java ├── service │ ├── MeasurementService.java │ └── TrendService.java ├── mapper │ ├── UserMapper.java │ └── MeasurementMapper.java ├── entity │ ├── User.java │ └── Measurement.java └── dto ├── MeasurementRequest.java └── TrendResponse.javacontroller、service、mapper、entity、dto五层对应「接口、业务、数据访问、表映射、传输对象」五个职责。需要说明的是entity和dto分开是刻意的实体类字段跟数据库列一一对应dto则裁剪字段并做参数校验避免把数据库结构直接暴露给APP端。pom.xml里核心依赖只需要四个。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdcom.alibaba/groupId artifactIdfastjson/artifactId version2.0.43/version /dependency /dependenciesSpring Boot版本锁在2.7.x而不是3.x原因是3.x要求Java 17而不少使用这套源码的环境还是Java 8。mybatis-spring-boot-starter必须在Spring Boot的依赖管理之外显式声明版本因为Spring Boot官方没有维护它的版本映射。3.2 实体类与Mapper一张表一个接口实体类字段与数据库列一一对应直接贴Measurement。public class Measurement { private Long id; private Long userId; private BigDecimal weightKg; private BigDecimal bodyFatRate; private String note; private LocalDateTime measuredAt; private LocalDateTime createdAt; // getter / setter 略实际源码中自动生成 }注意weightKg的类型是BigDecimal而不是double。MyBatis的映射会自动把数据库的DECIMAL(5,2)转成BigDecimal后续趋势计算全部用BigDecimal运算最后再转换为double输出避免精度丢失。Mapper接口只需要三个方法。public interface MeasurementMapper { int insert(Measurement measurement); ListMeasurement selectByUserAndTimeRange(Param(userId) Long userId, Param(start) LocalDateTime start, Param(end) LocalDateTime end); ListMeasurement selectLatestByUser(Param(userId) Long userId, Param(limit) int limit); }对应的MeasurementMapper.xml里selectByUserAndTimeRange是趋势计算的核心查询。select idselectByUserAndTimeRange resultTypecom.example.weight.entity.Measurement SELECT id, user_id AS userId, weight_kg AS weightKg, body_fat_rate AS bodyFatRate, note, measured_at AS measuredAt, created_at AS createdAt FROM measurement WHERE user_id #{userId} AND measured_at gt; #{start} AND measured_at lt; #{end} ORDER BY measured_at ASC /select这里必须写AS userId做别名映射否则MyBatis默认开启的驼峰映射会把user_id映射到userId一旦数据库字段用了下划线而实体类用驼峰没有别名就会全字段为null。排序用ASC保证时间升序趋势算法依赖有序数据查询层不排序算法层再排就是重复劳动。3.3 Controller按APP端的习惯返回JSONAPP端的诉求是「一次请求拿到完整数据」不要拆成五次调用。提供三个接口覆盖录入和查询全部场景。RestController RequestMapping(/api/v1/measurements) public class MeasurementController { private final MeasurementService measurementService; private final TrendService trendService; PostMapping public ResultLong create(RequestBody Valid MeasurementRequest request) { return Result.ok(measurementService.create(request)); } GetMapping(/history) public ResultListMeasurement history(RequestParam Long userId, RequestParam String startDate, RequestParam String endDate) { return Result.ok(measurementService.listByRange(userId, startDate, endDate)); } GetMapping(/trend) public ResultTrendResponse trend(RequestParam Long userId, RequestParam(defaultValue 30) int days) { return Result.ok(trendService.computeTrend(userId, days)); } }ResultT是统一响应包装包含code、message、data三个字段code0表示成功。日期参数用String接收再在Service里解析成LocalDate不要直接用Date类型接收JSON因为APP端传过来的格式可能是yyyy-MM-dd也可能是yyyy-MM-dd HH:mm:ssString解析可控性更强。3.4 本地启动与接口自测命令编译启动。# 先改 application.yml 里的数据库连接再启动 mvn spring-boot:run接口自测。# 新增一条记录 curl -X POST http://localhost:8080/api/v1/measurements \ -H Content-Type: application/json \ -d {userId:1,weightKg:72.5,bodyFatRate:21.3,measuredAt:2024-11-20 07:30:00,note:早晨空腹} # 查询最近30天趋势 curl http://localhost:8080/api/v1/measurements/trend?userId1days30application.yml三个必配项。配置项示例值作用spring.datasource.urljdbc:mysql://localhost:3306/weight_db?useUnicodetruecharacterEncodingutf8连接串带UTF8编码spring.datasource.password占位符密码放环境变量不放源码库mybatis.mapper-locationsclasspath:mapper/*.xml告诉MyBatis去哪里找XML文件自测时mvn spring-boot:run打印出Tomcat started后流程是POST一条假数据再GET查询历史重点看JSON里weightKg是否带两位小数、measuredAt是否和传参一致。如果measuredAt变成null八成是Fastjson或Jackson没识别到yyyy-MM-dd HH:mm:ss格式在application.yml里加一行spring.jackson.date-format即可。提示如果连本机调试都不想装MySQLH2数据库开着MySQL兼容模式也能把这套源码跑起来但建表语法里的ENGINEInnoDB要删掉。4. 体重记录里的「专业」在哪波动容忍、趋势算法与提醒源码4.1 为什么体重数据要处理而不是直接展示体重是典型的带噪生理信号。一天内水分摄入、盐分高低、排便时间差就能让同一个人的体重波动±1.5公斤。如果APP把每一条原始记录都画在折线图上趋势线会被噪声完全淹没用户看到的是一根上下乱跳的曲线不仅没有指导意义还会因为「昨天重了0.8公斤」产生焦虑。处理思路分两层。第一层是「过滤」对原始值做平滑常见做法是移动平均或指数加权平均第二层是「归约」在同一天有多条记录时取早晨空腹那一条作为当日标准值没有早晨记录就取当日均值。原始记录永远保留平滑结果只用于展示和判断这是数据可追溯性的底线。删原始数据、只留处理结果的做法是这类APP最常踩的坑一旦算法调优历史数据全废了。4.2 用指数加权平均过滤波动EWMA的Java实现与alpha取值指数加权平均EWMA比简单移动平均更适合体重场景它对近期数据敏感能快速反映真实趋势变化而且只需要保留前一个平滑值计算成本O(1)。公式是S(t) alpha * x(t) (1 - alpha) * S(t-1)x(t)是当前测量值S(t)是当前平滑值。public class EwmaFilter { private final double alpha; public EwmaFilter(double alpha) { this.alpha alpha; } public ListBigDecimal smooth(ListBigDecimal rawValues) { ListBigDecimal result new ArrayList(rawValues.size()); if (rawValues.isEmpty()) { return result; } double prev rawValues.get(0).doubleValue(); result.add(BigDecimal.valueOf(prev)); for (int i 1; i rawValues.size(); i) { double x rawValues.get(i).doubleValue(); prev alpha * x (1 - alpha) * prev; result.add(BigDecimal.valueOf(prev).setScale(2, RoundingMode.HALF_UP)); } return result; } }alpha取值直接决定平滑强度。alpha1时输出等于输入完全不平滑alpha0时输出恒等于第一个值过度平滑。体重场景alpha0.3是常用的经验值它表示最新一次测量对平滑值贡献30%的权重大约7次测量后旧数据的影响降到10%以下。如果用户每天固定早晨测一次alpha0.3大概一周后曲线开始贴合真实趋势。配合EWMA还要算一个delta字段即最新平滑值减去7天前的平滑值。delta 0.2表示一周净增重超过0.2公斤delta -0.2表示减重有效Math.abs(delta) 0.2视为平台期。这个判断逻辑放在Service层返回值里带上trendDirection字段前端直接显示「上升 / 下降 / 平稳」三个文案不在APP端计算。4.3 目标分解与提醒触发把每周速度算给用户看目标功能的核心是一个数学问题距离目标日期还有N周目标是T公斤当前平滑值是C公斤每周需要减多少。代码把时间单位精确到周避免「每天都要掉秤」这种不合理预期。public BigDecimal weeklyTargetRate(BigDecimal currentSmoothedWeight, BigDecimal targetWeight, LocalDate today, LocalDate targetDate) { long daysLeft ChronoUnit.DAYS.between(today, targetDate); if (daysLeft 0) { return BigDecimal.ZERO; } BigDecimal totalToLose currentSmoothedWeight.subtract(targetWeight); double weeksLeft daysLeft / 7.0; return totalToLose.divide(BigDecimal.valueOf(weeksLeft), 2, RoundingMode.HALF_UP); }divide必须传RoundingMode否则当除不尽时抛ArithmeticException。BigDecimal.valueOf(weeksLeft)确保分子分母都是BigDecimal避免double参与除法后精度漂移。提醒逻辑基于这个速率生成。当weeklyTargetRate绝对值超过1.5公斤周减重速率超过体重的1%不安全Service返回TOO_FAST提醒连续3天delta 0返回TREND_UP提醒超过72小时没有新记录返回NO_RECORD提醒。这三种提醒用策略接口实现更干净。public interface ReminderStrategy { String check(MeasurementContext context); boolean supports(ReminderType type); }每个策略一个类ReminderService里注入ListReminderStrategy并按需调用。这个设计的价值在于提醒规则是业务上最常变的部分——运营可能会加「连续一个月未记录发两次推送」——策略模式让每改动一个规则只动一个类测试也只测一个类。5. 源码交付形态可读性、可迁移与两个常踩的坑5.1 让源码能交接也能面试分层与README的写法源码工程做到「clone下来能启动、看目录能定位逻辑」就及格了一半。README里必须写清楚三件事环境要求Java 8、Maven 3.6、MySQL 5.7、建库建表脚本路径、默认接口的请求和响应示例。注意不要在这里堆接口文档那应该由Swagger或springdoc-openapi自动生成README只解决「启动问题」。每个Controller的类头注释写「这个接口服务于APP的哪个页面」比如MeasurementController的注释是「体重录入页 历史列表页」。后端代码容易在两个月后被投入新需求的人看而唯一记得页面长什么样的是写APP的人所以务必保证接口、页面、注释三者对应。5.2 坑一时间与时区乱套导致趋势曲线错位这是体重记录APP最隐蔽的坑。APP端和服务器端在不同时区时measured_at存的是「用户称重的本地时间」但数据库连接串没指定时区MySQL默认用服务器时区转换。结果是用户在UTC8早上7点称重数据库存成了UTC时间查询回来时一次性偏移8小时如果同一天早晚各测一次早晚顺序直接互换EWMA算出来的趋势方向是反的。解决方法是统一约定数据库连接串加上serverTimezoneAsia/Shanghai实体类用LocalDateTime而不是DateJSON序列化格式固定成yyyy-MM-dd HH:mm:ss。这三处统一后无论APP端在哪个时区传进来的时间戳都先转成这个格式再入库查询时原样返回。5.3 坑二浮点精度在目标计算里反复出问题接口返回趋势数据时delta和weeklyTargetRate如果直接返回BigDecimalFastjson序列化后输出的是科学计数法APP端doubleValue()再接可能变成0.30000000000000004。格式化必须在后端完成用setScale(2, RoundingMode.HALF_UP)输出两位小数别再往上传原始值。验证这个坑是否修复的办法用同一份测试数据跑两次/trend接口对比两次JSON字符串是否完全一致。如果存在随机尾数差异就是序列化层没有统一四舍五入。把格式化逻辑集中在TrendResponse的setter里而不是散落在Service各处排查时只需要看一个文件。本文还有配套的精品资源点击获取
返回列表