# fitlog-server **Repository Path**: mml520/fitlog-server ## Basic Information - **Project Name**: fitlog-server - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-01-06 - **Last Updated**: 2026-01-07 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # FitLog AI 系统后端文档 ## 1. 项目简介 **FitLog AI** 是一个专为健身爱好者设计的全栈式训练与饮食追踪应用。它集成了 Google Gemini AI 能力,支持自然语言日志解析、食物图片识别以及基于个人历史数据的流式 AI 教练指导。 ## 2. 核心功能模块 ### A. 训练管理 (Workout Tracking) - **手动录入**:支持选择训练部位(胸、背、肩、腿等)、记录动作名称、组数、次数及备注。 - **多媒体记录**:支持为每个动作上传训练快照(图片)。 - **智能解析 (Smart Input)**:用户输入一段话(如:“今天练胸,卧推100kg 5x5”),AI 自动解析并填充表单。 - **可视化统计**:通过饼图展示训练部位占比,通过柱状图展示近期的训练频率与强度。 ### B. 饮食追踪 (Diet Tracking) - **每日饮食记录**:记录早餐、午餐、晚餐及加餐。 - **AI 食物识别**:拍摄或上传食物图片,AI 自动识别食物名称并估算热量(kcal)。 - **补水记录**:追踪每日饮水量。 ### C. AI 教练对话 (AI Analysis & Coach) - **流式对话 (SSE)**:基于个人训练和饮食历史,提供实时的改进建议。 - **深度复盘**:一键生成本周/本月的训练报告。 - **分享海报**:支持将 AI 生成的专业分析报告转化为精美海报并分享。 ### D. 离线与体验 (PWA) - **离线访问**:支持 Service Worker 缓存,具备基础的 PWA 能力。 - **手势操作**:列表支持左滑删除,交互流畅。 ------ ## 3. 接口规范 (API Reference) 所有接口的基础地址为:https://aaa.kiss666.site/api ### 3.1 训练相关 | 接口名称 | 方法 | 路径 | 描述 | | ------------- | ------ | -------------- | ------------------------------ | | 获取训练列表 | GET | /workouts | 获取所有已保存的训练日志 | | 保存/更新训练 | POST | /workouts | 提交新的训练日志或更新现有日志 | | 删除训练 | DELETE | /workouts/{id} | 根据 ID 删除指定训练记录 | ### 3.2 饮食相关 | 接口名称 | 方法 | 路径 | 描述 | | ------------- | ------ | ----------- | ------------------------ | | 获取饮食列表 | GET | /diets | 获取所有饮食记录 | | 保存/更新饮食 | POST | /diets | 提交每日饮食与补水数据 | | 删除饮食 | DELETE | /diets/{id} | 根据 ID 删除指定饮食记录 | ### 3.3 AI 服务 (核心逻辑) | 接口名称 | 方法 | 路径 | 描述 | | ---------------- | ---- | ------------------ | ------------------------------------------------------------ | | **AI 流式对话** | POST | /ai/chat | 传入历史记录、训练数据、饮食数据。采用 **SSE (Server-Sent Events)** 返回流式文本。 | | **食物识别** | POST | /ai/recognize-food | 传入图片的 Base64 数据。返回食物描述及预估热量。 | | **自然语言解析** | POST | /ai/parse-log | 传入一段非结构化文本。返回解析后的 JSON 结构化训练数据。 | ### 3.4 文件服务 | 接口名称 | 方法 | 路径 | 描述 | | -------- | ---- | --------------------------------------- | ------------------------------------------------------ | | 文件上传 | POST | /files/upload | 上传图片文件(multipart/form-data)。返回文件名/路径。 | | 图片访问 | GET | https://aaa.kiss666.site/uploads/{path} | 通过拼接路径直接访问静态资源图片。 | ## 4. 后端代码与架构 提供的是整体的代码 1. pom.xml ```xml 4.0.0 org.springframework.boot spring-boot-starter-parent 2.7.18 com.fitlog fitlog-ai-server 0.0.1-SNAPSHOT fitlog-ai-server FitLog AI Backend Service 17 3.5.9 org.springframework.boot spring-boot-starter-web com.baomidou mybatis-plus-boot-starter ${mybatis-plus.version} com.mysql mysql-connector-j runtime org.projectlombok lombok true org.springframework.boot spring-boot-starter-validation org.springframework.boot spring-boot-starter-test test org.apache.maven.plugins maven-compiler-plugin 3.8.1 ${java.version} ${java.version} org.projectlombok lombok ${lombok.version} org.springframework.boot spring-boot-maven-plugin ``` 2. application.yml ```yml spring: application: name: fitlog-ai-server datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.175.101:3306/fitlog?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true username: root password: root mybatis-plus: configuration: map-underscore-to-camel-case: true # 开启下划线转驼峰 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 输出 SQL 日志到控制台 global-config: db-config: id-type: assign_id # 使用雪花算法生成 ID logic-delete-field: deleted # 逻辑删除字段名(如需要) server: port: 8080 ``` 3. CorsConfig.java ```java package com.fitlog.ai.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.io.File; /** * 全局跨域配置 */ @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") // 使用 pattern 允许所有来源且支持 credentials .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 动态识别操作系统并设置路径 String os = System.getProperty("os.name").toLowerCase(); String path = os.contains("win") ? "D:/fitlog/uploads/" : "/home/fitlog/uploads/"; // 确保物理目录存在 File dir = new File(path); if (!dir.exists()) dir.mkdirs(); // 将 /uploads/** 映射到物理路径 registry.addResourceHandler("/uploads/**") .addResourceLocations("file:" + path); } } ``` 4. AIController.java ```java package com.fitlog.ai.controller; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.client.RestTemplate; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.net.InetSocketAddress; import java.net.Proxy; import java.net.ProxySelector; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; import java.time.LocalDate; import java.util.*; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.stream.Collectors; @RestController @RequestMapping("/api/ai") public class AIController { // 注意:Gemini 2.0 Flash 响应速度极快,建议使用 private final String API_KEY = "AIzaSyBENvqY_pIUiWJ88jUn9C8txhNAPaUcMLM"; private final String MODEL_NAME = "gemini-3-flash-preview"; private final ObjectMapper objectMapper = new ObjectMapper(); private final ExecutorService executor = Executors.newCachedThreadPool(); private final RestTemplate restTemplate; // 代理设置 private final String PROXY_HOST = "127.0.0.1"; private final int PROXY_PORT = 10809; public AIController() { org.springframework.http.client.SimpleClientHttpRequestFactory factory = new org.springframework.http.client.SimpleClientHttpRequestFactory(); factory.setProxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(PROXY_HOST, PROXY_PORT))); factory.setConnectTimeout(10000); factory.setReadTimeout(30000); this.restTemplate = new RestTemplate(factory); } /** * 构建系统指令和上下文 */ private String getContextPrompt(List> workouts, List> diets) { String trainingHistory = workouts.stream().map(w -> String.format( "\n 日期: %s\n 部位: %s\n 动作: %s\n 总结: %s\n", w.get("date"), w.get("targetMuscle"), ((List>) w.get("exercises")).stream() .map(e -> String.format("%s(%sx%s)%s", e.get("name"), e.get("sets"), e.get("reps"), e.get("notes") != null ? " 备注: " + e.get("notes") : "")) .collect(Collectors.joining(", ")), w.getOrDefault("summary", "无") )).collect(Collectors.joining("\n---\n")); String dietHistory = diets.stream().map(d -> String.format( "\n 日期: %s\n 饮食: %s\n 饮水: %s ml\n 备注: %s\n", d.get("date"), ((List>) d.get("meals")).stream() .map(m -> String.format("[%s] %s (%s kcal)", m.get("time"), m.get("description"), m.getOrDefault("calories", "未知"))) .collect(Collectors.joining("; ")), d.getOrDefault("waterIntake", "未知"), d.getOrDefault("notes", "无") )).collect(Collectors.joining("\n---\n")); return "你是一位顶级的私人健身教练和营养学专家。\n" + "以下是用户的最新训练数据:\n" + trainingHistory + "\n\n" + "以下是用户的最新饮食记录:\n" + dietHistory + "\n\n" + "请基于以上数据回答用户的问题。如果用户要求你进行“深度复盘”,请提供结构化的专业分析。\n" + "你的回答应该专业、科学、富有鼓励性,并尽可能使用 Markdown 格式(标题、加粗、列表)。"; } /** * 核心:流式对话接口 */ @PostMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter chat(@RequestBody Map payload) { // 设置超时时间为 0,表示永不超时 SseEmitter emitter = new SseEmitter(0L); executor.execute(() -> { try { String userMessage = (String) payload.get("userMessage"); List> history = (List>) payload.get("history"); List> workouts = (List>) payload.get("workouts"); List> diets = (List>) payload.get("diets"); // 构建请求体 Map reqBody = new HashMap<>(); List> contents = new ArrayList<>(); if (history != null) { for (Map m : history) { contents.add(Map.of("role", m.get("role"), "parts", List.of(Map.of("text", m.get("content"))))); } } contents.add(Map.of("role", "user", "parts", List.of(Map.of("text", userMessage)))); reqBody.put("contents", contents); reqBody.put("system_instruction", Map.of("parts", List.of(Map.of("text", getContextPrompt(workouts, diets))))); // 启用思考模型(可选) reqBody.put("generationConfig", Map.of("temperature", 0.7)); // 使用 HttpClient 进行流式请求 HttpClient client = HttpClient.newBuilder() .proxy(ProxySelector.of(new InetSocketAddress(PROXY_HOST, PROXY_PORT))) .connectTimeout(Duration.ofSeconds(10)) .build(); String streamUrl = "https://generativelanguage.googleapis.com/v1beta/models/" + MODEL_NAME + ":streamGenerateContent?key=" + API_KEY; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(streamUrl)) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(reqBody))) .build(); // 异步发送并处理响应流 client.sendAsync(request, HttpResponse.BodyHandlers.ofLines()) .thenAccept(response -> { response.body().forEach(line -> { try { if (line.trim().isEmpty()) return; // Gemini 的流式返回每行都是一个 JSON 块,包含在 [ ... ] 中 // 我们需要提取出其中的 text 部分 JsonNode node = objectMapper.readTree(line.replaceFirst("^\\s*,", "").replaceFirst("^\\[", "").replaceFirst("\\]$", "")); String text = node.path("candidates").get(0).path("content").path("parts").get(0).path("text").asText(); if (text != null && !text.isEmpty()) { // 发送给前端,注意 SSE 格式:data: 内容\n\n emitter.send(SseEmitter.event().data(text)); } } catch (Exception e) { // 忽略解析错误的行(如数组首尾的括号) } }); emitter.complete(); }) .exceptionally(ex -> { emitter.completeWithError(ex); return null; }); } catch (Exception e) { emitter.completeWithError(e); } }); return emitter; } /** * 食物识别(保持非流式,因为是 JSON 结构化数据) */ @PostMapping("/recognize-food") public Map recognizeFood(@RequestBody Map payload) { try { String url = "https://generativelanguage.googleapis.com/v1beta/models/" + MODEL_NAME + ":generateContent?key=" + API_KEY; Map req = new HashMap<>(); req.put("contents", List.of(Map.of("parts", List.of( Map.of("inline_data", Map.of("mime_type", "image/jpeg", "data", payload.get("image"))), Map.of("text", "Identify food and estimate calories. Return JSON with 'description' and 'calories'.") )))); req.put("generationConfig", Map.of("response_mime_type", "application/json")); ResponseEntity res = restTemplate.postForEntity(url, req, Map.class); String json = (String) ((Map)((List)((Map)((Map)((List)res.getBody().get("candidates")).get(0)).get("content")).get("parts")).get(0)).get("text"); return objectMapper.readValue(json, Map.class); } catch (Exception e) { return Map.of("error", e.getMessage()); } } /** * 智能日志解析 */ @PostMapping("/parse-log") public Map parseLog(@RequestBody Map payload) { try { String url = "https://generativelanguage.googleapis.com/v1beta/models/" + MODEL_NAME + ":generateContent?key=" + API_KEY; String today = LocalDate.now().toString(); String prompt = "解析健身日志并返回JSON。当前日期: " + today + "\n输入: " + payload.get("text"); Map req = new HashMap<>(); req.put("contents", List.of(Map.of("parts", List.of(Map.of("text", prompt))))); req.put("generationConfig", Map.of("response_mime_type", "application/json")); ResponseEntity res = restTemplate.postForEntity(url, req, Map.class); String json = (String) ((Map)((List)((Map)((Map)((List)res.getBody().get("candidates")).get(0)).get("content")).get("parts")).get(0)).get("text"); return objectMapper.readValue(json, Map.class); } catch (Exception e) { return Map.of("error", e.getMessage()); } } } ``` 5. DietController ```java package com.fitlog.ai.controller; import com.fitlog.ai.entity.Diet; import com.fitlog.ai.service.IDietService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; /** * 饮食记录 控制器 */ @RestController @RequestMapping("/api/diets") public class DietController { @Autowired private IDietService dietService; @GetMapping public List getAll() { return dietService.list(); } @GetMapping("/{id}") public Diet getById(@PathVariable String id) { return dietService.getById(id); } @PostMapping public Diet save(@RequestBody Diet diet) { dietService.saveOrUpdate(diet); return diet; } @PutMapping public Diet update(@RequestBody Diet diet) { dietService.updateById(diet); return diet; } @DeleteMapping("/{id}") public boolean delete(@PathVariable String id) { return dietService.removeById(id); } } ``` 6. FileUploadController ```java package com.fitlog.ai.controller; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.io.IOException; import java.util.UUID; @RestController @RequestMapping("/api/files") public class FileUploadController { @PostMapping("/upload") public String upload(@RequestParam("file") MultipartFile file) throws IOException { if (file.isEmpty()) return "Error: File is empty"; // 动态路径选择 String os = System.getProperty("os.name").toLowerCase(); String uploadPath = os.contains("win") ? "D:/fitlog/uploads/" : "/home/fitlog/uploads/"; // 确保目录存在 File dir = new File(uploadPath); if (!dir.exists()) dir.mkdirs(); // 保持文件名唯一并防止中文乱码/特殊字符 String fileName = UUID.randomUUID().toString() + "_" + file.getOriginalFilename().replaceAll("\\s", "_"); // 保存到物理路径 file.transferTo(new File(uploadPath + fileName)); // 返回浏览器可直接访问的 URL return fileName; } } ``` 7. WorkoutController ```java package com.fitlog.ai.controller; import com.fitlog.ai.entity.Workout; import com.fitlog.ai.service.IWorkoutService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; /** * 训练记录 控制器 */ @RestController @RequestMapping("/api/workouts") public class WorkoutController { @Autowired private IWorkoutService workoutService; @GetMapping public List getAll() { return workoutService.list(); } @GetMapping("/{id}") public Workout getById(@PathVariable String id) { return workoutService.getById(id); } @PostMapping public Workout save(@RequestBody Workout workout) { // MyBatis-Plus 的 saveOrUpdate 会根据 ID 是否存在自动选择 Insert 或 Update workoutService.saveOrUpdate(workout); return workout; } @PutMapping public Workout update(@RequestBody Workout workout) { workoutService.updateById(workout); return workout; } @DeleteMapping("/{id}") public boolean delete(@PathVariable String id) { return workoutService.removeById(id); } } ``` 8. Diet 9. ```java package com.fitlog.ai.entity; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import lombok.Data; import java.time.LocalDate; import java.util.List; /** * 饮食记录实体 */ @Data @TableName(value = "diets", autoResultMap = true) public class Diet { @TableId private String id; private LocalDate date; /** * 使用 JacksonTypeHandler 自动转换 JSON 为 List */ @TableField(typeHandler = JacksonTypeHandler.class) private List meals; private Integer waterIntake; private String notes; } ``` 10. Exercise 11. ```java package com.fitlog.ai.entity; import lombok.Data; import java.util.List; /** * 训练中的具体动作模型 */ @Data public class Exercise { private String id; private String name; private String sets; private String reps; private String notes; private Boolean completed; private List images; // 存储 Base64 或图片链接 } ``` 12. Meal ```java package com.fitlog.ai.entity; import lombok.Data; /** * 饮食中的餐次模型 */ @Data public class Meal { private String id; private String time; private String description; private String calories; private String image; } ``` 13. Workout 14. ```java package com.fitlog.ai.entity; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import lombok.Data; import java.time.LocalDate; import java.util.List; /** * 训练记录实体 */ @Data @TableName(value = "workouts", autoResultMap = true) public class Workout { @TableId private String id; private LocalDate date; private String targetMuscle; private String summary; /** * 使用 JacksonTypeHandler 将 JSON 字符串自动转换为 List */ @TableField(typeHandler = JacksonTypeHandler.class) private List exercises; } ``` 15. DietMapper 16. ```java package com.fitlog.ai.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.fitlog.ai.entity.Diet; import org.apache.ibatis.annotations.Mapper; /** * 饮食记录 Mapper 接口 */ @Mapper public interface DietMapper extends BaseMapper { } ``` 17. WorkoutMapper 18. ```java package com.fitlog.ai.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.fitlog.ai.entity.Workout; import org.apache.ibatis.annotations.Mapper; /** * 训练记录 Mapper 接口 */ @Mapper public interface WorkoutMapper extends BaseMapper { } ``` 19. IDietService 20. ```java package com.fitlog.ai.service; import com.baomidou.mybatisplus.extension.service.IService; import com.fitlog.ai.entity.Diet; /** * 饮食记录服务类 */ public interface IDietService extends IService { } ``` 21. IWorkoutService 22. ```java package com.fitlog.ai.service; import com.baomidou.mybatisplus.extension.service.IService; import com.fitlog.ai.entity.Workout; /** * 训练记录服务类 */ public interface IWorkoutService extends IService { } ``` 23. DietServiceImpl 24. ```java package com.fitlog.ai.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.fitlog.ai.entity.Diet; import com.fitlog.ai.mapper.DietMapper; import com.fitlog.ai.service.IDietService; import org.springframework.stereotype.Service; /** * 饮食记录服务实现类 */ @Service public class DietServiceImpl extends ServiceImpl implements IDietService { } ``` 25. WorkoutServiceImpl 26. ```java package com.fitlog.ai.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.fitlog.ai.entity.Workout; import com.fitlog.ai.mapper.WorkoutMapper; import com.fitlog.ai.service.IWorkoutService; import org.springframework.stereotype.Service; /** * 训练记录服务实现类 */ @Service public class WorkoutServiceImpl extends ServiceImpl implements IWorkoutService { } ``` 27. FitLogApplication 28. ```java package com.fitlog.ai; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class FitLogApplication { public static void main(String[] args) { SpringApplication.run(FitLogApplication.class, args); } } ```