轻断食14+7方案小程序项目 - 完整实现方案

轻断食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 功能模块

  1. 用户管理模块

    • 微信授权登录
    • 用户信息管理
    • 健康数据记录
  2. 断食计划模块

    • 14+7计划设置
    • 实时计时器
    • 进食窗口提醒
  3. 饮食记录模块

    • 食物库管理
    • 热量计算
    • 营养成分分析
  4. 健康数据模块

    • 体重追踪
    • 身体指标记录
    • 进度可视化
  5. 社区互动模块

    • 经验分享
    • 打卡挑战
    • 专家建议

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 安全措施

  1. 接口鉴权:JWT token验证
  2. SQL注入防护:使用MyBatis Plus参数绑定
  3. XSS防护:输入输出过滤
  4. CSRF防护:验证请求来源
  5. 敏感数据加密:用户密码等敏感信息加密存储

6.2 性能优化

  1. 数据库优化:合理使用索引,查询优化
  2. 缓存策略:Redis缓存热点数据
  3. 连接池:使用Druid连接池
  4. 异步处理:非核心业务异步处理
  5. CDN加速:静态资源CDN分发

6.3 监控告警

  1. 应用监控:Spring Boot Actuator
  2. 日志收集:ELK Stack
  3. 异常告警:短信/邮件通知
  4. 性能监控:APM工具

七、项目总结

本项目实现了一个完整的轻断食14+7方案小程序系统,具有以下特点:

  1. 完整的用户系统:支持微信授权登录,用户信息管理
  2. 科学的断食计划:14+7方案实施,智能提醒
  3. 全面的数据记录:饮食、体重、断食记录一体化
  4. 丰富的可视化:数据图表展示,进度可视化
  5. 个性化建议:根据用户数据提供个性化建议
  6. 社区互动:用户分享、打卡挑战

系统采用前后端分离架构,后端使用Spring Boot框架,前端使用微信小程序原生开发,具有良好的扩展性和维护性。项目代码结构清晰,注释完整,符合编码规范,便于后续开发和维护。

该方案不仅实现了基本的断食计时功能,还通过数据分析为用户提供了科学的健康管理工具,帮助用户更好地执行轻断食计划,实现健康生活目标。

相关推荐
2501_915918419 小时前
HTTPS 代理失效,启用双向认证(mTLS)的 iOS 应用网络怎么抓包调试
android·网络·ios·小程序·https·uni-app·iphone
数字游民952710 小时前
半年时间大概上了70个web站和小程序,累计提示词超过20w
人工智能·ai·小程序·vibecoding·数字游民9527
说私域10 小时前
微商企业未来迭代的核心方向与多元探索——以链动2+1模式AI智能名片商城小程序为核心支撑
大数据·人工智能·小程序·流量运营·私域运营
276695829211 小时前
美团 小程序 mtgsig
python·小程序·node·js·mtgsig1.2·美团小程序·大众点评小程序
2501_9151063211 小时前
混合应用(Hybrid)安全加固,不依赖源码对成品 IPA 混淆
android·安全·小程序·https·uni-app·iphone·webview
00后程序员张12 小时前
在 iOS 上架中如何批量方便快捷管理 Bundle ID
android·ios·小程序·https·uni-app·iphone·webview
韩立学长12 小时前
【开题答辩实录分享】以《智慧校园平台微信小程序》为例进行选题答辩实录分享
spring boot·微信小程序·小程序
CHU72903512 小时前
旧物回收小程序前端功能设计:以用户为核心构建环保便捷通道
小程序
h_654321012 小时前
微信小程序:按顺序一张图片加载完后,再加载另一张
微信小程序·小程序
小小王app小程序开发13 小时前
无限赏抽赏小程序核心玩法拆解与技术运营分析
小程序