轻断食14+7方案小程序项目 - 完整实现方案
一、项目概述
1.1 项目背景
轻断食(Intermittent Fasting)是一种流行的健康饮食模式,其中14+7方案(14小时禁食,10小时进食窗口)是最为普遍的入门级方案。本项目旨在开发一个完整的微信小程序,帮助用户科学、便捷地执行14+7轻断食计划,配合Java后端提供数据管理和个性化建议服务。b09b9cac-ebab-4ec7-8c18-196829e8d064
1.2 技术架构
- 前端:微信小程序(使用WXML、WXSS、JavaScript)
- 后端:Java + Spring Boot + MyBatis Plus
- 数据库:MySQL 8.0
- 缓存:Redis
- 部署:Docker容器化部署
二、系统设计
2.1 功能模块
-
用户管理模块
- 微信授权登录
- 用户信息管理
- 健康数据记录
-
断食计划模块
- 14+7计划设置
- 实时计时器
- 进食窗口提醒
-
饮食记录模块
- 食物库管理
- 热量计算
- 营养成分分析
-
健康数据模块
- 体重追踪
- 身体指标记录
- 进度可视化
-
社区互动模块
- 经验分享
- 打卡挑战
- 专家建议
2.2 数据库设计
sql
-- 用户表
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`openid` varchar(100) NOT NULL COMMENT '微信openid',
`nickname` varchar(100) DEFAULT NULL,
`avatar_url` varchar(500) DEFAULT NULL,
`gender` tinyint(1) DEFAULT NULL,
`birthday` date DEFAULT NULL,
`height` decimal(5,2) DEFAULT NULL COMMENT '身高(cm)',
`initial_weight` decimal(5,2) DEFAULT NULL COMMENT '初始体重(kg)',
`target_weight` decimal(5,2) DEFAULT NULL COMMENT '目标体重(kg)',
`fasting_start_time` time DEFAULT NULL COMMENT '每日禁食开始时间',
`fasting_end_time` time DEFAULT NULL COMMENT '每日禁食结束时间',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_openid` (`openid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 断食记录表
CREATE TABLE `fasting_record` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL,
`record_date` date NOT NULL COMMENT '记录日期',
`fasting_start_time` datetime NOT NULL COMMENT '禁食开始时间',
`fasting_end_time` datetime DEFAULT NULL COMMENT '禁食结束时间',
`actual_fasting_hours` decimal(4,1) DEFAULT NULL COMMENT '实际禁食时长',
`status` tinyint(1) DEFAULT '1' COMMENT '状态:1-进行中,2-已完成,3-中断',
`notes` text COMMENT '备注',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_user_date` (`user_id`,`record_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 饮食记录表
CREATE TABLE `diet_record` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL,
`record_date` date NOT NULL,
`meal_type` tinyint(1) DEFAULT NULL COMMENT '餐次类型:1-早餐,2-午餐,3-晚餐,4-加餐',
`food_name` varchar(200) NOT NULL,
`quantity` decimal(6,2) DEFAULT NULL COMMENT '数量',
`unit` varchar(20) DEFAULT NULL COMMENT '单位',
`calories` int(11) DEFAULT NULL COMMENT '热量(卡)',
`protein` decimal(5,2) DEFAULT NULL COMMENT '蛋白质(g)',
`carbohydrate` decimal(5,2) DEFAULT NULL COMMENT '碳水(g)',
`fat` decimal(5,2) DEFAULT NULL COMMENT '脂肪(g)',
`record_time` datetime DEFAULT CURRENT_TIMESTAMP,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_user_date` (`user_id`,`record_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 体重记录表
CREATE TABLE `weight_record` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL,
`record_date` date NOT NULL,
`weight` decimal(5,2) NOT NULL COMMENT '体重(kg)',
`body_fat` decimal(4,2) DEFAULT NULL COMMENT '体脂率(%)',
`muscle` decimal(5,2) DEFAULT NULL COMMENT '肌肉量(kg)',
`bmi` decimal(4,2) DEFAULT NULL COMMENT 'BMI指数',
`notes` varchar(500) DEFAULT NULL,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_user_date` (`user_id`,`record_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 食物库表
CREATE TABLE `food_library` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL COMMENT '食物名称',
`category` varchar(50) DEFAULT NULL COMMENT '食物类别',
`calories_per_100g` int(11) DEFAULT NULL COMMENT '每100g热量',
`protein_per_100g` decimal(5,2) DEFAULT NULL COMMENT '每100g蛋白质',
`carbohydrate_per_100g` decimal(5,2) DEFAULT NULL COMMENT '每100g碳水',
`fat_per_100g` decimal(5,2) DEFAULT NULL COMMENT '每100g脂肪',
`common_serving` varchar(100) DEFAULT NULL COMMENT '常见份量',
`status` tinyint(1) DEFAULT '1' COMMENT '状态:1-启用,0-禁用',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
三、Java后端实现
3.1 项目结构
fasting-app-backend/
├── src/main/java/com/fasting/app/
│ ├── config/ # 配置类
│ ├── controller/ # 控制器层
│ ├── service/ # 服务层
│ │ ├── impl/ # 服务实现
│ ├── mapper/ # 数据访问层
│ ├── entity/ # 实体类
│ ├── dto/ # 数据传输对象
│ ├── vo/ # 视图对象
│ ├── common/ # 通用工具
│ │ ├── Result.java # 统一返回结果
│ │ ├── constants/ # 常量定义
│ │ └── utils/ # 工具类
│ └── FastingApplication.java # 启动类
└── resources/
├── application.yml # 配置文件
└── mapper/ # MyBatis映射文件
3.2 核心依赖配置
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.14</version>
</parent>
<groupId>com.fasting</groupId>
<artifactId>fasting-app-backend</artifactId>
<version>1.0.0</version>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<!-- Spring Boot Starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 数据库相关 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.34</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- 微信小程序SDK -->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-miniapp</artifactId>
<version>4.5.0</version>
</dependency>
<!-- 验证 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
3.3 核心实体类实现
java
// User.java
@Data
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String openid;
private String nickname;
private String avatarUrl;
private Integer gender;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date birthday;
private BigDecimal height;
private BigDecimal initialWeight;
private BigDecimal targetWeight;
@JsonFormat(pattern = "HH:mm:ss")
private LocalTime fastingStartTime;
@JsonFormat(pattern = "HH:mm:ss")
private LocalTime fastingEndTime;
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
}
// FastingRecord.java
@Data
@TableName("fasting_record")
public class FastingRecord {
@TableId(type = IdType.AUTO)
private Long id;
private Long userId;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date recordDate;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date fastingStartTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date fastingEndTime;
private BigDecimal actualFastingHours;
private Integer status;
private String notes;
@TableField(fill = FieldFill.INSERT)
private Date createTime;
}
// FastingRecordVO.java - 用于前端展示
@Data
public class FastingRecordVO {
private Long id;
private Date recordDate;
private Date fastingStartTime;
private Date fastingEndTime;
private BigDecimal actualFastingHours;
private Integer status;
private String statusDesc;
private String notes;
// 计算禁食剩余时间(如果是当天正在进行中的记录)
private String remainingTime;
// 进食窗口状态
private Boolean isEatingWindow;
// 进度百分比(0-100)
private Integer progressPercentage;
}
3.4 服务层实现
java
// FastingService.java
public interface FastingService {
/**
* 开始新的禁食周期
*/
Result startFasting(Long userId, Date startTime);
/**
* 结束禁食周期
*/
Result endFasting(Long userId, Date endTime);
/**
* 获取当日禁食状态
*/
Result getTodayFastingStatus(Long userId);
/**
* 获取用户断食统计数据
*/
Result getFastingStatistics(Long userId, Date startDate, Date endDate);
/**
* 获取用户禁食日历数据
*/
Result getFastingCalendar(Long userId, Integer year, Integer month);
}
// FastingServiceImpl.java
@Service
@Slf4j
public class FastingServiceImpl implements FastingService {
@Autowired
private FastingRecordMapper fastingRecordMapper;
@Autowired
private UserMapper userMapper;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String TODAY_FASTING_KEY = "fasting:today:";
@Override
@Transactional
public Result startFasting(Long userId, Date startTime) {
try {
User user = userMapper.selectById(userId);
if (user == null) {
return Result.error("用户不存在");
}
// 检查是否已经有当天的禁食记录
Date today = DateUtil.beginOfDay(new Date());
LambdaQueryWrapper<FastingRecord> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(FastingRecord::getUserId, userId)
.eq(FastingRecord::getRecordDate, today);
FastingRecord existingRecord = fastingRecordMapper.selectOne(queryWrapper);
if (existingRecord != null) {
return Result.error("今日已开始禁食");
}
// 创建新的禁食记录
FastingRecord record = new FastingRecord();
record.setUserId(userId);
record.setRecordDate(today);
record.setFastingStartTime(startTime);
record.setStatus(1); // 进行中
fastingRecordMapper.insert(record);
// 缓存当天禁食状态
cacheTodayFastingStatus(userId, record);
log.info("用户{}开始禁食,开始时间:{}", userId, startTime);
return Result.success("禁食开始成功", record);
} catch (Exception e) {
log.error("开始禁食失败", e);
throw new RuntimeException("开始禁食失败", e);
}
}
@Override
@Transactional
public Result endFasting(Long userId, Date endTime) {
try {
Date today = DateUtil.beginOfDay(new Date());
LambdaQueryWrapper<FastingRecord> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(FastingRecord::getUserId, userId)
.eq(FastingRecord::getRecordDate, today)
.eq(FastingRecord::getStatus, 1);
FastingRecord record = fastingRecordMapper.selectOne(queryWrapper);
if (record == null) {
return Result.error("没有找到进行中的禁食记录");
}
// 计算实际禁食时长(小时)
long startMillis = record.getFastingStartTime().getTime();
long endMillis = endTime.getTime();
double hours = (endMillis - startMillis) / (1000.0 * 60 * 60);
record.setFastingEndTime(endTime);
record.setActualFastingHours(BigDecimal.valueOf(hours));
record.setStatus(2); // 已完成
record.setUpdateTime(new Date());
fastingRecordMapper.updateById(record);
// 更新缓存
cacheTodayFastingStatus(userId, record);
// 检查是否达到14小时目标
String message = "禁食结束成功";
if (hours >= 14) {
message = "恭喜!您已完成14小时禁食目标!";
// 可以添加成就系统逻辑
}
log.info("用户{}结束禁食,时长:{}小时", userId, hours);
return Result.success(message, record);
} catch (Exception e) {
log.error("结束禁食失败", e);
throw new RuntimeException("结束禁食失败", e);
}
}
@Override
public Result getTodayFastingStatus(Long userId) {
try {
// 先从缓存获取
String cacheKey = TODAY_FASTING_KEY + userId;
FastingRecordVO cachedStatus = (FastingRecordVO) redisTemplate.opsForValue().get(cacheKey);
if (cachedStatus != null) {
return Result.success(cachedStatus);
}
// 缓存不存在,从数据库查询
Date today = DateUtil.beginOfDay(new Date());
LambdaQueryWrapper<FastingRecord> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(FastingRecord::getUserId, userId)
.eq(FastingRecord::getRecordDate, today);
FastingRecord record = fastingRecordMapper.selectOne(queryWrapper);
if (record == null) {
// 如果没有记录,返回默认状态
FastingRecordVO defaultStatus = new FastingRecordVO();
defaultStatus.setRecordDate(today);
defaultStatus.setStatus(0); // 未开始
defaultStatus.setStatusDesc("未开始");
defaultStatus.setIsEatingWindow(true); // 默认在进食窗口
return Result.success(defaultStatus);
}
// 转换为VO对象并计算额外信息
FastingRecordVO vo = convertToVO(record);
// 计算进食窗口状态
User user = userMapper.selectById(userId);
if (user != null && user.getFastingStartTime() != null && user.getFastingEndTime() != null) {
LocalTime now = LocalTime.now();
boolean isEatingWindow = now.isAfter(user.getFastingEndTime()) &&
now.isBefore(user.getFastingStartTime());
vo.setIsEatingWindow(isEatingWindow);
}
// 计算进度百分比(如果是进行中状态)
if (record.getStatus() == 1) {
long startMillis = record.getFastingStartTime().getTime();
long currentMillis = System.currentTimeMillis();
long targetMillis = startMillis + (14 * 60 * 60 * 1000); // 14小时目标
if (currentMillis <= targetMillis) {
int progress = (int) ((currentMillis - startMillis) * 100 / (14 * 60 * 60 * 1000));
vo.setProgressPercentage(Math.min(progress, 100));
// 计算剩余时间
long remainingMillis = targetMillis - currentMillis;
vo.setRemainingTime(formatRemainingTime(remainingMillis));
}
}
// 缓存结果
redisTemplate.opsForValue().set(cacheKey, vo, 5, TimeUnit.MINUTES);
return Result.success(vo);
} catch (Exception e) {
log.error("获取禁食状态失败", e);
return Result.error("获取禁食状态失败");
}
}
@Override
public Result getFastingStatistics(Long userId, Date startDate, Date endDate) {
try {
// 获取用户基本信息
User user = userMapper.selectById(userId);
if (user == null) {
return Result.error("用户不存在");
}
// 查询时间段内的禁食记录
LambdaQueryWrapper<FastingRecord> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(FastingRecord::getUserId, userId)
.ge(FastingRecord::getRecordDate, startDate)
.le(FastingRecord::getRecordDate, endDate)
.eq(FastingRecord::getStatus, 2); // 只统计已完成的
List<FastingRecord> records = fastingRecordMapper.selectList(queryWrapper);
// 计算统计数据
FastingStatisticsVO statistics = new FastingStatisticsVO();
statistics.setTotalDays(records.size());
// 计算平均禁食时长
double totalHours = records.stream()
.filter(r -> r.getActualFastingHours() != null)
.mapToDouble(r -> r.getActualFastingHours().doubleValue())
.sum();
if (records.size() > 0) {
statistics.setAverageHours(totalHours / records.size());
}
// 计算达标率(达到14小时的比例)
long achievedCount = records.stream()
.filter(r -> r.getActualFastingHours() != null &&
r.getActualFastingHours().doubleValue() >= 14)
.count();
if (records.size() > 0) {
statistics.setAchievementRate((double) achievedCount / records.size() * 100);
}
// 获取连续禁食天数
int consecutiveDays = calculateConsecutiveDays(userId);
statistics.setConsecutiveDays(consecutiveDays);
// 计算预估的热量消耗(根据体重和禁食时间估算)
if (user.getInitialWeight() != null) {
// 简单估算:每公斤体重每小时消耗约0.8卡路里
double estimatedCalories = totalHours * user.getInitialWeight().doubleValue() * 0.8;
statistics.setEstimatedCaloriesBurned(estimatedCalories);
}
// 生成趋势数据
Map<Date, Double> trendData = new TreeMap<>();
for (FastingRecord record : records) {
trendData.put(record.getRecordDate(),
record.getActualFastingHours().doubleValue());
}
statistics.setTrendData(trendData);
return Result.success(statistics);
} catch (Exception e) {
log.error("获取禁食统计数据失败", e);
return Result.error("获取统计数据失败");
}
}
/**
* 计算连续禁食天数
*/
private int calculateConsecutiveDays(Long userId) {
// 查询最近30天的记录
Date endDate = new Date();
Date startDate = DateUtil.offsetDay(endDate, -30);
LambdaQueryWrapper<FastingRecord> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(FastingRecord::getUserId, userId)
.ge(FastingRecord::getRecordDate, startDate)
.le(FastingRecord::getRecordDate, endDate)
.eq(FastingRecord::getStatus, 2)
.orderByDesc(FastingRecord::getRecordDate);
List<FastingRecord> records = fastingRecordMapper.selectList(queryWrapper);
int consecutiveDays = 0;
Date expectedDate = endDate;
for (FastingRecord record : records) {
if (DateUtil.isSameDay(record.getRecordDate(), expectedDate) ||
DateUtil.isSameDay(record.getRecordDate(), DateUtil.offsetDay(expectedDate, -1))) {
consecutiveDays++;
expectedDate = record.getRecordDate();
} else {
break;
}
}
return consecutiveDays;
}
private FastingRecordVO convertToVO(FastingRecord record) {
if (record == null) return null;
FastingRecordVO vo = new FastingRecordVO();
BeanUtils.copyProperties(record, vo);
// 设置状态描述
switch (record.getStatus()) {
case 1:
vo.setStatusDesc("进行中");
break;
case 2:
vo.setStatusDesc("已完成");
break;
case 3:
vo.setStatusDesc("已中断");
break;
default:
vo.setStatusDesc("未知");
}
return vo;
}
private String formatRemainingTime(long millis) {
long hours = millis / (1000 * 60 * 60);
long minutes = (millis % (1000 * 60 * 60)) / (1000 * 60);
return String.format("%02d:%02d", hours, minutes);
}
private void cacheTodayFastingStatus(Long userId, FastingRecord record) {
String cacheKey = TODAY_FASTING_KEY + userId;
FastingRecordVO vo = convertToVO(record);
redisTemplate.opsForValue().set(cacheKey, vo, 5, TimeUnit.MINUTES);
}
}
// FastingStatisticsVO.java
@Data
public class FastingStatisticsVO {
private Integer totalDays; // 总天数
private Double averageHours; // 平均禁食时长
private Double achievementRate; // 达标率
private Integer consecutiveDays; // 连续天数
private Double estimatedCaloriesBurned; // 预估消耗热量
private Map<Date, Double> trendData; // 趋势数据
}
3.5 控制器层实现
java
@RestController
@RequestMapping("/api/fasting")
@Slf4j
public class FastingController {
@Autowired
private FastingService fastingService;
@Autowired
private UserService userService;
/**
* 开始禁食
*/
@PostMapping("/start")
public Result startFasting(@RequestHeader("X-User-Id") Long userId,
@RequestBody StartFastingDTO dto) {
// 验证用户身份
User user = userService.getById(userId);
if (user == null) {
return Result.error(401, "用户未登录");
}
return fastingService.startFasting(userId, dto.getStartTime());
}
/**
* 结束禁食
*/
@PostMapping("/end")
public Result endFasting(@RequestHeader("X-User-Id") Long userId,
@RequestBody EndFastingDTO dto) {
return fastingService.endFasting(userId, dto.getEndTime());
}
/**
* 获取今日禁食状态
*/
@GetMapping("/today")
public Result getTodayFasting(@RequestHeader("X-User-Id") Long userId) {
return fastingService.getTodayFastingStatus(userId);
}
/**
* 获取禁食统计数据
*/
@GetMapping("/statistics")
public Result getStatistics(@RequestHeader("X-User-Id") Long userId,
@RequestParam String startDate,
@RequestParam String endDate) {
try {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date start = sdf.parse(startDate);
Date end = sdf.parse(endDate);
return fastingService.getFastingStatistics(userId, start, end);
} catch (ParseException e) {
log.error("日期解析失败", e);
return Result.error("日期格式错误");
}
}
/**
* 获取禁食日历
*/
@GetMapping("/calendar")
public Result getCalendar(@RequestHeader("X-User-Id") Long userId,
@RequestParam Integer year,
@RequestParam Integer month) {
return fastingService.getFastingCalendar(userId, year, month);
}
/**
* 设置禁食计划
*/
@PostMapping("/plan")
public Result setFastingPlan(@RequestHeader("X-User-Id") Long userId,
@RequestBody FastingPlanDTO dto) {
try {
User user = userService.getById(userId);
if (user == null) {
return Result.error("用户不存在");
}
// 验证时间格式
LocalTime startTime = LocalTime.parse(dto.getFastingStartTime());
LocalTime endTime = LocalTime.parse(dto.getFastingEndTime());
// 计算进食窗口(10小时)
long eatingWindowHours = ChronoUnit.HOURS.between(endTime, startTime);
if (eatingWindowHours < 0) {
eatingWindowHours += 24; // 跨天处理
}
if (eatingWindowHours != 10) {
return Result.error("进食窗口应为10小时,请重新设置");
}
// 更新用户设置
user.setFastingStartTime(startTime);
user.setFastingEndTime(endTime);
userService.updateById(user);
return Result.success("禁食计划设置成功");
} catch (DateTimeParseException e) {
return Result.error("时间格式错误,请使用HH:mm格式");
}
}
}
3.6 微信小程序登录集成
java
// WechatController.java
@RestController
@RequestMapping("/api/wechat")
@Slf4j
public class WechatController {
@Value("${wechat.app-id}")
private String appId;
@Value("${wechat.app-secret}")
private String appSecret;
@Autowired
private UserService userService;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 微信小程序登录
*/
@PostMapping("/login")
public Result wechatLogin(@RequestBody WechatLoginDTO loginDTO) {
try {
// 验证code
String code = loginDTO.getCode();
if (StringUtils.isBlank(code)) {
return Result.error("code不能为空");
}
// 调用微信接口获取openid和session_key
String url = String.format(
"https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
appId, appSecret, code);
String response = HttpUtil.get(url);
JSONObject jsonObject = JSONObject.parseObject(response);
String openid = jsonObject.getString("openid");
String sessionKey = jsonObject.getString("session_key");
if (StringUtils.isBlank(openid)) {
log.error("微信登录失败:{}", jsonObject.getString("errmsg"));
return Result.error("微信登录失败");
}
// 查找或创建用户
User user = userService.findByOpenid(openid);
boolean isNewUser = false;
if (user == null) {
// 新用户注册
user = new User();
user.setOpenid(openid);
user.setCreateTime(new Date());
userService.save(user);
isNewUser = true;
}
// 更新用户信息(如果有)
if (loginDTO.getUserInfo() != null) {
WechatUserInfo userInfo = loginDTO.getUserInfo();
user.setNickname(userInfo.getNickName());
user.setAvatarUrl(userInfo.getAvatarUrl());
user.setGender(userInfo.getGender());
userService.updateById(user);
}
// 生成自定义登录态
String token = generateToken(user.getId(), openid);
// 缓存用户信息
cacheUserInfo(token, user);
// 返回登录结果
LoginResultVO result = new LoginResultVO();
result.setToken(token);
result.setUserId(user.getId());
result.setIsNewUser(isNewUser);
result.setUserInfo(user);
log.info("用户登录成功:{}", user.getId());
return Result.success("登录成功", result);
} catch (Exception e) {
log.error("微信登录失败", e);
return Result.error("登录失败");
}
}
/**
* 生成JWT token
*/
private String generateToken(Long userId, String openid) {
// 这里可以使用JWT,简化起见使用UUID
String token = UUID.randomUUID().toString().replace("-", "");
// 存储token和用户信息的关联
UserToken userToken = new UserToken();
userToken.setUserId(userId);
userToken.setOpenid(openid);
userToken.setToken(token);
userToken.setCreateTime(new Date());
userToken.setExpireTime(DateUtil.offsetHour(new Date(), 24)); // 24小时过期
redisTemplate.opsForValue().set("token:" + token, userToken, 24, TimeUnit.HOURS);
return token;
}
private void cacheUserInfo(String token, User user) {
redisTemplate.opsForValue().set("user:" + token, user, 24, TimeUnit.HOURS);
}
/**
* 验证token
*/
@GetMapping("/verify")
public Result verifyToken(@RequestHeader("Authorization") String token) {
UserToken userToken = (UserToken) redisTemplate.opsForValue().get("token:" + token);
if (userToken == null || userToken.getExpireTime().before(new Date())) {
return Result.error(401, "token无效或已过期");
}
// 更新token过期时间
userToken.setExpireTime(DateUtil.offsetHour(new Date(), 24));
redisTemplate.opsForValue().set("token:" + token, userToken, 24, TimeUnit.HOURS);
return Result.success("token有效");
}
}
四、微信小程序前端实现
4.1 项目结构
miniprogram/
├── pages/
│ ├── index/ # 首页
│ ├── fasting/ # 禁食页面
│ ├── diet/ # 饮食记录
│ ├── statistics/ # 统计数据
│ └── profile/ # 个人中心
├── components/ # 组件
├── utils/ # 工具类
├── services/ # API服务
├── config/ # 配置
└── app.json # 小程序配置
4.2 核心页面实现
javascript
// pages/fasting/fasting.js
Page({
data: {
fastingStatus: null,
isFasting: false,
remainingTime: '00:00:00',
progress: 0,
todayFastingHours: 0,
isEatingWindow: true,
timer: null
},
onLoad() {
this.loadFastingStatus();
this.startTimer();
},
onUnload() {
if (this.data.timer) {
clearInterval(this.data.timer);
}
},
// 加载禁食状态
loadFastingStatus() {
wx.showLoading({ title: '加载中...' });
wx.request({
url: 'https://your-api.com/api/fasting/today',
header: {
'Authorization': wx.getStorageSync('token')
},
success: (res) => {
if (res.data.code === 200) {
this.setData({
fastingStatus: res.data.data,
isFasting: res.data.data.status === 1
});
if (this.data.isFasting) {
this.updateRemainingTime();
}
}
},
complete: () => {
wx.hideLoading();
}
});
},
// 开始禁食
startFasting() {
wx.showModal({
title: '提示',
content: '确定要开始禁食吗?',
success: (res) => {
if (res.confirm) {
const startTime = new Date().toISOString();
wx.request({
url: 'https://your-api.com/api/fasting/start',
method: 'POST',
header: {
'Authorization': wx.getStorageSync('token'),
'Content-Type': 'application/json'
},
data: {
startTime: startTime
},
success: (res) => {
if (res.data.code === 200) {
wx.showToast({ title: '禁食开始!', icon: 'success' });
this.loadFastingStatus();
} else {
wx.showToast({ title: res.data.msg, icon: 'error' });
}
}
});
}
}
});
},
// 结束禁食
endFasting() {
wx.showModal({
title: '提示',
content: '确定要结束禁食吗?',
success: (res) => {
if (res.confirm) {
const endTime = new Date().toISOString();
wx.request({
url: 'https://your-api.com/api/fasting/end',
method: 'POST',
header: {
'Authorization': wx.getStorageSync('token'),
'Content-Type': 'application/json'
},
data: {
endTime: endTime
},
success: (res) => {
if (res.data.code === 200) {
wx.showToast({
title: res.data.msg || '禁食结束!',
icon: 'success'
});
this.loadFastingStatus();
} else {
wx.showToast({ title: res.data.msg, icon: 'error' });
}
}
});
}
}
});
},
// 开始计时器
startTimer() {
this.data.timer = setInterval(() => {
if (this.data.isFasting) {
this.updateRemainingTime();
}
}, 1000);
},
// 更新剩余时间
updateRemainingTime() {
if (!this.data.fastingStatus || !this.data.fastingStatus.fastingStartTime) {
return;
}
const startTime = new Date(this.data.fastingStatus.fastingStartTime).getTime();
const now = new Date().getTime();
const targetTime = startTime + (14 * 60 * 60 * 1000);
if (now >= targetTime) {
this.setData({
remainingTime: '00:00:00',
progress: 100
});
return;
}
const remaining = targetTime - now;
const hours = Math.floor(remaining / (1000 * 60 * 60));
const minutes = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((remaining % (1000 * 60)) / 1000);
const progress = Math.floor((now - startTime) / (14 * 60 * 60 * 1000) * 100);
this.setData({
remainingTime: `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`,
progress: Math.min(progress, 100)
});
}
});
4.3 配置文件
json
// app.json
{
"pages": [
"pages/index/index",
"pages/fasting/fasting",
"pages/diet/diet",
"pages/statistics/statistics",
"pages/profile/profile"
],
"window": {
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#4CAF50",
"navigationBarTitleText": "轻断食助手",
"navigationBarTextStyle": "white"
},
"tabBar": {
"color": "#999999",
"selectedColor": "#4CAF50",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页",
"iconPath": "images/home.png",
"selectedIconPath": "images/home-active.png"
},
{
"pagePath": "pages/fasting/fasting",
"text": "断食",
"iconPath": "images/fasting.png",
"selectedIconPath": "images/fasting-active.png"
},
{
"pagePath": "pages/diet/diet",
"text": "饮食",
"iconPath": "images/diet.png",
"selectedIconPath": "images/diet-active.png"
},
{
"pagePath": "pages/statistics/statistics",
"text": "统计",
"iconPath": "images/stats.png",
"selectedIconPath": "images/stats-active.png"
},
{
"pagePath": "pages/profile/profile",
"text": "我的",
"iconPath": "images/profile.png",
"selectedIconPath": "images/profile-active.png"
}
]
},
"requiredPrivateInfos": [
"chooseAddress",
"chooseLocation"
]
}
五、部署与运维
5.1 Docker部署配置
dockerfile
# Dockerfile
FROM openjdk:11-jre-slim
VOLUME /tmp
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
yaml
# docker-compose.yml
version: '3.8'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: fasting123
MYSQL_DATABASE: fasting_db
MYSQL_USER: fasting_user
MYSQL_PASSWORD: fasting123
ports:
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- fasting-network
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis-data:/data
networks:
- fasting-network
backend:
build: .
ports:
- "8080:8080"
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/fasting_db?useSSL=false&serverTimezone=Asia/Shanghai
SPRING_DATASOURCE_USERNAME: fasting_user
SPRING_DATASOURCE_PASSWORD: fasting123
SPRING_REDIS_HOST: redis
SPRING_REDIS_PORT: 6379
depends_on:
- mysql
- redis
networks:
- fasting-network
volumes:
mysql-data:
redis-data:
networks:
fasting-network:
driver: bridge
5.2 Nginx配置
nginx
server {
listen 80;
server_name your-domain.com;
# 后端API
location /api/ {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 静态资源
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
# 小程序相关
location /mp/ {
alias /var/www/miniprogram/;
}
}
六、安全与性能优化
6.1 安全措施
- 接口鉴权:JWT token验证
- SQL注入防护:使用MyBatis Plus参数绑定
- XSS防护:输入输出过滤
- CSRF防护:验证请求来源
- 敏感数据加密:用户密码等敏感信息加密存储
6.2 性能优化
- 数据库优化:合理使用索引,查询优化
- 缓存策略:Redis缓存热点数据
- 连接池:使用Druid连接池
- 异步处理:非核心业务异步处理
- CDN加速:静态资源CDN分发
6.3 监控告警
- 应用监控:Spring Boot Actuator
- 日志收集:ELK Stack
- 异常告警:短信/邮件通知
- 性能监控:APM工具
七、项目总结
本项目实现了一个完整的轻断食14+7方案小程序系统,具有以下特点:
- 完整的用户系统:支持微信授权登录,用户信息管理
- 科学的断食计划:14+7方案实施,智能提醒
- 全面的数据记录:饮食、体重、断食记录一体化
- 丰富的可视化:数据图表展示,进度可视化
- 个性化建议:根据用户数据提供个性化建议
- 社区互动:用户分享、打卡挑战
系统采用前后端分离架构,后端使用Spring Boot框架,前端使用微信小程序原生开发,具有良好的扩展性和维护性。项目代码结构清晰,注释完整,符合编码规范,便于后续开发和维护。
该方案不仅实现了基本的断食计时功能,还通过数据分析为用户提供了科学的健康管理工具,帮助用户更好地执行轻断食计划,实现健康生活目标。