Initial commit: MSH System\n\n- msh_single_uniapp: Vue 2 + UniApp 前端(微信小程序/H5/App/支付宝小程序)\n- msh_crmeb_22: Spring Boot 2.2 后端(C端API/管理端/业务逻辑)\n- models-integration: AI服务集成(Coze/KieAI/腾讯ASR)\n- docs: 产品文档与设计稿

This commit is contained in:
2026-02-28 05:40:21 +08:00
commit 14d29d51c0
2182 changed files with 482509 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

12
.cursor/debug.log Normal file
View File

@@ -0,0 +1,12 @@
{"location":"UploadServiceImpl.commonUpload","message":"before createFile","data":{"rootPath":"/Users/apple/.crmeb_upload/","webPath":"crmebimage/public/checkin/2026/02/14/","destPath":"/Users/apple/.crmeb_upload/crmebimage/public/checkin/2026/02/14/63e7b8e86e0147abafd057a1dd520732f69kuixvgh.jpg"},"timestamp":1771054614504,"hypothesisId":"H1,H2,H4","runId":"post-fix"}
{"location":"UploadServiceImpl.commonUpload","message":"before createFile","data":{"rootPath":"/Users/apple/.crmeb_upload/","webPath":"crmebimage/public/audio/2026/02/14/","destPath":"/Users/apple/.crmeb_upload/crmebimage/public/audio/2026/02/14/92e75695b6934eef879a8067835bdf130gl7q8lus2.mp3"},"timestamp":1771054622634,"hypothesisId":"H1,H2,H4","runId":"post-fix"}
{"data":{"rawValue":true,"setToUserSign":1,"parsedBoolean":true},"hypothesisId":"E","location":"ToolCheckinServiceImpl.java:135","id":"log_1771054634346_b","message":"enableAIVideo parsed","timestamp":1771054634346}
{"location":"UploadServiceImpl.commonUpload","message":"before createFile","data":{"rootPath":"/Users/apple/.crmeb_upload/","webPath":"crmebimage/public/checkin/2026/02/14/","destPath":"/Users/apple/.crmeb_upload/crmebimage/public/checkin/2026/02/14/dd52bbd508044ac699acf0e1be8ab7dd77ns0z9rf1.jpg"},"timestamp":1771054891217,"hypothesisId":"H1,H2,H4","runId":"post-fix"}
{"location":"UploadServiceImpl.commonUpload","message":"before createFile","data":{"rootPath":"/Users/apple/.crmeb_upload/","webPath":"crmebimage/public/audio/2026/02/14/","destPath":"/Users/apple/.crmeb_upload/crmebimage/public/audio/2026/02/14/a87b47ddf8fc4172963747c9b8a1e4105kshwyiubg.mp3"},"timestamp":1771054902191,"hypothesisId":"H1,H2,H4","runId":"post-fix"}
{"data":{"rawValue":false,"setToUserSign":0,"parsedBoolean":false},"hypothesisId":"E","location":"ToolCheckinServiceImpl.java:135","id":"log_1771054906349_b","message":"enableAIVideo parsed","timestamp":1771054906349}
{"location":"UploadServiceImpl.commonUpload","message":"before createFile","data":{"rootPath":"/Users/apple/.crmeb_upload/","webPath":"crmebimage/public/checkin/2026/02/14/","destPath":"/Users/apple/.crmeb_upload/crmebimage/public/checkin/2026/02/14/2728953d15ba4e26a7c1a799a4c15b49elt7yjci5x.png"},"timestamp":1771054919713,"hypothesisId":"H1,H2,H4","runId":"post-fix"}
{"location":"UploadServiceImpl.commonUpload","message":"before createFile","data":{"rootPath":"/Users/apple/.crmeb_upload/","webPath":"crmebimage/public/audio/2026/02/14/","destPath":"/Users/apple/.crmeb_upload/crmebimage/public/audio/2026/02/14/73e142306c5b4f5ea5b93171b8ce54f370chfc8lz6.mp3"},"timestamp":1771054927494,"hypothesisId":"H1,H2,H4","runId":"post-fix"}
{"data":{"rawValue":true,"setToUserSign":1,"parsedBoolean":true},"hypothesisId":"E","location":"ToolCheckinServiceImpl.java:135","id":"log_1771054935503_b","message":"enableAIVideo parsed","timestamp":1771054935503}
{"location":"UploadServiceImpl.commonUpload","message":"before createFile","data":{"rootPath":"/Users/apple/.crmeb_upload/","webPath":"crmebimage/public/checkin/2026/02/14/","destPath":"/Users/apple/.crmeb_upload/crmebimage/public/checkin/2026/02/14/db082b45736e42e08c8cecd15798090eexut37l8lp.jpg"},"timestamp":1771055166731,"hypothesisId":"H1,H2,H4","runId":"post-fix"}
{"location":"UploadServiceImpl.commonUpload","message":"before createFile","data":{"rootPath":"/Users/apple/.crmeb_upload/","webPath":"crmebimage/public/audio/2026/02/14/","destPath":"/Users/apple/.crmeb_upload/crmebimage/public/audio/2026/02/14/e75ddab2e25d40b492e2036e2f44301fcuri32duxq.mp3"},"timestamp":1771055173015,"hypothesisId":"H1,H2,H4","runId":"post-fix"}
{"data":{"rawValue":true,"setToUserSign":1,"parsedBoolean":true},"hypothesisId":"E","location":"ToolCheckinServiceImpl.java:135","id":"log_1771055180778_b","message":"enableAIVideo parsed","timestamp":1771055180778}

View File

@@ -0,0 +1,111 @@
---
name: eGFR fix and video gen
overview: Fix the eGFR field not appearing in calculator results due to a Lombok/Jackson serialization mismatch, and ensure the check-in page correctly calls the KieAI API to generate sora videos when the toggle is enabled.
todos:
- id: fix-egfr-serialization
content: Add @JsonProperty("eGFR") to HealthData.eGFR field in NutritionCalculateResponse.java to fix JSON serialization mismatch
status: completed
- id: fix-nframes-hardcode
content: Fix n_frames hardcoded value in ToolSora2ServiceImpl.java to use the request input value instead of always "15"
status: completed
- id: verify-checkin-video-flow
content: Verify and fix the end-to-end video generation flow from checkin-publish.vue through KieAI API, ensuring taskId is properly stored in UserSign
status: completed
- id: improve-video-error-handling
content: Improve error handling and user feedback in checkin-publish.vue for video generation failures
status: completed
isProject: false
---
# Fix eGFR Display and Check-in Video Generation
## Issue 1: eGFR Not Showing in Calculator Results
### Root Cause
The field `private String eGFR` in `HealthData` uses Lombok `@Data`, which generates the getter `getEGFR()`. Jackson follows Java Beans naming convention: when the first two characters after stripping "get" are both uppercase ("EG"), the property name stays as-is: `"EGFR"`. The frontend expects `data.healthData.eGFR` but receives `data.healthData.EGFR`, resulting in `undefined` and displaying `--`.
```mermaid
sequenceDiagram
participant FE as Frontend
participant BE as Backend
FE->>BE: GET /tool/calculator/result/{id}
BE->>BE: buildResponse() sets healthData.setEGFR(...)
BE-->>FE: JSON: {"healthData":{"EGFR":"7.9",...}}
FE->>FE: healthData.eGFR is undefined
FE->>FE: Displays "--"
```
### Fix
Add `@JsonProperty("eGFR")` annotation to the `eGFR` field in [NutritionCalculateResponse.java](msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/response/NutritionCalculateResponse.java) (line 55):
```java
@JsonProperty("eGFR")
@ApiModelProperty(value = "eGFR数值ml/min/1.73m²)", example = "7.9")
private String eGFR;
```
This forces Jackson to use the exact field name `"eGFR"` in JSON output, matching the frontend's expectation.
---
## Issue 2: Check-in Page Video Generation
### Current State
The implementation already exists in [checkin-publish.vue](msh_single_uniapp/pages/tool/checkin-publish.vue) (lines 590-627). It calls `api.createImageToVideoTask()` / `api.createTextToVideoTask()` from [models-api.js](msh_single_uniapp/api/models-api.js) which hit the backend [KieAIController.java](msh_crmeb_22/crmeb-front/src/main/java/com/zbkj/front/controller/KieAIController.java) endpoints at `/api/front/kieai/image-to-video` and `/api/front/kieai/text-to-video`.
Auth is not required (kieai endpoints are excluded from the interceptor in [WebConfig.java](msh_crmeb_22/crmeb-front/src/main/java/com/zbkj/front/config/WebConfig.java) line 98).
```mermaid
sequenceDiagram
participant User
participant CheckinPage as checkin-publish.vue
participant ModelsAPI as models-api.js
participant Backend as KieAIController
participant Sora2 as ToolSora2ServiceImpl
participant KieAI as KieAI API
User->>CheckinPage: Toggle "生成打卡视频" ON, click 发布
CheckinPage->>ModelsAPI: createImageToVideoTask({imageUrl, prompt, uid})
ModelsAPI->>Backend: POST /api/front/kieai/image-to-video
Backend->>Sora2: createImageToVideoTask(...)
Sora2->>KieAI: Upload image to KieAI
Sora2->>KieAI: POST /api/v1/jobs/createTask (sora-2-image-to-video)
KieAI-->>Sora2: {taskId: "..."}
Sora2->>Sora2: Save Article record with taskId
Sora2-->>Backend: taskId
Backend-->>ModelsAPI: CommonResult{code:200, data: taskId}
ModelsAPI-->>CheckinPage: response
CheckinPage->>CheckinPage: submitCheckin({...taskId...})
```
### Issues to Fix
**a) Request parameter format bug in `models-api.js**`
The `createImageToVideoTask` function sends `image_urls` as `[params.imageUrl]` (single URL in array). However, the `Sora2Request.Input.image_urls` field is `String[]` -- this should work for deserialization, but the request body also sets `uid` as a potential number (from vuex). The `Sora2Request.uid` is `String` type. Jackson auto-coerces `number -> String`, so this is fine. No code change needed here.
**b) `n_frames` hardcoded in backend**
The backend [ToolSora2ServiceImpl.java](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolSora2ServiceImpl.java) hardcodes `input.put("n_frames", "15")` at lines 44 and 109, ignoring the value sent by the frontend (`5`). For check-in videos, a shorter duration (5 frames ~ 5 seconds) is more appropriate. Change to use the value from the request input.
**c) Video task result not linked to check-in in backend**
Currently the video task creates an `Article` record but has no direct link to the `UserSign` (check-in) record. The frontend passes `taskId` in the `submitCheckin` request, but this linkage should be verified in the backend `ToolCheckinServiceImpl` to properly store and retrieve the video for the check-in.
**d) Error handling improvement in frontend**
The current error handling silently fails. Improve feedback to the user when video generation fails.
### Changes
- **[ToolSora2ServiceImpl.java](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolSora2ServiceImpl.java)**: Use the `n_frames` value from the request input instead of hardcoding `"15"`. Both `createTextToVideoTask` (line 44) and `createImageToVideoTask` (line 109) need this fix.
- **[checkin-publish.vue](msh_single_uniapp/pages/tool/checkin-publish.vue)**: Improve video generation error handling and ensure proper prompt construction for nutritional video content.
- **Verify backend checkin service** stores the `taskId` from the video task in the `UserSign` record correctly.

View File

@@ -0,0 +1,475 @@
---
name: MealPlan Image AI Generation
overview: 将 models-integration 项目中 KieAI 图像/视频生成接口复刻到 crmeb-front并在 mealPlan 菜品图片 URL 无效时,通过 KieAI NanoBanana API 生成图片、上传 OSS、返回有效 URL。
todos:
- id: replicate-kieai-config
content: 在 crmeb-common 中新增 KieAIConfig 配置类,在 application-sophia.yml 中添加 kie-ai 配置
status: completed
- id: replicate-kieai-dtos
content: 在 crmeb-common 中复刻 KieAI 相关 DTOTextToImageInput, CreateTaskRequest, CreateTaskResponse, QueryTaskResponse, NanoBananaRequest, NanoBananaResponse 等)
status: completed
- id: replicate-kieai-helper
content: 在 crmeb-service 中复刻 NanoBananaHelperAPI 调用工具类)
status: completed
- id: replicate-kieai-service
content: 在 crmeb-service 中复刻 KieAI 图像生成 Service 接口及实现ToolKieAIService / ToolKieAIServiceImpl
status: completed
- id: replicate-kieai-controller
content: 在 crmeb-front 中新增 KieAIController暴露文生图、图编辑、任务查询等 REST 接口
status: completed
- id: create-cache-table
content: 创建 v2_dish_image_cache 数据库表及对应 MyBatis Entity/Dao/XML
status: completed
- id: extend-oss-service
content: 扩展 OssService 支持 InputStream 上传AI 生成的图片无需先落盘)
status: completed
- id: implement-dish-image-service
content: 实现 DishImageServiceURL 检测 + KieAI 文生图 + 轮询等待 + 下载图片 + OSS 上传 + 缓存写入
status: completed
- id: modify-calculator-service
content: 修改 ToolCalculatorServiceImpl.generateMealPlan() 集成 DishImageService
status: completed
- id: error-handling
content: 实现降级策略KieAI 生成失败时使用默认占位图,不阻断主流程
status: completed
isProject: false
---
# MealPlan 菜品图片 KieAI 生成 + OSS 上传方案
## 一、现状分析
### 1.1 问题
`[ToolCalculatorServiceImpl.generateMealPlan()](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCalculatorServiceImpl.java)`11 道菜品的图片 URL 是硬编码的,指向 `https://uthink2025.oss-cn-shanghai.aliyuncs.com/recipes/xxx.jpg`。如果这些图片在 OSS 上不存在,前端会显示裂图。
### 1.2 已有基础设施
- **crmeb 项目**: Coze API SDK (v0.2.3)、阿里云 OSS 上传 (`[OssServiceImpl](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/OssServiceImpl.java)`)、OSS 配置存储在 `system_config` 表中
- **models-integration 项目**: 已有完整的 KieAI 集成,包括:
- `NanoBanana` 文生图 API`POST {baseUrl}/predictions`
- `Sora2` 文生视频 / 图生视频 API`POST {baseUrl}/api/v1/jobs/createTask`
- 任务状态轮询、回调通知机制
- 文件上传接口(`POST {uploadBaseUrl}/api/file-url-upload`
### 1.3 KieAI API 核心信息
| 项目 | 值 |
| --------------- | ---------------------------------------------------- |
| API Base URL | `https://api.kie.ai` |
| Upload Base URL | `https://kieai.redpandaai.co` |
| 文生图端点 | `POST /predictions` |
| 查询任务端点 | `GET /predictions/{taskId}` |
| 认证方式 | `Authorization: Bearer {apiToken}` |
| 文生图模型 | `google/nano-banana` |
| 图片编辑模型 | `google/nano-banana-edit` / `google/nano-banana-pro` |
| 任务状态值 | `queuing` / `processing` / `completed` / `failed` |
---
## 二、整体架构
### 2.1 KieAI 接口复刻架构
```mermaid
graph TD
subgraph crmebFront [crmeb-front]
KieAICtrl[KieAIController]
end
subgraph crmebService [crmeb-service]
ToolKieAISvc[ToolKieAIService]
ToolKieAISvcImpl[ToolKieAIServiceImpl]
KieAIHelper[KieAIHelper]
end
subgraph crmebCommon [crmeb-common]
KieAICfg[KieAIConfig]
DTOs[DTOs: TextToImageInput etc.]
end
subgraph external [External]
KieAIAPI["KieAI API (api.kie.ai)"]
end
KieAICtrl --> ToolKieAISvc
ToolKieAISvc --> ToolKieAISvcImpl
ToolKieAISvcImpl --> KieAIHelper
KieAIHelper --> KieAICfg
KieAIHelper --> DTOs
KieAIHelper --> KieAIAPI
```
### 2.2 菜品图片生成流程
```mermaid
sequenceDiagram
participant Client
participant CalculatorService
participant DishImageService
participant DB as v2_dish_image_cache
participant KieAI as KieAI NanoBanana API
participant OSS as 阿里云OSS
Client->>CalculatorService: POST /calculator/calculate
CalculatorService->>CalculatorService: generateMealPlan()
loop 每道菜品
CalculatorService->>DishImageService: ensureValidImage(dishName, originalUrl)
DishImageService->>DB: 查询缓存(dishName)
alt 缓存命中
DB-->>DishImageService: 返回缓存的 oss_url
else 缓存未命中
DishImageService->>DishImageService: HTTP HEAD 检查 originalUrl
alt 原始URL有效
DishImageService->>DB: 写入缓存
else 原始URL无效(404)
DishImageService->>KieAI: createTextToImageTask(prompt)
KieAI-->>DishImageService: 返回 taskId
DishImageService->>KieAI: waitForTaskCompletion(taskId)
KieAI-->>DishImageService: 返回 output URLs
DishImageService->>DishImageService: 下载图片 byte[]
DishImageService->>OSS: 上传到 recipes/ 目录
OSS-->>DishImageService: 返回 OSS URL
DishImageService->>DB: 写入缓存
end
end
DishImageService-->>CalculatorService: 返回有效 URL
end
CalculatorService-->>Client: NutritionCalculateResponse
```
---
## 三、实现步骤
### 步骤 1复刻 KieAI 配置到 crmeb-common
**新增文件:** `crmeb-common/src/main/java/com/zbkj/common/config/KieAIConfig.java`
`[models-integration KieAIConfig](models-integration/src/main/java/com/integration/api/config/KieAIConfig.java)` 复刻,保留核心字段:
```java
@Configuration
@ConfigurationProperties(prefix = "kie-ai")
public class KieAIConfig {
private String baseUrl = "https://api.kie.ai";
private String apiToken; // API Token
private String apiUploadBaseUrl = "https://kieai.redpandaai.co";
private String apiCallbackUrl; // 回调 URL
private Integer connectTimeout = 30000;
private Integer readTimeout = 60000;
private Integer pollInterval = 2000; // 轮询间隔
private Integer maxWaitTime = 300000; // 最大等待时间
private String defaultOutputFormat = "png";
private String defaultImageSize = "1:1"; // 菜品图默认正方形
}
```
**修改:** `application-sophia.yml` 新增配置段:
```yaml
kie-ai:
base-url: https://api.kie.ai
api-token: 484661585fe62c5bcb77e6d392ba8ee8
api-callback-url: https://sophia-shop.uj345.cc/api/front/kieai/callback
api-upload-base-url: https://kieai.redpandaai.co
connect-timeout: 30000
read-timeout: 60000
poll-interval: 2000
max-wait-time: 300000
default-output-format: png
default-image-size: "1:1"
```
### 步骤 2复刻 KieAI DTOs 到 crmeb-common
**新增文件位置:** `crmeb-common/src/main/java/com/zbkj/common/request/kieai/``crmeb-common/src/main/java/com/zbkj/common/response/kieai/`
`[models-integration DTOs](models-integration/src/main/java/com/integration/api/dto/)` 复刻以下类:
| 源文件 (models-integration) | 目标位置 (crmeb-common) | 说明 |
| ------------------------- | --------------------------------------------- | --------- |
| `TextToImageInput.java` | `request/kieai/KieAITextToImageInput.java` | 文生图输入参数 |
| `ImageEditInput.java` | `request/kieai/KieAIImageEditInput.java` | 图编辑输入参数 |
| `CreateTaskRequest.java` | `request/kieai/KieAICreateTaskRequest.java` | 创建任务请求 |
| `NanoBananaRequest.java` | `request/kieai/KieAINanoBananaRequest.java` | 前端传入的完整请求 |
| `CreateTaskResponse.java` | `response/kieai/KieAICreateTaskResponse.java` | 创建任务响应 |
| `QueryTaskResponse.java` | `response/kieai/KieAIQueryTaskResponse.java` | 查询任务响应 |
| `NanoBananaResponse.java` | `response/kieai/KieAINanoBananaResponse.java` | 通用响应包装 |
核心 DTO 结构(`KieAIQueryTaskResponse`
```java
public class KieAIQueryTaskResponse {
private String id; // 任务ID
private String status; // queuing/processing/completed/failed
private String created_at;
private String finished_at;
private String model;
private Object input;
private List<String> output; // 生成的图片URL列表
private String error;
}
```
### 步骤 3复刻 KieAI Helper 到 crmeb-service
**新增文件:** `crmeb-service/src/main/java/com/zbkj/service/helper/KieAIHelper.java`
`[NanoBananaHelper](models-integration/src/main/java/com/integration/api/helper/NanoBananaHelper.java)` 复刻,核心方法:
- `createHeaders()` -- 构建 `Authorization: Bearer {token}` 请求头
- `createTask(KieAICreateTaskRequest)` -- POST `/predictions` 创建文生图任务
- `queryTask(String taskId)` -- GET `/predictions/{taskId}` 查询任务
- `buildTextToImageRequest(KieAITextToImageInput, callbackUrl)` -- 构建请求
- `isApiTokenConfigured()` -- 验证 Token 配置
使用项目中已有的 `RestTemplate`(如果 crmeb 中没有 RestTemplate Bean需要注册一个
### 步骤 4复刻 KieAI Service 到 crmeb-service
**新增文件:**
- `crmeb-service/src/main/java/com/zbkj/service/service/tool/ToolKieAIService.java`(接口)
- `crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolKieAIServiceImpl.java`(实现)
`[NanoBananaServiceImpl](models-integration/src/main/java/com/integration/api/service/impl/NanoBananaServiceImpl.java)` 复刻核心方法:
```java
public interface ToolKieAIService {
/** 文生图 - 创建任务 */
KieAICreateTaskResponse createTextToImageTask(KieAINanoBananaRequest request);
/** 查询任务状态 */
KieAIQueryTaskResponse queryTask(String taskId);
/** 同步等待任务完成(轮询) */
KieAIQueryTaskResponse waitForTaskCompletion(String taskId, long maxWaitTime);
/** 图编辑 - 创建任务 */
KieAICreateTaskResponse createImageEditTask(KieAINanoBananaRequest request);
}
```
**注意:** 复刻时简化设计 -- 不需要 `NanoBananaTask` 数据库表和 `Article` 表关联逻辑,因为 crmeb 中的主要使用场景是内部调用(菜品图片生成),任务记录可选。
### 步骤 5复刻 KieAI Controller 到 crmeb-front
**新增文件:** `crmeb-front/src/main/java/com/zbkj/front/controller/KieAIController.java`
`[KieAI2ImageController](models-integration/src/main/java/com/integration/api/controller/KieAI2ImageController.java)` 复刻,暴露以下接口:
| 接口 | 方法 | 说明 |
| ------------------------------------- | ---- | ------ |
| `/api/front/kieai/text-to-image` | POST | 文生图 |
| `/api/front/kieai/image-edit` | POST | 图编辑 |
| `/api/front/kieai/task/{taskId}` | GET | 查询任务 |
| `/api/front/kieai/task/{taskId}/wait` | GET | 同步等待任务 |
| `/api/front/kieai/callback` | POST | 回调通知 |
同时需要在安全白名单中添加 `/api/front/kieai/**` 路径(参考 Coze 接口的白名单配置)。
### 步骤 6新建菜品图片缓存表
```sql
CREATE TABLE `v2_dish_image_cache` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`dish_name` VARCHAR(100) NOT NULL COMMENT '菜品名称',
`original_url` VARCHAR(500) DEFAULT NULL COMMENT '原始图片URL',
`oss_url` VARCHAR(500) NOT NULL COMMENT 'OSS有效图片URL',
`ai_provider` VARCHAR(50) DEFAULT 'kieai' COMMENT 'AI生成来源',
`task_id` VARCHAR(100) DEFAULT NULL COMMENT 'KieAI任务ID',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_dish_name` (`dish_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜品图片缓存表';
```
对应新增:
- `crmeb-common/.../model/tool/V2DishImageCache.java`(实体)
- `crmeb-service/.../dao/tool/V2DishImageCacheDao.java`Mapper
- `resources/mapper/tool/V2DishImageCacheDao.xml`MyBatis XML
### 步骤 7扩展 OssService 支持 InputStream 上传
当前 `[OssServiceImpl.upload()](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/OssServiceImpl.java)` 只支持 `File` 参数。新增 InputStream 重载:
```java
// OssService 接口新增
void upload(CloudVo cloudVo, String objectKey, InputStream inputStream, long contentLength);
// OssServiceImpl 实现
@Override
public void upload(CloudVo cloudVo, String objectKey, InputStream inputStream, long contentLength) {
OSS ossClient = new OSSClientBuilder().build(cloudVo.getRegion(), cloudVo.getAccessKey(), cloudVo.getSecretKey());
try {
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(contentLength);
PutObjectRequest putRequest = new PutObjectRequest(cloudVo.getBucketName(), objectKey, inputStream, metadata);
ossClient.putObject(putRequest);
} finally {
ossClient.shutdown();
}
}
```
### 步骤 8实现 DishImageService
**新增文件:**
- `crmeb-service/.../service/tool/DishImageService.java`
- `crmeb-service/.../service/impl/tool/DishImageServiceImpl.java`
核心流程:
```java
@Service
public class DishImageServiceImpl implements DishImageService {
@Autowired private V2DishImageCacheDao cacheDao;
@Autowired private ToolKieAIService kieAIService;
@Autowired private OssService ossService;
@Autowired private SystemConfigService systemConfigService;
@Override
public String ensureValidImageUrl(String dishName, String originalUrl) {
// 1. 查缓存
V2DishImageCache cache = cacheDao.selectByDishName(dishName);
if (cache != null) return cache.getOssUrl();
// 2. HTTP HEAD 检查原始 URL3秒超时
if (checkUrlAccessible(originalUrl)) {
saveCache(dishName, originalUrl, originalUrl, null);
return originalUrl;
}
// 3. KieAI 文生图
try {
String prompt = "一道精美的中式菜品照片:" + dishName
+ ",高清美食摄影风格,白色餐盘,俯拍角度,自然光照";
KieAICreateTaskResponse createResp = kieAIService.createTextToImageTask(buildRequest(prompt));
KieAIQueryTaskResponse result = kieAIService.waitForTaskCompletion(createResp.getId(), 120000);
if ("completed".equals(result.getStatus()) && result.getOutput() != null) {
String imageUrl = result.getOutput().get(0);
// 4. 下载图片并上传 OSS
byte[] imageBytes = downloadImage(imageUrl);
String ossUrl = uploadToOss(imageBytes, dishName);
// 5. 写缓存
saveCache(dishName, originalUrl, ossUrl, createResp.getId());
return ossUrl;
}
} catch (Exception e) {
log.error("菜品图片AI生成失败: {}", dishName, e);
}
// 6. 降级:返回默认占位图
return getDefaultPlaceholderUrl();
}
}
```
### 步骤 9修改 ToolCalculatorServiceImpl
`[generateMealPlan()](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCalculatorServiceImpl.java)` 方法末尾集成 DishImageService
```java
@Autowired
private DishImageService dishImageService;
private MealPlan generateMealPlan(String ckdStage, Boolean dialysis) {
MealPlan plan = new MealPlan();
// ... 现有代码不变(创建早/午/晚餐 Dish 列表)...
// 确保所有菜品图片URL有效
ensureMealPlanImages(plan);
return plan;
}
private void ensureMealPlanImages(MealPlan plan) {
Stream.of(plan.getBreakfast(), plan.getLunch(), plan.getDinner())
.filter(Objects::nonNull)
.flatMap(List::stream)
.forEach(dish -> {
String validUrl = dishImageService.ensureValidImageUrl(dish.getName(), dish.getImage());
dish.setImage(validUrl);
});
}
```
---
## 四、异常处理与降级策略
- KieAI 生成失败 / 超时 -> 返回**默认占位图** URL预先上传到 OSS 的通用食物图片)
- HTTP URL 检测设置 **3 秒超时**,避免阻塞主流程
- 整个图片处理过程用 try-catch 包裹,**失败不影响营养计算结果的返回**
- 日志记录每次 AI 生成和上传操作,便于排查
- KieAI API Token 未配置时跳过 AI 生成,直接使用原始 URL 或占位图
---
## 五、性能优化
- **缓存优先**: DB 缓存命中后直接返回,零网络开销
- **仅 11 道固定菜品**: 缓存全部填充后,后续请求无 AI 调用
- **首次预热**: 可提供管理后台接口或启动任务,手动触发所有菜品图片预生成
- **并行处理**: 可用 `CompletableFuture` 并行检查/生成多道菜品图片(可选优化)
---
## 六、涉及文件清单
### 6.1 新增文件KieAI 接口复刻)
| 模块 | 文件路径 | 说明 |
| ------------- | --------------------------------------------- | ------------- |
| crmeb-common | `config/KieAIConfig.java` | KieAI 配置类 |
| crmeb-common | `request/kieai/KieAITextToImageInput.java` | 文生图输入 DTO |
| crmeb-common | `request/kieai/KieAIImageEditInput.java` | 图编辑输入 DTO |
| crmeb-common | `request/kieai/KieAICreateTaskRequest.java` | 创建任务请求 DTO |
| crmeb-common | `request/kieai/KieAINanoBananaRequest.java` | 前端完整请求 DTO |
| crmeb-common | `response/kieai/KieAICreateTaskResponse.java` | 创建任务响应 DTO |
| crmeb-common | `response/kieai/KieAIQueryTaskResponse.java` | 查询任务响应 DTO |
| crmeb-common | `response/kieai/KieAINanoBananaResponse.java` | 通用响应包装 |
| crmeb-service | `helper/KieAIHelper.java` | API 调用工具类 |
| crmeb-service | `service/tool/ToolKieAIService.java` | KieAI 服务接口 |
| crmeb-service | `service/impl/tool/ToolKieAIServiceImpl.java` | KieAI 服务实现 |
| crmeb-front | `controller/KieAIController.java` | KieAI REST 接口 |
### 6.2 新增文件(菜品图片缓存)
| 模块 | 文件路径 | 说明 |
| ------------- | ----------------------------------------------- | ------------------------- |
| crmeb-common | `model/tool/V2DishImageCache.java` | 缓存实体 |
| crmeb-service | `dao/tool/V2DishImageCacheDao.java` | MyBatis Mapper |
| crmeb-service | `resources/mapper/tool/V2DishImageCacheDao.xml` | MyBatis XML |
| crmeb-service | `service/tool/DishImageService.java` | 菜品图片服务接口 |
| crmeb-service | `service/impl/tool/DishImageServiceImpl.java` | 菜品图片服务实现 |
| - | SQL 迁移脚本 | 建 `v2_dish_image_cache` 表 |
### 6.3 修改文件
| 模块 | 文件路径 | 改动内容 |
| ------------- | -------------------------------------------------- | ------------------------ |
| crmeb-service | `service/OssService.java` | 新增 InputStream 上传重载方法 |
| crmeb-service | `service/impl/OssServiceImpl.java` | 实现 InputStream 上传 |
| crmeb-service | `service/impl/tool/ToolCalculatorServiceImpl.java` | 集成 DishImageService |
| crmeb-front | `resources/application-sophia.yml` | 新增 `kie-ai` 配置段 |
| crmeb-front | 安全白名单配置 | 添加 `/api/front/kieai/**` |

View File

@@ -0,0 +1,193 @@
---
name: Migrate APIs to crmeb-front
overview: Migrate all backend API implementations that models-api.js currently calls from models-integration to crmeb-front, and update the frontend to route all requests through crmeb-front's /api/front/ prefix.
todos:
- id: module-1-video
content: "Module 1: Copy KieAI Video (Sora2) -- DTO, Service, ServiceImpl, Controller endpoints (text-to-video, image-to-video, pro variants, watermark, file-upload, task query)"
status: completed
- id: module-2-asr
content: "Module 2: Copy Tencent ASR -- add tencentcloud-sdk-java dependency, Config, DTO, Service, ServiceImpl, Controller (create-task, query-status, sentence-recognition)"
status: completed
- id: module-3-articles
content: "Module 3: Copy Article Models -- Model/VO/Mapper, Service, Controller at /api/front/article-models (getById, list, search)"
status: completed
- id: coze-resume
content: Add Coze workflow resume endpoint to existing CozeController and ToolCozeService
status: completed
- id: interceptor-config
content: Update WebConfig/application.yml to exclude new routes from token interceptor
status: completed
- id: frontend-update
content: "Update models-api.js: change API_BASE_URL, update all paths to /api/front/ prefix, move secrets to server-side config"
status: completed
isProject: false
---
# Migrate models-api.js Backend to crmeb-front
## Current Architecture
The frontend `models-api.js` calls `API_BASE_URL = 'https://sophia-shop.uj345.cc/models'` (models-integration, port 5081). The goal is to migrate all these endpoints to crmeb-front (port 8081) so the frontend only needs to call `/api/front/` endpoints.
## Endpoint Mapping: models-api.js -> models-integration -> crmeb-front
### Already Ported (only need frontend URL change)
These endpoints already exist in crmeb-front and have working service implementations:
- **Coze Chat**: `/api/coze/chat` -> `/api/front/coze/chat` (CozeController)
- **Coze Retrieve**: `/api/coze/chat/retrieve` -> `/api/front/coze/chat/retrieve`
- **Coze Messages**: `/api/coze/chat/messages/list` -> `/api/front/coze/chat/messages/list`
- **Coze Workflow Run**: `/api/coze/workflow/run` -> `/api/front/coze/workflow/run`
- **Coze Workflow Stream**: `/api/coze/workflow/stream` -> `/api/front/coze/workflow/stream`
- **Coze File Upload**: `/api/coze/file/upload` -> `/api/front/coze/file/upload`
- **KieAI Image Edit**: `/api/kieai/image/image-edit` -> `/api/front/kieai/image-edit` (KieAIController)
- **Upload File**: already calls `sophia-shop.uj345.cc/api/front/upload/imageOuter` (crmeb-front)
### Need to Add to crmeb-front (3 modules)
#### Module 1: KieAI Video (Sora2) -- 6 endpoints
Source: [KieAI2VideoController.java](models-integration/src/main/java/com/integration/api/controller/KieAI2VideoController.java) + [Sora2ServiceImpl.java](models-integration/src/main/java/com/integration/api/service/impl/Sora2ServiceImpl.java)
New endpoints to add to `KieAIController` or a new `KieAIVideoController`:
- `POST /api/front/kieai/text-to-video` (createTextToVideoTask)
- `POST /api/front/kieai/image-to-video` (createImageToVideoTask)
- `POST /api/front/kieai/pro/text-to-video` (createProTextToVideoTask)
- `POST /api/front/kieai/pro/image-to-video` (createProImageToVideoTask)
- `POST /api/front/kieai/remove-watermark` (removeWatermark)
- `POST /api/front/kieai/file-url-upload` (uploadFileByUrl)
- `GET /api/front/kieai/video/task/{taskId}` (getTaskStatus - video task query)
Files to create/copy:
- **DTO**: Copy `Sora2Request` -> `com.zbkj.common.request.kieai.Sora2Request`
- **DTO**: Copy `TaskStatus` -> `com.zbkj.common.response.kieai.KieAIVideoTaskStatus`
- **DTO**: Copy `CreateProTextToVideoRequest` -> `com.zbkj.common.request.kieai.CreateProTextToVideoRequest`
- **Service**: Create `ToolSora2Service` interface in `crmeb-service`
- **ServiceImpl**: Copy `Sora2ServiceImpl` logic -> `ToolSora2ServiceImpl` in `crmeb-service`
- **Controller**: Add video endpoints to [KieAIController.java](msh_crmeb_22/crmeb-front/src/main/java/com/zbkj/front/controller/KieAIController.java) or new controller
- **Helper**: Copy `HttpRequestUtils` -> `com.zbkj.common.utils.HttpRequestUtils` (or reuse existing crmeb HTTP utils)
Dependencies already available:
- Apache HttpClient (in crmeb-common pom.xml)
- FastJSON (in crmeb parent pom.xml)
- `KieAIConfig` already exists with `baseUrl`, `apiCallbackUrl` etc.
Config to add to `KieAIConfig`:
- `apiUploadBaseUrl` property (used by Sora2ServiceImpl for file uploads)
#### Module 2: Tencent ASR -- 3 endpoints
Source: [TencentAsrController.java](models-integration/src/main/java/com/integration/api/controller/TencentAsrController.java) + [TencentAsrServiceImpl.java](models-integration/src/main/java/com/integration/api/service/impl/TencentAsrServiceImpl.java)
New endpoints:
- `POST /api/front/tencent/asr/create-task`
- `GET /api/front/tencent/asr/query-status/{taskId}`
- `POST /api/front/tencent/asr/sentence-recognition`
Files to create/copy:
- **DTO**: Copy `TencentAsrRequest` -> `com.zbkj.common.request.tencent.TencentAsrRequest`
- **DTO**: Copy `TencentAsrResponse` -> `com.zbkj.common.response.tencent.TencentAsrResponse`
- **DTO**: Copy `TencentAsrTaskStatus` -> `com.zbkj.common.response.tencent.TencentAsrTaskStatus`
- **Config**: Create `TencentAsrConfig` in `com.zbkj.common.config` (prefix `tencent-asr`, fields: secretId, secretKey, region, connectTimeout, readTimeout, defaultEngineModel, defaultResTextFormat, defaultChannelNum, enabled)
- **Service**: Create `ToolTencentAsrService` interface
- **ServiceImpl**: Copy `TencentAsrServiceImpl` logic -> `ToolTencentAsrServiceImpl`
- **Controller**: Create `TencentAsrController` in crmeb-front at `/api/front/tencent/asr`
**New dependency needed** in `crmeb-common/pom.xml`:
```xml
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java</artifactId>
<version>3.1.880</version>
</dependency>
```
**Config to add** to `application-sophia.yml`:
```yaml
tencent-asr:
secret-id: ${TENCENT_SECRET_ID:xxx}
secret-key: ${TENCENT_SECRET_KEY:xxx}
region: ap-shanghai
enabled: true
```
#### Module 3: Articles (models-integration style) -- 3 endpoints
Source: [ArticleController.java](models-integration/src/main/java/com/integration/api/controller/ArticleController.java)
Note: crmeb-front already has an `ArticleController` at `/api/front/article` for CMS articles, but that is a different data model (CRMEB's own articles). The models-integration `ArticleController` manages AI-generated content articles in a separate `eb_article` table with fields like `taskId`, `prompt`, `videoUrl`, `statusTask`.
Approach: Add these as new endpoints in a **new controller** `ArticleModelsController` at `/api/front/article-models` to avoid conflict with the existing crmeb ArticleController.
New endpoints:
- `GET /api/front/article-models/{id}` (getArticleById)
- `GET /api/front/article-models` (getArticleList, paginated)
- `GET /api/front/article-models/search` (searchArticles)
Files needed:
- The `Article` model, `ArticleVO`, `ArticleMapper`, `UserMapper` already exist in models-integration's DB tables. Need to ensure the crmeb service can access the same `eb_article` table or create equivalent mapper/model in crmeb-common.
- **Service**: Create `ToolArticleModelsService` interface + impl
- **Controller**: Create `ArticleModelsController`
### Callback Endpoints (remain in models-integration)
The following callbacks should **stay in models-integration** since they are called by external services (KieAI, Coze) and need a stable URL:
- `POST /api/kieai/callback`
- `POST /api/kieai/callback20994`
- `POST /api/diet-checkin/callback/video`
- `POST /api/diet-checkin/callback/analysis`
These use `callBackUrl` that is configured in the task creation requests, so they can continue pointing to models-integration.
### Coze Workflow Resume
The frontend `cozeWorkflowResume` function calls `/api/coze/workflow/resume`, but this endpoint does **not exist** in either models-integration or crmeb-front. The Coze SDK does support workflow resume. Need to:
- Add `/api/front/coze/workflow/resume` endpoint to crmeb-front CozeController
- Add `workflowResume()` method to `ToolCozeService` / `ToolCozeServiceImpl`
## Frontend Changes (models-api.js)
After backend is ready, update [models-api.js](msh_single_uniapp/utils/models-api.js):
1. Change `API_BASE_URL` from `'https://sophia-shop.uj345.cc/models'` to `'https://sophia-shop.uj345.cc'` (crmeb-front domain)
2. Update all API paths to use `/api/front/` prefix:
- `/api/articles/{id}` -> `/api/front/article-models/{id}`
- `/api/articles` -> `/api/front/article-models`
- `/api/articles/search` -> `/api/front/article-models/search`
- `/api/kieai/text-to-video` -> `/api/front/kieai/text-to-video`
- `/api/kieai/image-to-video` -> `/api/front/kieai/image-to-video`
- `/api/kieai/image/image-edit` -> `/api/front/kieai/image-edit`
- `/api/kieai/task/{taskId}` -> `/api/front/kieai/video/task/{taskId}`
- `/api/kieai/file-url-upload` -> `/api/front/kieai/file-url-upload`
- `/api/tencent/asr/create-task` -> `/api/front/tencent/asr/create-task`
- `/api/tencent/asr/query-status/{id}` -> `/api/front/tencent/asr/query-status/{id}`
- `/api/coze/chat` -> `/api/front/coze/chat`
- `/api/coze/chat/retrieve` -> `/api/front/coze/chat/retrieve`
- `/api/coze/chat/messages/list` -> `/api/front/coze/chat/messages/list`
- `/api/coze/workflow/run` -> `/api/front/coze/workflow/run`
- `/api/coze/workflow/stream` -> `/api/front/coze/workflow/stream`
- `/api/coze/workflow/resume` -> `/api/front/coze/workflow/resume`
- `/api/coze/file/upload` -> `/api/front/coze/file/upload` (in `cozeUploadFile`)
3. Remove hardcoded `tenant_id`, `api_key`, callback URLs from frontend -- move these to server-side config
## Interceptor Config
Ensure the new routes are excluded from token interceptor in WebConfig (or application.yml `excludePathPatterns`):
- `/api/front/tencent/asr/**`
- `/api/front/article-models/**`
- KieAI video endpoints are under `/api/front/kieai/**` which is likely already excluded

View File

@@ -0,0 +1,559 @@
---
name: Tool Module Test Plan
overview: Create a comprehensive frontend functional test plan for the WeChat mini-program tool module, covering the hub page (tool_main/index.vue) and all 18 sub-pages under pages/tool/.
todos:
- id: test-hub
content: "测试 Tool 首页 (tool_main/index.vue): 数据加载、四宫格导航、推荐内容、登录态"
status: completed
- id: test-calculator
content: "测试食谱计算器流程: calculator.vue 表单校验提交 -> calculator-result.vue 结果展示与采纳"
status: completed
- id: test-ai
content: "测试 AI 营养师: 文字对话、语音输入(ASR)、图片发送、Coze API 集成"
status: completed
- id: test-checkin
content: "测试打卡流程: checkin.vue 积分/连续打卡 -> checkin-publish.vue 发布 -> checkin-detail.vue 详情 -> dietary-records.vue 列表 -> checkin-copy.vue 借鉴"
status: completed
- id: test-food
content: "测试食物百科: food-encyclopedia.vue 搜索/分类/列表 -> food-detail.vue 详情"
status: completed
- id: test-knowledge
content: "测试营养知识: nutrition-knowledge.vue Tab切换/列表 -> nutrient-detail.vue 详情"
status: completed
- id: test-community
content: "测试社区帖子: post-detail.vue 展示/点赞/收藏/评论/关注"
status: completed
- id: test-profile
content: "测试用户与积分: my-profile.vue / points-rules.vue / invite-rewards.vue / welcome-gift.vue"
status: completed
- id: test-common
content: "通用测试: 登录态、网络异常、UI多机型适配、性能、页面标题与返回"
status: completed
isProject: false
---
# Tool 模块前端功能测试计划
## 一、测试范围
本测试计划覆盖微信小程序 Tool 模块的全部 19 个页面:
- **入口首页**: [pages/tool_main/index.vue](msh_single_uniapp/pages/tool_main/index.vue)
- **子页面 (18个)**: [pages/tool/](msh_single_uniapp/pages/tool/) 目录下全部 `.vue` 文件
### 页面与路由清单
| 页面文件 | 路由路径 | 页面标题 | 分类 |
| ---- | ---- | ---- | --- |
> 注意Markdown 表格不便渲染,以列表替代:
**食谱计算器**
- `calculator.vue` -> `/pages/tool/calculator` (食谱计算器)
- `calculator-result.vue` -> `/pages/tool/calculator-result` (营养计算结果)
**AI 营养师**
- `ai-nutritionist.vue` -> `/pages/tool/ai-nutritionist` (AI营养师)
**食物百科**
- `food-encyclopedia.vue` -> `/pages/tool/food-encyclopedia` (食物百科)
- `food-detail.vue` -> `/pages/tool/food-detail` (食物详情)
**营养知识**
- `nutrition-knowledge.vue` -> `/pages/tool/nutrition-knowledge` (营养知识)
- `nutrient-detail.vue` -> `/pages/tool/nutrient-detail` (营养素详情)
**饮食打卡**
- `checkin.vue` -> `/pages/tool/checkin` (打卡)
- `checkin-publish.vue` -> `/pages/tool/checkin-publish` (饮食打卡)
- `checkin-detail.vue` -> `/pages/tool/checkin-detail` (打卡详情)
- `checkin-copy.vue` -> `/pages/tool/checkin-copy` (一键借鉴打卡)
- `dietary-records.vue` -> `/pages/tool/dietary-records` (饮食记录)
**社区帖子**
- `post-detail.vue` -> `/pages/tool/post-detail` (帖子详情)
**食谱**
- `recipe-detail.vue` -> `/pages/tool/recipe-detail` (食谱详情)
**用户与积分**
- `my-profile.vue` -> `/pages/tool/my-profile` (我的)
- `points-rules.vue` -> `/pages/tool/points-rules` (积分规则)
- `invite-rewards.vue` -> `/pages/tool/invite-rewards` (邀请有礼)
- `welcome-gift.vue` -> `/pages/tool/welcome-gift` (会员福利)
---
## 二、测试环境与前置条件
- 测试平台:微信开发者工具 + 真机iOS / Android
- 网络环境WiFi 正常网络 + 弱网 (3G) + 断网
- 账号准备:已登录账号 / 未登录访客
- 后端接口:已部署测试环境 API`tool/*` 全部接口可用)
- 测试数据:至少 1 条已有计算结果、1 条打卡记录、1 条社区帖子
---
## 三、功能测试用例
### 3.1 Tool 首页 (tool_main/index.vue)
**页面加载**
- 页面打开后,并发调用 `getRecommendedRecipes``getRecommendedKnowledge``getUserHealthStatus` 三个接口,数据正常展示
- 接口返回失败时(单个/全部),页面不白屏,其他模块正常展示
- 弱网环境下 loading 状态正常
**用户信息区域**
- 已登录:显示用户头像、昵称、健康档案状态
- 未登录:显示"请点击登录",点击触发登录弹窗
- 点击头像跳转到 `/pages/tool/my-profile`
- 点击"打卡"按钮跳转到 `/pages/tool/checkin`
**四宫格工具入口**
- "食谱计算器" 跳转到 `/pages/tool/calculator`
- "AI营养师" 跳转到 `/pages/tool/ai-nutritionist`
- "食物百科" 跳转到 `/pages/tool/food-encyclopedia`
- "营养知识" 跳转到 `/pages/tool/nutrition-knowledge`
**推荐食谱区域**
- 展示 2 条推荐食谱(封面图、标题)
- 点击食谱卡片跳转 `/pages/tool/recipe-detail?id=xxx`
- 点击"更多"显示 toast "食谱列表功能开发中"
- 无数据时不展示该区域 / 展示空态
**健康知识区域**
- 展示 2 条推荐知识(标题、摘要)
- 点击知识条目跳转 `/pages/tool/nutrition-knowledge?id=xxx`
- 点击"更多"跳转 `/pages/tool/nutrition-knowledge`
**推广卡片**
- "慢生活营养专家" 卡片正常展示
- 点击跳转 `/pages/tool/welcome-gift`
---
### 3.2 食谱计算器 (calculator.vue)
**表单输入**
- 性别选择(男/女)单选正常
- 年龄、身高、干体重、血肌酐输入框:只允许数字,限制合理范围
- 透析状态切换(是/否),选"是"后透析类型选项出现
- 透析类型(血液透析/腹膜透析)单选正常
- 所有必填项为空时,点击"开始计算"给出校验提示
**计算提交**
- 填写完整后点击"开始计算",调用 `calculateNutrition` 接口(`POST tool/calculator/calculate`
- 接口返回成功后跳转到 `/pages/tool/calculator-result?id=xxx`
- 接口返回失败时 toast 提示错误信息
- 重复点击防抖(按钮 loading 状态)
---
### 3.3 营养计算结果 (calculator-result.vue)
**页面加载**
- 携带 `id` 参数进入,调用 `getCalculatorResult(id)` 获取数据
- Loading 状态展示正常
- 无结果 / id 无效时展示 error 状态
**健康概览 Tab**
- 展示 eGFR 值、体重、BMI、CKD 分期数据卡片
- 营养目标(蛋白质、热量、钠、钾、磷等)正确展示
- 食物份量列表正确展示
**膳食计划 Tab**
- Tab 切换正常(健康概览 / 膳食计划)
- 早餐、午餐、晚餐 三餐方案展示正确
- 每餐菜品名称、份量、图片展示
- 菜品图片加载(含 AI 生成图片、OSS 压缩图片)正常
- 温馨提示区域展示
**操作按钮**
- "采纳方案" 按钮调用 `adoptNutritionPlan(resultId)``POST tool/calculator/adopt`
- 采纳成功后提示并更新状态
- "联系营养师" 按钮跳转 AI 营养师或显示联系方式
---
### 3.4 AI 营养师 (ai-nutritionist.vue)
**对话界面**
- 页面打开后展示推广 banner
- 快捷问题列表展示,点击快捷问题自动发送
- 消息列表scroll-view正常滚动新消息自动滚到底部
**文字对话**
- 输入文字后点击发送按钮,消息气泡出现(用户侧)
- AI 回复消息气泡出现AI 侧),打字指示器展示
- 使用 Coze API 流程:`cozeChat` -> `cozeRetrieveChat` -> `cozeMessageList`
- 空消息不允许发送
**语音输入**
- 点击麦克风按钮开始录音
- 录音完成后上传语音 (`uploadFile`) 并调用 ASR (`createAsrTask` -> `queryAsrStatus`)
- 语音识别结果转为文字并发送
- 录音授权拒绝时提示用户
**图片发送**
- 点击相机按钮选择/拍照图片
- 图片上传后通过 Coze 接口发送给 AI
- 图片预览功能正常
**异常处理**
- 网络断开时发送消息提示网络错误
- AI 回复超时处理
- 会话 ID 管理正常(多次对话保持上下文)
---
### 3.5 食物百科 (food-encyclopedia.vue)
- 页面加载调用 `getFoodList` 接口,展示食物列表
- 顶部搜索框输入关键词,调用 `searchFood` 接口
- 搜索防抖正常(不会每个字符触发请求)
- 横向分类滑动切换(全部/谷物/蔬菜/水果/肉类/海鲜)
- 切换分类后列表刷新
- 食物卡片展示:图片、名称、安全标签、营养信息
- 点击食物卡片跳转 `/pages/tool/food-detail`
- 列表分页加载(上拉加载更多)
- 空搜索结果展示空态
---
### 3.6 食物详情 (food-detail.vue)
- 页面加载展示食物图片、分类、安全标签
- 关键营养素宫格展示
- 营养成分表完整展示
- "相似食物" 按钮功能
- 分享按钮功能正常
---
### 3.7 营养知识 (nutrition-knowledge.vue)
- Tab 切换:营养素 / 饮食指导 / 文章
- 各 Tab 调用 `getKnowledgeList` 接口type 参数不同)
- 营养素列表展示,点击跳转 `/pages/tool/nutrient-detail`
- 饮食指导列表展示
- 文章列表展示
- 列表分页(上拉加载更多)
- 携带 `id` 参数进入时自动定位到对应内容
---
### 3.8 营养素详情 (nutrient-detail.vue)
- 页面通过本地营养素数据 map 渲染
- 展示:营养素头部信息、状态卡片、重要性、摄入建议、食物来源、风险提示、建议
- 底部免责声明展示
- 不存在的营养素名称进入时展示空态或提示
---
### 3.9 打卡首页 (checkin.vue)
- 积分展示区域,调用 `getUserPoints` 接口
- 7 日连续打卡圈展示,调用 `getCheckinStreak` 接口
- "积分规则"按钮跳转 `/pages/tool/points-rules`
- "立即打卡"按钮跳转 `/pages/tool/checkin-publish`
- 任务列表展示,调用 `getCheckinTasks` 接口
- 兑换列表展示
- 底部提示 banner 展示
---
### 3.10 饮食打卡发布 (checkin-publish.vue)
**图片上传**
- 点击添加照片,打开相册/相机
- 最多上传 6 张图片
- 图片预览和删除功能
- 图片上传调用 `uploadFile` 接口
**餐次选择**
- 早餐/午餐/晚餐/加餐 选择器正常
**语音备注**
- 录音功能正常(同 AI 营养师)
- 语音识别结果填入备注
**AI 功能开关**
- AI 视频生成开关切换
- 开启后提交时调用 `createImageToVideoTask``createTextToVideoTask`
**提交**
- 点击"发布"调用 `submitCheckin` 接口(`POST tool/checkin/submit`
- 提交成功后跳转打卡详情或返回打卡首页
- 无图片时提示必须上传
- 防止重复提交
---
### 3.11 打卡详情 (checkin-detail.vue)
- 携带 `id` 参数进入,调用 `getCheckinDetail(id)`
- 图片轮播swiper正常底部图片计数器
- 餐次标签展示
- 作者信息、积分徽章展示
- 描述文本展示
- AI 营养分析卡片展示
- "分享到社区"按钮功能
---
### 3.12 一键借鉴打卡 (checkin-copy.vue)
- 原始记录卡片展示
- 照片网格选择(可勾选/取消勾选)
- 备注文本编辑
- AI 视频开关
- 取消/确认按钮
- 注意:当前 `loadOriginalRecord` 为 TODO需确认数据来源是否已接通
---
### 3.13 饮食记录 (dietary-records.vue)
- 自定义导航栏展示(`navigationStyle: "custom"`
- Tab 切换:全部 / 早餐 / 午餐 / 晚餐
- 调用 `getCheckinList` 接口,切换 Tab 传不同 `mealType`
- 列表项展示图片、餐次、备注、积分、AI 分析
- 上拉加载更多
- 空态展示(无记录时)
- 下拉刷新
---
### 3.14 社区帖子详情 (post-detail.vue)
**内容展示**
- 调用 `getCommunityDetail(id)` 获取帖子
- 图片轮播、餐次标签、标题、评分、描述、标签展示
- 作者信息区、关注按钮
- 营养统计数据展示
- AI 评论展示
**互动功能**
- 点赞:调用 `toggleLike`,状态切换正常
- 收藏:调用 `toggleCollect`,状态切换正常
- 关注:调用 `toggleFollow`,状态切换正常
- "一键借鉴打卡"按钮跳转 `/pages/tool/checkin-copy`
**评论功能**
- 调用 `getCommentList` 加载评论列表
- 底部输入框发表评论,调用 `addComment`
- 评论成功后列表刷新
**相关帖子**
- 调用 `getCommunityList` 展示相关帖子
- 点击相关帖子跳转对应详情
---
### 3.15 食谱详情 (recipe-detail.vue)
- 封面图展示
- 食谱信息(名称、难度徽章)展示
- 营养团队卡片展示
- 简介文本展示
- 营养数据宫格展示
- 膳食方案(早/午/晚)展示
- 注意事项列表展示
- 温馨提示展示
- 底部操作栏:点赞、收藏、打卡按钮
- 注意:当前 `loadRecipeData` 为 TODO需确认数据加载是否已接通
---
### 3.16 我的 (my-profile.vue)
- 用户头像、昵称展示
- 统计数据(打卡次数、积分、关注数)展示
- "我的健康" 区域展示
- "我的内容" 区域展示
- "工具与服务" 区域展示
- "设置" 区域展示
- 退出登录按钮功能
- 各导航项跳转正确
---
### 3.17 积分规则 (points-rules.vue)
- 页面为纯静态内容scroll-view 滚动正常
- 展示区域:赚取规则、使用方式、温馨提示、注意事项
- 文字排版在不同机型上无溢出/截断
---
### 3.18 邀请有礼 (invite-rewards.vue)
- 邀请卡片展示
- 奖励盒子列表展示
- 邀请按钮功能(触发分享或复制链接)
- 海报按钮功能
- 权益说明列表展示
- 邀请步骤展示
- 注意:当前为静态占位数据,确认是否有接口接通
---
### 3.19 会员福利 (welcome-gift.vue)
- 礼品 banner 展示
- 礼品项列表展示
- 二维码展示
- 领取步骤说明
- "添加微信" 按钮功能(复制微信号或打开二维码)
- 免责声明展示
---
## 四、通用测试项(每页必测)
### 4.1 页面基础
- 页面标题正确(与 pages.json 配置一致)
- 返回按钮功能正常
- 页面下拉不出现橡皮筋效果(或符合预期)
### 4.2 登录态
- 未登录访问需登录页面时,弹出登录引导
- 登录后自动返回原页面并刷新数据
### 4.3 网络异常
- 断网时接口请求给出友好提示
- 弱网环境下 loading 态不丢失
- 恢复网络后可重试/刷新
### 4.4 UI 适配
- iPhone SE / iPhone 14 Pro Max / 小屏安卓 / 平板 屏幕适配
- 安全区域(底部 Home Indicator适配
- 暗色模式适配(如支持)
- 长文本不溢出、不截断
### 4.5 性能
- 页面首屏加载时间 < 2s
- 列表页滚动流畅,无卡顿
- 图片懒加载正常
---
## 五、关键 API 接口清单
以下接口在前端测试中需确认可用:
**首页**
- `GET tool/home/recipes`
- `GET tool/home/knowledge`
- `GET tool/home/health-status`
**食谱计算器**
- `POST tool/calculator/calculate`
- `GET tool/calculator/result/{id}`
- `POST tool/calculator/adopt`
**AI 营养师** (通过 models-api.js)
- Coze 系列接口 (cozeChat / cozeRetrieveChat / cozeMessageList)
- ASR 接口 (createAsrTask / queryAsrStatus)
- 文件上传 (uploadFile)
**打卡**
- `POST tool/checkin/submit`
- `GET tool/checkin/list`
- `GET tool/checkin/detail/{id}`
- `GET tool/checkin/streak`
- `GET tool/checkin/tasks`
- `POST tool/checkin/copy`
- `POST tool/checkin/learn`
**食物百科**
- `GET tool/food/search`
- `GET tool/food/list`
- `GET tool/food/detail/{id}`
- `GET tool/food/similar/{id}`
**营养知识**
- `GET tool/knowledge/list`
- `GET tool/knowledge/detail/{id}`
- `GET tool/knowledge/nutrient/{name}`
**社区**
- `GET tool/community/detail/{id}`
- `GET tool/community/list`
- `POST tool/community/like`
- `POST tool/community/collect`
- `POST tool/community/follow`
- `POST tool/community/comment`
- `GET tool/community/comment/list/{postId}`
**积分**
- `GET tool/points/info`
- `GET tool/points/rules`
**文件上传**
- `POST tool/upload/image`
- `POST tool/upload/voice`
---
## 六、已知待完善项
以下页面有 TODO/未接通功能,测试时需标注为"受限测试"
- `checkin-copy.vue`: `loadOriginalRecord` 为 TODO数据加载待实现
- `food-detail.vue`: `loadFoodData` 为空实现,静态数据
- `recipe-detail.vue`: `loadRecipeData` 为 TODO静态数据
- `tool_main/index.vue`: "食谱列表" 入口仅 toast 提示开发中
- `invite-rewards.vue`: 邀请统计为静态占位数据

View File

@@ -0,0 +1,107 @@
---
name: 修复KieAI回调反序列化问题
overview: 修复 KieAI 回调接口的 JSON 反序列化错误,确保视频生成完成后能正确更新 article 表中的 video_url 和 status_task 字段
todos: []
isProject: false
---
# 修复 KieAI 回调反序列化问题
## 问题分析
通过日志分析发现:
```
Could not resolve parameter [0]... Problem deserializing 'setterless' property 'resultUrls':
get method returned null
```
KieAI 的回调请求已经到达后端 `/api/front/kieai/callback`,但是 Jackson 在反序列化 `KieAIQueryTaskResponse` 时失败,导致:
1. 回调处理方法 `handleCallback` 无法正常执行
2. `handleTaskCallback` 中更新 article 表的逻辑从未运行
3. 数据库中的 video_url 和 status_task 字段保持未更新状态
## 技术细节
**反序列化失败原因:**
- `KieAIQueryTaskResponse.TaskData` 类使用了 `@Data` 注解Lombok
- `resultUrls` 字段虽然有 getter但 Jackson 在反序列化嵌套的 List 类型时需要显式的 setter
- 当前的便捷方法 `getResultUrls()` 定义在父类,不满足 Jackson 对 `TaskData.resultUrls` 字段的反序列化要求
## 修复方案
### 文件:`[KieAIQueryTaskResponse.java](msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/response/kieai/KieAIQueryTaskResponse.java)`
**当前问题代码(第 73-113 行):**
```java
@Data
public static class TaskData implements Serializable {
// ... other fields ...
@ApiModelProperty(value = "结果URL数组视频/图片地址)")
private java.util.List<String> resultUrls; // Lombok @Data 应该生成 setter但可能被优化掉了
// ... other fields ...
}
```
**修复方法:**
添加显式的 setter 方法到 `TaskData` 类中:
```java
@Data
public static class TaskData implements Serializable {
// ... existing fields ...
@ApiModelProperty(value = "结果URL数组视频/图片地址)")
private java.util.List<String> resultUrls;
// 显式添加 setter 方法以确保 Jackson 反序列化正常工作
public void setResultUrls(java.util.List<String> resultUrls) {
this.resultUrls = resultUrls;
}
// ... other fields ...
}
```
**为什么这样修复:**
1. Lombok 的 `@Data` 注解理论上会自动生成 getter/setter但在某些情况下如嵌套泛型、继承关系等Jackson 可能无法正确识别
2. 显式声明 setter 方法可以确保 Jackson 在反序列化时能够正确填充 `resultUrls` 字段
3. 这不会与 Lombok 冲突,显式方法优先级更高
## 验证方法
修复后,当 KieAI 发送回调时:
1. 查看日志,应该看到:
```
接收KieAI任务回调任务ID: xxx
处理KieAI任务回调任务ID: xxx, 状态: success
找到article记录articleId: xxx, videoUrl: xxx, statusTask: 1
更新article视频信息成功
```
2. 数据库验证:
```sql
SELECT id, task_id, video_url, status_task FROM eb_article WHERE task_id = 'xxx';
```
应该看到 `video_url` 被正确填充,`status_task` 为 1成功或 2失败
3. 前端验证:
- 访问 `http://127.0.0.1:20822/api/front/tool/community/detail/302`
- 响应中应该包含 `videoUrl` 和 `hasVideo: true`
## 其他说明
**现有的回调处理逻辑是正确的:**
- `ToolKieAIServiceImpl.handleTaskCallback()` 方法(第 148-187 行)逻辑完整
- 通过 `taskId` 查询 article 记录
- 解析 `resultUrls` 并更新 `video_url` 和 `status_task`
- 日志记录完善
问题仅在于回调数据无法被正确反序列化,导致方法未被调用。

View File

@@ -0,0 +1,459 @@
---
name: 打卡视频生成与展示
overview: 实现打卡勾选生成视频后,在饮食记录页面定时查询视频任务状态并显示,在打卡详情支持视频播放,勾选生成视频的打卡帖子在社区最新tab显示
todos:
- id: backend-checkin-article
content: 修改打卡提交逻辑,勾选视频时创建eb_article记录存储taskId
status: completed
- id: backend-list-api
content: 更新打卡列表/详情接口,关联article表返回videoUrl和videoStatus
status: completed
- id: backend-callback
content: 完善KieAI回调处理,更新article的video_url和status_task
status: completed
- id: backend-community
content: 更新社区列表接口,关联打卡记录的视频信息
status: completed
- id: frontend-dietary-records
content: 饮食记录页面增加视频状态显示和定时轮询逻辑
status: completed
- id: frontend-detail-video
content: 打卡详情页增加视频播放器组件
status: completed
- id: frontend-community-badge
content: 社区页面卡片增加视频标记
status: completed
- id: frontend-api
content: 新增getVideoTaskStatus API方法
status: completed
isProject: false
---
# 打卡视频生成与展示功能实现计划
## 背景分析
根据代码探索,当前系统已具备:
- **打卡发布**: `[checkin-publish.vue](msh_single_uniapp/pages/tool/checkin-publish.vue)` 支持勾选"生成打卡视频分享到社区",会调用KieAI创建视频任务并返回 `taskId`
- **数据存储**: `eb_user_sign` 表存储 `task_id``enable_ai_video` 字段,`eb_article` 表存储视频URL和任务状态
- **视频生成**: 通过 `[ToolSora2Service](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolSora2ServiceImpl.java)` 调用KieAI API
- **状态查询接口**: `GET /api/front/kieai/video/task/{taskId}` 已存在
**缺失功能**:
1. 打卡时未创建 `eb_article` 记录来存储视频
2. 饮食记录页面不显示视频任务状态
3. 打卡详情页不支持视频播放
4. 社区帖子未关联视频信息
## 实现方案
### 数据库层面
#### 1. 修改表结构
**eb_user_sign表**: 已有 `task_id`, `enable_ai_video` 字段,无需修改
**v2_community_posts表**: 已有 `video_url` 字段(从SQL看到),需确认Java实体类是否包含
**eb_article表**: 已有 `video_url`, `task_id`, `status_task`, `check_in_record_id` 字段
#### 2. 数据流设计
```mermaid
flowchart TD
A[用户勾选生成视频并发布打卡] --> B[前端:调用KieAI创建视频任务]
B --> C[前端:获得taskId]
C --> D[前端:提交打卡 带taskId+enableAIVideo]
D --> E[后端:保存eb_user_sign记录]
E --> F[后端:创建eb_article记录]
F --> G[eb_article: taskId, statusTask=0, checkInRecordId]
H[饮食记录页面] --> I[加载打卡列表API]
I --> J[返回 taskId, enableAIVideo, videoUrl]
J --> K[前端:检测有taskId且无videoUrl]
K --> L[启动定时轮询 /kieai/video/task/taskId]
L --> M{任务状态?}
M -->|进行中| N[显示生成中状态]
M -->|完成| O[更新videoUrl到article]
M -->|失败| P[显示失败状态]
O --> Q[页面刷新后显示视频]
R[打卡详情页] --> S[查询详情API]
S --> T[返回taskId+videoUrl]
T --> U{有videoUrl?}
U -->|是| V[显示video标签播放]
U -->|否+有taskId| W[显示生成中状态]
X[社区最新tab] --> Y[查询v2_community_posts]
Y --> Z[关联eb_user_sign.enableAIVideo]
Z --> AA[显示有视频标记的帖子]
```
### 后端改动
#### 1. 更新打卡提交逻辑
`[ToolCheckinServiceImpl.submit()](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCheckinServiceImpl.java)`
- 在打卡成功后,如果 `enableAIVideo=1` 且有 `taskId`,创建 `eb_article` 记录:
- `type = 2` (视频类型)
- `task_id = taskId`
- `status_task = 0` (已创建)
- `check_in_record_id = userSign.id`
- `title` = 打卡备注或默认标题
- 其他字段按现有逻辑填充
#### 2. 更新打卡列表接口
`[ToolCheckinServiceImpl.getList()](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCheckinServiceImpl.java)`
- 查询时关联 `eb_article` 表(通过 `eb_user_sign.task_id = eb_article.task_id`)
- 返回字段增加:
- `videoUrl`: 从 `eb_article.video_url` 获取
- `videoStatus`: 从 `eb_article.status_task` 获取 (0=生成中, 1=完成, 2=失败)
- `taskId`: 已有
- `enableAIVideo`: 已有
#### 3. 更新打卡详情接口
`[ToolCheckinServiceImpl.getDetail()](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCheckinServiceImpl.java)`
- 增加返回 `videoUrl``videoStatus` 字段
#### 4. 完善KieAI回调处理
`[KieAIController.handleCallback()](msh_crmeb_22/crmeb-front/src/main/java/com/zbkj/front/controller/KieAIController.java)`
- 当前只记录日志,需改为:
- 解析回调参数获取 `taskId`, `state`, `result_urls`
- 根据 `taskId` 更新 `eb_article` 表:
- `status_task = 1` (成功) 或 `2` (失败)
- `video_url = result_urls[0]`
参考 `models-integration` 项目的实现:
```java
articleMapper.updateVideoUrlAndTaskStatusByTaskId(taskId, videoUrl, statusTask);
```
#### 5. 更新社区列表接口
`[ToolCommunityServiceImpl.getList()](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCommunityServiceImpl.java)`
- 当前已关联 `eb_user_sign` 获取 `mealType`
- 同样方式关联获取 `enable_ai_video``task_id`
- 通过 `task_id` 关联 `eb_article` 获取 `video_url`
- 返回字段增加:
- `hasVideo`: boolean (是否有视频)
- `videoUrl`: 视频地址
- `enableAIVideo`: 是否启用了视频生成
#### 6. 更新V2CommunityPost实体类
`[V2CommunityPost.java](msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/tool/V2CommunityPost.java)`
- 确认是否已有 `videoUrl` 字段,若无则添加:
```java
@ApiModelProperty(value = "视频地址")
private String videoUrl;
```
### 前端改动
#### 1. 饮食记录页面增加视频状态显示
`[dietary-records.vue](msh_single_uniapp/pages/tool/dietary-records.vue)`
**显示逻辑**:
- 列表项增加视频状态角标:
-`videoUrl`: 显示"📹视频"标记
-`taskId` 但无 `videoUrl`: 显示"⏳生成中"或进度条
- `videoStatus = 2`: 显示"❌生成失败"
**轮询逻辑**:
```javascript
data() {
return {
pollingTasks: [], // 需要轮询的任务列表
pollingTimer: null
}
},
methods: {
// 启动轮询
startPolling() {
if (this.pollingTimer) return;
this.pollingTimer = setInterval(() => {
this.pollVideoTasks();
}, 5000); // 每5秒查询一次
},
// 轮询视频任务状态
async pollVideoTasks() {
const tasksToCheck = this.recordList.filter(item =>
item.enableAIVideo && item.taskId && !item.videoUrl
);
if (tasksToCheck.length === 0) {
this.stopPolling();
return;
}
for (const task of tasksToCheck) {
try {
const res = await getVideoTaskStatus(task.taskId);
if (res.code === 200 && res.data) {
const status = res.data.state;
if (status === 'success') {
// 刷新列表以获取最新视频URL
this.loadRecordList();
break;
} else if (status === 'failed') {
// 标记失败
task.videoStatus = 2;
}
}
} catch (error) {
console.error('查询视频任务失败:', error);
}
}
},
// 停止轮询
stopPolling() {
if (this.pollingTimer) {
clearInterval(this.pollingTimer);
this.pollingTimer = null;
}
}
}
```
**生命周期**:
```javascript
onLoad() {
this.loadRecordList();
this.startPolling();
},
onUnload() {
this.stopPolling();
},
onShow() {
// 从其他页面返回时重新检查
this.loadRecordList();
this.startPolling();
}
```
**模板增加视频状态标记**:
```vue
<view class="video-status-badge" v-if="item.enableAIVideo">
<text v-if="item.videoUrl" class="status-success">📹 有视频</text>
<text v-else-if="item.videoStatus === 2" class="status-failed"> 生成失败</text>
<text v-else class="status-pending"> 生成中</text>
</view>
```
#### 2. 打卡详情页增加视频播放
`[checkin-detail.vue](msh_single_uniapp/pages/tool/checkin-detail.vue)`
**数据结构更新**:
```javascript
data() {
return {
checkinData: {
// ...现有字段
videoUrl: '',
taskId: '',
enableAIVideo: false,
videoStatus: 0
}
}
}
```
**模板增加视频播放器**:
```vue
<!-- 在图片轮播区域下方或上方增加 -->
<view class="video-section" v-if="checkinData.videoUrl">
<view class="video-header">
<text class="video-icon">🎬</text>
<text class="video-title">打卡视频</text>
</view>
<video
class="checkin-video"
:src="checkinData.videoUrl"
controls
:show-center-play-btn="true"
:enable-progress-gesture="true"
object-fit="contain"
></video>
</view>
<!-- 视频生成中状态 -->
<view class="video-generating" v-else-if="checkinData.enableAIVideo && checkinData.taskId">
<text class="generating-icon"></text>
<text class="generating-text">视频生成中,请稍后...</text>
</view>
```
**formatCheckinData更新**:
```javascript
formatCheckinData(item) {
// ...现有逻辑
this.checkinData = {
// ...现有字段
videoUrl: item.videoUrl || '',
taskId: item.taskId || '',
enableAIVideo: item.enableAIVideo || false,
videoStatus: item.videoStatus || 0
};
}
```
#### 3. 社区页面显示视频标记
`[community.vue](msh_single_uniapp/pages/tool_main/community.vue)`
**formatPostList更新**:
```javascript
formatPostList(list) {
return list.map(item => {
// ...现有逻辑
return {
// ...现有字段
hasVideo: item.hasVideo || false,
videoUrl: item.videoUrl || '',
enableAIVideo: item.enableAIVideo || false
};
});
}
```
**卡片增加视频标记**:
```vue
<view class="post-card">
<!-- 现有内容 -->
<!-- 视频标记 -->
<view class="video-badge" v-if="item.hasVideo || item.videoUrl">
<text class="badge-icon">🎬</text>
<text class="badge-text">视频</text>
</view>
</view>
```
**样式**:
```scss
.video-badge {
position: absolute;
top: 8px;
right: 8px;
background: rgba(0, 0, 0, 0.6);
border-radius: 12px;
padding: 4px 10px;
display: flex;
align-items: center;
gap: 4px;
.badge-icon {
font-size: 14px;
}
.badge-text {
font-size: 12px;
color: #fff;
}
}
```
#### 4. 新增API方法
`[tool.js](msh_single_uniapp/api/tool.js)`
```javascript
/**
* 查询视频任务状态
* @param {String} taskId - 任务ID
*/
export function getVideoTaskStatus(taskId) {
return request.get(`kieai/video/task/${taskId}`, {
apiKey: 'your-api-key' // 从配置获取
});
}
```
### 关键技术细节
#### 1. 轮询策略
- **触发条件**: 列表中存在 `enableAIVideo=true``taskId` 不为空但 `videoUrl` 为空的记录
- **轮询间隔**: 5秒
- **停止条件**:
- 所有任务都有 `videoUrl``videoStatus=2`
- 页面销毁(onUnload)
- 超过最大轮询次数(如60次,即5分钟)
- **优化**: 只轮询当前可见列表中的任务,避免全量查询
#### 2. 视频播放兼容性
- 使用uni-app的 `<video>` 组件,支持小程序、H5、APP
- 视频格式要求: mp4 (KieAI返回格式)
- 封面图: 可使用打卡的第一张照片作为视频封面
#### 3. 错误处理
- **视频生成失败**: 显示失败状态,允许用户重新生成或查看原因
- **网络错误**: 轮询失败时静默处理,不影响用户浏览
- **回调失败**: 前端轮询作为兜底方案
## 文件清单
### 后端Java文件
| 文件 | 改动类型 | 说明 |
| -------------------------------------------------------------------------------------------------------------------------------------------- | ----- | -------------------------------------------- |
| `[ToolCheckinServiceImpl.java](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCheckinServiceImpl.java)` | 修改 | submit增加article创建,getList/getDetail增加video字段 |
| `[KieAIController.java](msh_crmeb_22/crmeb-front/src/main/java/com/zbkj/front/controller/KieAIController.java)` | 修改 | 完善回调处理更新article表 |
| `[ToolKieAIServiceImpl.java](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolKieAIServiceImpl.java)` | 修改 | handleTaskCallback增加更新article逻辑 |
| `[ToolCommunityServiceImpl.java](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCommunityServiceImpl.java)` | 修改 | getList关联video信息 |
| `[V2CommunityPost.java](msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/tool/V2CommunityPost.java)` | 检查/修改 | 确认有videoUrl字段 |
| `[ArticleDao.java](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/dao/ArticleDao.java)` | 新增方法 | 增加updateVideoUrlByTaskId方法 |
### 前端Vue文件
| 文件 | 改动类型 | 说明 |
| ------------------------------------------------------------------------- | ---- | ------------------------ |
| `[dietary-records.vue](msh_single_uniapp/pages/tool/dietary-records.vue)` | 修改 | 增加视频状态显示和轮询逻辑 |
| `[checkin-detail.vue](msh_single_uniapp/pages/tool/checkin-detail.vue)` | 修改 | 增加视频播放器 |
| `[community.vue](msh_single_uniapp/pages/tool_main/community.vue)` | 修改 | 增加视频标记 |
| `[tool.js](msh_single_uniapp/api/tool.js)` | 新增方法 | 增加getVideoTaskStatus API |
## 测试要点
1. **打卡流程**: 勾选生成视频 → 发布 → 验证taskId保存 → 验证article记录创建
2. **状态轮询**: 饮食记录页面显示"生成中" → 等待KieAI完成 → 自动更新显示"有视频"
3. **视频播放**: 打卡详情查看视频 → 播放正常 → 控制条功能正常
4. **社区显示**: 最新tab显示有视频标记的帖子 → 点击查看带视频
5. **异常处理**: 视频生成失败 → 显示失败状态 → 不影响打卡记录查看
6. **回调机制**: KieAI回调成功 → article表正确更新 → 前端轮询立即获取到结果

View File

@@ -0,0 +1,166 @@
---
name: 计算器结果保存食谱
overview: 在用户采纳营养计划时,将食谱计算器的配餐方案提取并保存到 v2_recipes 食谱表,并在首页"精选食谱"中混合展示用户自己的食谱(排在前面)。
todos:
- id: db-migration
content: 新建 SQL 变更文件v2_recipes 表新增 source 和 source_id 字段
status: completed
- id: entity-update
content: V2Recipe.java 实体类新增 source 和 sourceId 属性
status: completed
- id: adopt-save-recipe
content: ToolCalculatorServiceImpl.adopt() 中新增保存食谱到 v2_recipes 的逻辑(含幂等检查)
status: completed
- id: home-mix-recipes
content: ToolHomeServiceImpl.getRecommendedRecipes() 混入当前用户的计算器食谱
status: completed
- id: home-return-fields
content: 推荐食谱接口返回字段补充 description、source、totalProtein
status: completed
- id: frontend-recipe-card
content: 首页 index.vue 食谱卡片展示适配tag、desc 等字段映射)
status: completed
isProject: false
---
# 食谱计算器结果保存到食谱表
## 整体流程
```mermaid
sequenceDiagram
participant User as 用户
participant FE as 前端
participant Ctrl as ToolController
participant CalcSvc as ToolCalculatorServiceImpl
participant RecipeDao as V2RecipeDao
participant HomeSvc as ToolHomeServiceImpl
User->>FE: 点击"采纳计划"
FE->>Ctrl: POST /calculator/adopt
Ctrl->>CalcSvc: adopt(resultId)
CalcSvc->>CalcSvc: 创建营养计划 (已有逻辑)
CalcSvc->>RecipeDao: 保存一条食谱到 v2_recipes
CalcSvc-->>FE: 返回 adoptResponse
Note over FE: 首页刷新时
FE->>Ctrl: GET /home/recipes
Ctrl->>HomeSvc: getRecommendedRecipes()
HomeSvc->>RecipeDao: 查询推荐食谱 + 用户自己的食谱
HomeSvc-->>FE: 返回混合列表
```
## 1. 数据库变更 - v2_recipes 新增字段
需要新增两个字段以追踪食谱来源:
```sql
ALTER TABLE v2_recipes
ADD COLUMN source VARCHAR(20) DEFAULT 'manual' COMMENT '来源manual(手动)/calculator(计算器)/ai(AI生成)' AFTER sort_order,
ADD COLUMN source_id BIGINT DEFAULT NULL COMMENT '来源ID如计算器结果ID' AFTER source;
```
同时新增一个变更 SQL 文件记录此次 DDL。
## 2. 后端 - 实体类更新
**文件**: [V2Recipe.java](msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/tool/V2Recipe.java)
新增两个字段:
```java
@ApiModelProperty(value = "来源manual/calculator/ai")
private String source;
@ApiModelProperty(value = "来源ID计算器结果ID等")
private Long sourceId;
```
## 3. 后端 - 采纳时保存食谱
**文件**: [ToolCalculatorServiceImpl.java](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCalculatorServiceImpl.java)
`adopt()` 方法中,步骤 5创建营养计划之后新增步骤"保存食谱到 v2_recipes"
- 注入 `V2RecipeDao`
-`V2CalculatorResult` 中解析 `mealPlanJson` 得到 MealPlan
- 构建 `V2Recipe` 对象:
- `userId` = 当前用户ID
- `name` = "每日营养配餐 - {ckdStage}",如 "每日营养配餐 - CKD 5期"
- `description` = "蛋白质 {proteinIntake}g/天 | 能量 {energyIntake}kcal/天"
- `coverImage` = 取午餐第一道菜的图片 URL最具代表性
- `mealType` = null整日配餐非单餐
- `category` = "营养配餐"
- `tagsJson` = `["AI配餐", "{ckdStage}"]`
- `ingredientsJson` = 聚合三餐所有食材
- `stepsJson` = 存放完整 mealPlanJson早/午/晚餐详情),复用此字段存储配餐详情
- `totalProtein` = result.getProteinIntake()
- `totalEnergy` = result.getEnergyIntake()
- `suitableStagesJson` = `["{ckdStage}"]`
- `suitableDialysis` = result.getHasDialysis()
- `status` = "published"
- `isRecommend` = 0不进入官方推荐通过 source + userId 查询)
- `isOfficial` = 0
- `source` = "calculator"
- `sourceId` = resultId
- 幂等处理:先检查是否已存在 `source='calculator' AND source_id=resultId` 的记录,避免重复
## 4. 后端 - 首页推荐食谱混入用户食谱
**文件**: [ToolHomeServiceImpl.java](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolHomeServiceImpl.java)
修改 `getRecommendedRecipes()` 方法:
1. 如果用户已登录,先查询该用户的食谱(`user_id = currentUserId AND status = 'published' AND source = 'calculator'`,按 `created_at DESC`,取最新 1 条)
2. 再查询官方推荐食谱(现有逻辑,`is_recommend = 1`
3. 将用户食谱排在前面,官方推荐排在后面,合并后返回
4. 总数仍控制在 `limit` 范围内(如用户有 1 条自己的食谱,则官方推荐取 limit-1 条)
5. 缓存 key 需要区分用户(登录用户的缓存 key 加上 userId
## 5. 后端 - 推荐食谱接口返回字段补充
**文件**: [ToolHomeServiceImpl.java](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolHomeServiceImpl.java)
在返回数据 map 中增加字段,使前端能够区分并展示更多信息:
```java
map.put("id", recipe.getRecipeId());
map.put("name", recipe.getName());
map.put("coverImage", recipe.getCoverImage());
map.put("totalEnergy", recipe.getTotalEnergy());
map.put("description", recipe.getDescription()); // 新增
map.put("source", recipe.getSource()); // 新增:标记来源
map.put("totalProtein", recipe.getTotalProtein()); // 新增
```
## 6. 前端 - 首页食谱展示适配
**文件**: [index.vue (首页)](msh_single_uniapp/pages/tool_main/index.vue)
调整食谱卡片展示逻辑:
- `item.tag`:如果 `source === 'calculator'` 显示 "我的配餐",否则显示 "推荐"
- `item.tagClass`:根据来源使用不同样式(如用户食谱用不同颜色)
- `item.desc`:使用 `description` 字段(如 "蛋白质 75.6g/天 | 能量 2205kcal/天"
- `item.time`:可不显示或显示 "每日配餐"
- `item.views`:使用 `viewCount` 或隐藏
修改 `loadData` 中对 `recipeList` 的处理逻辑,将后端返回的数据映射为前端需要的格式。
## 7. 前端 - 食谱详情页适配
点击首页食谱卡片跳转到 `recipe-detail` 页面,需要确认该页面能正确显示来自计算器的食谱内容(特别是 `stepsJson` 中存储的 mealPlan 数据)。如果需要特殊处理,在详情页根据 `source` 字段做渲染分支。
## 涉及文件清单
| 层级 | 文件 | 改动 |
| -------- | --------------------------------------------- | ------------------------------ |
| SQL | `docs/sql/v2_recipes_add_source.sql`(新建) | 新增 source、source_id 字段 |
| Entity | `V2Recipe.java` | 新增 source、sourceId 字段 |
| Service | `ToolCalculatorServiceImpl.java` | adopt() 中新增保存食谱逻辑 |
| Service | `ToolHomeServiceImpl.java` | getRecommendedRecipes() 混入用户食谱 |
| Frontend | `msh_single_uniapp/pages/tool_main/index.vue` | 食谱卡片展示适配 |

View File

@@ -0,0 +1,256 @@
---
name: ""
overview: ""
todos: []
isProject: false
---
# 诊断article.status_task异常和video_url未更新问题
## 问题描述
**问题1**提交饮食打卡记录时article表中的status_task字段保持为0或变成2而不是预期的1完成
**问题2**article表中的video_url字段始终为NULL即使视频已经生成完成
**关键信息**
- KieAI回调一般需要10秒以上才到达不会立即回调
- 两个问题有关联,都指向**KieAI回调没有正常执行或执行失败**
## 根因分析
### 核心问题KieAI回调未正常执行
根据分析,最可能的原因是:
**原因1回调反序列化失败**(最可能)
根据[修复kieai回调反序列化问题计划](修复kieai回调反序列化问题_85d41db5.plan.md)KieAI回调到达后端但Jackson反序列化失败
```
Could not resolve parameter [0]... Problem deserializing 'setterless' property 'resultUrls':
get method returned null
```
这导致:
1. `handleCallback` 方法无法执行
2. `handleTaskCallback` 中更新article的逻辑从未运行
3. `video_url``status_task` 字段保持未更新状态
**原因2回调URL配置错误**
- `kieAIConfig.getApiCallbackUrl()` 可能未配置或配置错误
- KieAI服务无法访问回调地址网络问题、防火墙等
- 回调地址格式不正确
**原因3回调数据格式问题**
- KieAI返回的数据结构与`KieAIQueryTaskResponse`不匹配
- `resultUrls`字段名称或格式异常
## 执行流程分析
### 预期流程(正常情况)
```mermaid
sequenceDiagram
participant Frontend as 前端
participant Sora2 as ToolSora2ServiceImpl
participant KieAI as KieAI服务
participant Callback as KieAIController
participant Service as ToolKieAIServiceImpl
participant DB as 数据库
Frontend->>Sora2: createImageToVideoTask
Sora2->>KieAI: POST createTask含callBackUrl
KieAI-->>Sora2: 返回taskId
Sora2->>DB: INSERT article status_task=0
Note over DB: status_task = 0
Note over KieAI: 10秒以上视频生成中
KieAI->>Callback: POST callback含resultUrls
Callback->>Callback: 反序列化KieAIQueryTaskResponse
Callback->>Service: handleTaskCallback
Service->>DB: SELECT article WHERE task_id
Service->>Service: extractVideoUrl
Service->>DB: UPDATE video_url status_task=1
Note over DB: video_url有值 status_task=1
```
### 实际流程(异常情况)
```mermaid
sequenceDiagram
participant Frontend as 前端
participant Sora2 as ToolSora2ServiceImpl
participant KieAI as KieAI服务
participant Callback as KieAIController
participant DB as 数据库
Frontend->>Sora2: createImageToVideoTask
Sora2->>KieAI: POST createTask
KieAI-->>Sora2: 返回taskId
Sora2->>DB: INSERT article status_task=0
Note over DB: status_task = 0
Note over KieAI: 10秒以上...
KieAI->>Callback: POST callback
Callback--xCallback: 反序列化失败
Note over Callback: Jackson报错setterless property resultUrls
Note over DB: video_url仍为NULL status_task仍为0
Note over Frontend,DB: 回调处理逻辑从未执行数据库未更新
```
## 解决方案
### 方案1修复回调反序列化问题主要修复
**文件**[KieAIQueryTaskResponse.java](msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/response/kieai/KieAIQueryTaskResponse.java)
**问题代码**TaskData类
```java
@Data
public static class TaskData implements Serializable {
@ApiModelProperty(value = "结果URL数组视频/图片地址)")
private java.util.List<String> resultUrls; // Lombok生成的setter可能被Jackson忽略
}
```
**修复方法**显式添加setter方法
```java
@Data
public static class TaskData implements Serializable {
@ApiModelProperty(value = "结果URL数组视频/图片地址)")
private java.util.List<String> resultUrls;
// 显式添加setter以确保Jackson反序列化正常
public void setResultUrls(java.util.List<String> resultUrls) {
this.resultUrls = resultUrls;
}
}
```
### 方案2验证回调URL配置
**检查配置文件**application.yml或application.properties
```yaml
kie-ai:
api-callback-url: http://your-domain.com/api/front/kieai/callback
```
**验证要点**
1. URL必须是KieAI服务可访问的公网地址
2. 确保没有防火墙阻止
3. 路径正确:`/api/front/kieai/callback`
### 方案3添加详细日志辅助调试
在[KieAIController.java](msh_crmeb_22/crmeb-front/src/main/java/com/zbkj/front/controller/KieAIController.java)的回调方法开头添加:
```java
@PostMapping("/callback")
public KieAINanoBananaResponse<String> handleCallback(
@RequestBody KieAIQueryTaskResponse callbackData) {
try {
logger.info("======== KieAI回调原始数据 ========");
logger.info("完整对象: {}", JSON.toJSONString(callbackData));
logger.info("taskId: {}", callbackData.getTaskId());
logger.info("state: {}", callbackData.getState());
logger.info("resultUrls: {}", callbackData.getResultUrls());
String taskId = callbackData.getTaskId();
logger.info("接收KieAI任务回调任务ID: {}", taskId);
toolKieAIService.handleTaskCallback(taskId, callbackData);
return KieAINanoBananaResponse.success("回调处理成功");
} catch (Exception e) {
logger.error("处理任务回调失败", e);
return KieAINanoBananaResponse.fail("回调处理失败");
}
}
```
## 诊断和修复步骤
### 步骤1修复反序列化问题优先级最高
1. 修改`KieAIQueryTaskResponse.java`中的`TaskData`显式添加setter
2. 重启后端服务
3. 提交一次打卡测试
### 步骤2检查后端日志
**如果回调仍未执行**,检查日志:
1. 搜索:`接收KieAI任务回调`
- 找到:回调到达了,检查反序列化错误
- 没找到回调未到达检查回调URL配置
2. 搜索:`deserializing``Jackson``Could not resolve`
- 找到:确认是反序列化问题
3. 查看article创建日志
```
文章新增成功, taskId: xxx
```
### 步骤3验证回调URL配置
1. 检查`application.yml`中的`kie-ai.api-callback-url`
2. 确认URL可从外网访问
3. 测试回调接口是否正常响应
### 步骤4验证修复效果
提交打卡后,检查数据库:
```sql
SELECT id, task_id, video_url, status_task, create_time, update_time
FROM eb_article
WHERE task_id = 'your_task_id'
ORDER BY create_time DESC
LIMIT 1;
```
**预期结果**
- `status_task = 1`(任务完成)
- `video_url` 有值
- `update_time` 在create_time的10秒以上后更新
## 问题总结
### 核心原因
KieAI回调无法被正确处理导致
1. `video_url`未更新始终为NULL
2. `status_task`未更新保持为0
### 最可能的原因
根据[修复kieai回调反序列化问题计划](修复kieai回调反序列化问题_85d41db5.plan.md)Jackson反序列化失败导致回调处理方法无法执行。
### 修复优先级
1. **优先级1**:修复`KieAIQueryTaskResponse.TaskData`反序列化添加显式setter
2. **优先级2**验证回调URL配置
3. **优先级3**:添加详细日志
## 涉及文件
- [KieAIQueryTaskResponse.java](msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/response/kieai/KieAIQueryTaskResponse.java) - 主要修复
- [KieAIController.java](msh_crmeb_22/crmeb-front/src/main/java/com/zbkj/front/controller/KieAIController.java) - 回调入口
- [ToolKieAIServiceImpl.java](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolKieAIServiceImpl.java) - 回调处理
- [ToolSora2ServiceImpl.java](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolSora2ServiceImpl.java) - article创建

View File

@@ -0,0 +1,91 @@
---
name: 食谱百科食物图AI生成与OSS更新
overview: 对 v2_foods 表中 image 非阿里云 OSS 的记录,根据 name 调用 KieAI 生成图片(压缩至不超过 100KB上传到 OSS 并回写 v2_foods.image复用现有 DishImageServiceImpl 的 KieAI + 压缩 + OSS 能力,扩展“食物”场景并增加更新数据库的入口与触发方式。
todos: []
isProject: false
---
# 食谱百科食物图 AI 生成并更新 v2_foods.image
## 现状
- **食谱百科前端**`[msh_single_uniapp/pages/tool/food-encyclopedia.vue](msh_single_uniapp/pages/tool/food-encyclopedia.vue)` 通过 `getFoodList` / `searchFood``[api/tool.js](msh_single_uniapp/api/tool.js)`)请求 `tool/food/list``tool/food/search`,列表项中的 `image` 直接来自后端。
- **后端数据**`[ToolFoodServiceImpl](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolFoodServiceImpl.java)``v2_foods` 查数据,返回的 map 包含 `image``[V2Food](msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/tool/V2Food.java)``image` 字段)。当前部分记录的 `image` 为 Figma/非 OSS 等非阿里云地址。
- **可复用能力**`[DishImageServiceImpl](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/DishImageServiceImpl.java)` 已实现KieAI 文生图 → 下载图片 → **压缩至 100KB**`MAX_IMAGE_BYTES = 100*1024``compressImageToMaxBytes`)→ 上传 OSS`recipes/`)→ 写 `V2DishImageCache`。菜品 prompt 为“一道精美的中式菜品照片:{name}…”。
## 目标
-**v2_foods****image 非阿里云 OSS** 的记录:根据 **name** 调用 AI 生成图片,生成图 **不超过 100KB**,上传到 OSS 后 **更新 v2_foods.image** 为 OSS 地址。
- 判断“非 OSS”`image` 为空或 `image` 不包含当前项目使用的 OSS 域名(如配置中的 `alUploadUrl` 或固定包含 `aliyuncs.com`)。
## 实现方案
### 1. 判断“是否为 OSS 地址”
- 在公共工具或 Service 内封装:若 `image` 为空/blank视为非 OSS若不为空则判断是否包含 OSS 域名(可从 `SystemConfigService.getValueByKey(SysConfigConstants.CONFIG_AL_UPLOAD_URL)` 取域名,或简单用 `image.contains("aliyuncs.com")`)。满足则为 OSS否则需要补图。
### 2. 后端:扩展“食物图片”能力并写回 v2_foods
**方案 A推荐在现有 DishImageServiceImpl 上扩展**
-`[DishImageServiceImpl](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/DishImageServiceImpl.java)` 中:
- 增加 **食物** 用 prompt 方法,例如:`buildFoodImagePrompt(String foodName)`,内容为“一份新鲜的食物/食材照片:{name},高清静物摄影,白色背景或餐盘,自然光,细节清晰”(与菜品区分开)。
- 增加 **食物** 的 OSS 路径常量,如 `OSS_FOODS_PATH = "foods/"`,上传时使用 `foods/food-{sanitizedName}-{timestamp}.jpg`,与 `recipes/` 区分。
- 新增 **公共流程**`generateImageAndUploadToOss(String name, String prompt, String ossPathPrefix)`(或拆成:用现有 KieAI + 下载 + `compressImageToMaxBytes(imageBytes, 100*1024)` + 上传,仅参数为 name/prompt/路径前缀),返回 OSS 完整 URL。
- 注入 `V2FoodDao`,新增方法:`ensureFoodImageAndUpdateDb(Long foodId)`
- 根据 foodId 查 `V2Food`
-`image` 已是 OSS用上述判断直接返回当前 image
- 否则用 `buildFoodImagePrompt(food.getName())` 调用上述“生成并上传”流程,得到 ossUrl
- 更新 `food.setImage(ossUrl)``v2FoodDao.updateById(food)`
- 失败时可按现有逻辑降级为占位图并同样写回 DB或只打日志不更新视产品要求而定。
-`[DishImageService](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/tool/DishImageService.java)` 接口中声明 `ensureFoodImageAndUpdateDb(Long foodId)`(或返回 String 的 `ensureFoodImageUrl(Long foodId)` 由调用方更新 DB二选一即可
**方案 B新建 FoodImageService**
- 新建 `FoodImageService` + `FoodImageServiceImpl`,内部注入 `DishImageService` 或直接复用 KieAI、Oss、压缩逻辑若将 DishImageServiceImpl 中下载/压缩/上传抽成 package 内可复用方法,或抽到公共工具类)。
- `FoodImageServiceImpl.ensureFoodImageAndUpdateDb(Long foodId)` 查 v2_foods → 判断 image 非 OSS → 调 KieAI 生图 → 压缩 ≤100KB → 上传 OSS路径 `foods/`)→ 更新 `v2_foods.image`
- 与方案 A 二选一即可;方案 A 复用更集中,改动面小。
### 3. 触发方式(可选其一或组合)
- **按需(列表/详情)**:在 `[ToolFoodServiceImpl](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolFoodServiceImpl.java)``getList`/`search` 返回列表前,对每条记录的 `image` 判断;若非 OSS可**异步**调用 `ensureFoodImageAndUpdateDb(food.getFoodId())`,本次仍返回原 image下次请求得到 OSS 地址;或**同步**调用(会拉长接口耗时,需权衡)。详情 `getDetail` 同理,对当前条若 image 非 OSS 可同步/异步补图并更新。
- **批量/管理端**:在 `[ToolController](msh_crmeb_22/crmeb-front/src/main/java/com/zbkj/front/controller/ToolController.java)` 或管理端增加接口,例如 `POST /api/front/tool/food/refresh-images` 或后台 `POST /api/admin/tool/food/refresh-images`:查询 `v2_foods``image` 为空或非 OSS 的记录,循环调用 `ensureFoodImageAndUpdateDb(foodId)`,可限制单次条数(如 20并返回处理数量避免一次性跑全表。
### 4. 技术细节摘要
- **图片大小**:沿用 `DishImageServiceImpl``compressImageToMaxBytes(bytes, 100*1024L)`,保证上传前 ≤100KB。
- **OSS 路径**:使用 `foods/food-{sanitizedName}-{timestamp}.jpg`,与菜品 `recipes/dish-...` 区分,便于运维与排查。
- **KieAI 配置**:与现有菜品一致,使用 `KieAIConfig``ToolKieAIService`;未配置 Token 时可按现有逻辑降级(占位图或跳过更新)。
- **前端**:无需改;列表/详情接口返回的 `image` 更新为 OSS 后,`[food-encyclopedia.vue](msh_single_uniapp/pages/tool/food-encyclopedia.vue)``[food-detail.vue](msh_single_uniapp/pages/tool/food-detail.vue)` 会自然展示新图。
### 5. 可选说明(不阻塞本次需求)
- 食谱百科列表跳详情当前为 `id=${item.name}`,而后端 `getFoodDetail``Long id`;若实际数据以 id 为主,建议前端改为传 `id=${item.id}` 并在详情用 id 请求,避免按 name 查的兼容逻辑。
## 涉及文件(建议)
| 类型 | 路径 |
| -------- | ---------------------------------------------------------------------------------------------------------------------------- |
| 接口 | `msh_crmeb_22/crmeb-service/.../tool/DishImageService.java` |
| 实现 | `msh_crmeb_22/crmeb-service/.../tool/DishImageServiceImpl.java` |
| 食物服务/控制器 | `msh_crmeb_22/crmeb-service/.../tool/ToolFoodServiceImpl.java``msh_crmeb_22/crmeb-front/.../ToolController.java`(若做按需或批量触发) |
## 流程概览
```mermaid
flowchart LR
A[v2_foods 记录] --> B{image 是否 OSS?}
B -->|是| C[直接使用]
B -->|否| D[buildFoodImagePrompt]
D --> E[KieAI 文生图]
E --> F[下载图片]
F --> G[压缩至 100KB]
G --> H[上传 OSS foods/]
H --> I[更新 v2_foods.image]
I --> C
```

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"java.configuration.updateBuildConfiguration": "interactive"
}

BIN
docs/.DS_Store vendored Normal file

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,542 @@
# Coze API 测试文档
> **版本**v1.0
> **创建日期**2026-02-08
> **基础路径**`/api/front/coze`
> **服务端口**20822
> **认证方式**:免登录(已加入白名单)
---
## 一、配置信息
### 1.1 当前环境配置
| 配置项 | 值 | 说明 |
|--------|-----|------|
| Base URL | `https://api.coze.cn` | Coze 平台 API 地址 |
| Auth Type | `pat` | Personal Access Token 模式 |
| Default Bot ID | `7591133240535449654` | 食谱计算器 Bot |
| Default User ID | `3243981400446844` | 测试用户 ID |
---
## 二、接口清单
| 序号 | 接口名称 | HTTP方法 | 路径 | 说明 |
|------|----------|----------|------|------|
| 1 | 发起对话 | `POST` | `/chat` | 与 Coze Bot 对话(支持流式/非流式) |
| 2 | 流式对话 | `POST` | `/chat/stream` | SSE 流式对话 |
| 3 | 执行工作流 | `POST` | `/workflow/run` | 触发预定义工作流 |
| 4 | 流式执行工作流 | `POST` | `/workflow/stream` | SSE 流式执行工作流 |
| 5 | 查看对话详情 | `POST` | `/chat/retrieve` | 查询对话状态 |
| 6 | 查看对话消息列表 | `POST` | `/chat/messages/list` | 获取对话消息 |
| 7 | 上传文件 | `POST` | `/file/upload` | 上传文件获取 file_id |
---
## 三、接口详细说明
### 3.1 发起对话
#### 基础信息
| 项目 | 值 |
|------|------|
| 请求路径 | `POST /api/front/coze/chat` |
| Content-Type | `application/json` |
| 是否鉴权 | **否**(白名单) |
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| `botId` | String | 是 | Coze Bot ID |
| `userId` | String | 是 | 业务系统用户 ID |
| `stream` | Boolean | 否 | 是否流式返回,默认 false |
| `chatHistory` | Array | 否 | 历史对话上下文 |
| `additionalMessages` | Array | 否 | 当前发送的消息 |
**chatHistory 结构**
| 参数名 | 类型 | 说明 |
|--------|------|------|
| `role` | String | 角色:`user` / `assistant` |
| `content` | String | 消息内容 |
| `contentType` | String | 内容类型:`text` / `object_string` / `card` |
#### 测试用例
##### TC-COZE-01: 基础对话测试
```bash
curl -X POST 'http://localhost:20822/api/front/coze/chat' \
-H "Content-Type: application/json" \
-d '{
"botId": "7591133240535449654",
"userId": "test_user_001",
"stream": false,
"additionalMessages": [
{
"role": "user",
"content": "你好,请介绍一下自己"
}
]
}'
```
**预期响应**
```json
{
"code": 200,
"message": "success",
"data": {
"id": "xxxxx",
"conversationId": "xxxxx",
"botId": "7591133240535449654",
"status": "completed",
"createdAt": 1707350400,
"completedAt": 1707350405
}
}
```
##### TC-COZE-02: 食谱计算器测试(男性透析患者)
```bash
curl -X POST 'http://localhost:20822/api/front/coze/chat' \
-H "Content-Type: application/json" \
-d '{
"botId": "7591133240535449654",
"userId": "3243981400446844",
"stream": false,
"additionalMessages": [
{
"role": "user",
"content": "请帮我计算营养方案我的信息如下性别男年龄55岁身高170cm正在进行血液透析干体重65.5kg血肌酐850μmol/L"
}
]
}'
```
##### TC-COZE-03: 食谱计算器测试(女性非透析患者)
```bash
curl -X POST 'http://localhost:20822/api/front/coze/chat' \
-H "Content-Type: application/json" \
-d '{
"botId": "7591133240535449654",
"userId": "3243981400446844",
"stream": false,
"additionalMessages": [
{
"role": "user",
"content": "请帮我计算营养方案我的信息如下性别女年龄48岁身高160cm未透析体重52kg血肌酐180μmol/L"
}
]
}'
```
##### TC-COZE-04: 带历史上下文的对话
```bash
curl -X POST 'http://localhost:20822/api/front/coze/chat' \
-H "Content-Type: application/json" \
-d '{
"botId": "7591133240535449654",
"userId": "test_user_001",
"stream": false,
"chatHistory": [
{
"role": "user",
"content": "我是一名55岁的男性透析患者",
"contentType": "text"
},
{
"role": "assistant",
"content": "好的,我了解了您的基本情况。请问您还有其他健康数据需要提供吗?",
"contentType": "text"
}
],
"additionalMessages": [
{
"role": "user",
"content": "我的身高是170cm体重65.5kg血肌酐850"
}
]
}'
```
---
### 3.2 流式对话
#### 基础信息
| 项目 | 值 |
|------|------|
| 请求路径 | `POST /api/front/coze/chat/stream` |
| Content-Type | `application/json` |
| Response Type | `text/event-stream` |
#### 测试用例
##### TC-COZE-05: 流式对话测试
```bash
curl -X POST 'http://localhost:20822/api/front/coze/chat/stream' \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{
"botId": "7591133240535449654",
"userId": "test_user_001",
"additionalMessages": [
{
"role": "user",
"content": "请详细介绍一下肾病患者的饮食注意事项"
}
]
}'
```
**预期响应SSE 格式)**
```
event: message
data: {"event":"conversation.chat.created","chat":{"id":"xxx","conversation_id":"xxx"}}
event: message
data: {"event":"conversation.message.delta","message":{"content":"肾病患者..."}}
event: message
data: {"event":"conversation.message.delta","message":{"content":"需要注意..."}}
event: message
data: {"event":"conversation.chat.completed","chat":{"status":"completed"}}
```
---
### 3.3 执行工作流
#### 基础信息
| 项目 | 值 |
|------|------|
| 请求路径 | `POST /api/front/coze/workflow/run` |
| Content-Type | `application/json` |
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| `workflowId` | String | 是 | 工作流 ID |
| `parameters` | Object | 否 | 工作流输入参数 |
| `isAsync` | Boolean | 否 | 是否异步执行,默认 false |
#### 测试用例
##### TC-COZE-06: 同步执行工作流
```bash
curl -X POST 'http://localhost:20822/api/front/coze/workflow/run' \
-H "Content-Type: application/json" \
-d '{
"workflowId": "1180790412263",
"isAsync": false,
"parameters": {
"gender": "male",
"age": 55,
"height": 170,
"dialysis": true,
"dialysisType": "hemodialysis",
"dryWeight": 65.5,
"creatinine": 850
}
}'
```
##### TC-COZE-07: 异步执行工作流
```bash
curl -X POST 'http://localhost:20822/api/front/coze/workflow/run' \
-H "Content-Type: application/json" \
-d '{
"workflowId": "1180790412263",
"isAsync": true,
"parameters": {
"userId": "test_user_001",
"recordId": 12345
}
}'
```
---
### 3.4 流式执行工作流
#### 基础信息
| 项目 | 值 |
|------|------|
| 请求路径 | `POST /api/front/coze/workflow/stream` |
| Content-Type | `application/json` |
| Response Type | `text/event-stream` |
#### 测试用例
##### TC-COZE-08: 流式工作流测试
```bash
curl -X POST 'http://localhost:20822/api/front/coze/workflow/stream' \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{
"workflowId": "1180790412263",
"parameters": {
"input": "分析用户的饮食记录"
}
}'
```
---
### 3.5 查看对话详情
#### 基础信息
| 项目 | 值 |
|------|------|
| 请求路径 | `POST /api/front/coze/chat/retrieve` |
| Content-Type | `application/json` |
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| `conversationId` | String | 是 | 对话 ID |
| `chatId` | String | 是 | Chat ID |
#### 测试用例
##### TC-COZE-09: 查询对话详情
```bash
curl -X POST 'http://localhost:20822/api/front/coze/chat/retrieve' \
-H "Content-Type: application/json" \
-d '{
"conversationId": "7460461142355574825",
"chatId": "7460461142355590000"
}'
```
**预期响应**
```json
{
"code": 200,
"message": "success",
"data": {
"id": "7460461142355590000",
"conversationId": "7460461142355574825",
"botId": "7591133240535449654",
"status": "completed",
"createdAt": 1707350400,
"completedAt": 1707350410,
"usage": {
"tokenCount": 1500,
"outputCount": 800,
"inputCount": 700
}
}
}
```
---
### 3.6 查看对话消息列表
#### 基础信息
| 项目 | 值 |
|------|------|
| 请求路径 | `POST /api/front/coze/chat/messages/list` |
| Content-Type | `application/json` |
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| `conversationId` | String | 是 | 对话 ID |
| `chatId` | String | 是 | Chat ID |
#### 测试用例
##### TC-COZE-10: 获取对话消息列表
```bash
curl -X POST 'http://localhost:20822/api/front/coze/chat/messages/list' \
-H "Content-Type: application/json" \
-d '{
"conversationId": "7460461142355574825",
"chatId": "7460461142355590000"
}'
```
**预期响应**
```json
{
"code": 200,
"message": "success",
"data": [
{
"id": "msg_001",
"role": "user",
"type": "question",
"content": "请帮我计算营养方案",
"contentType": "text",
"createdAt": 1707350400
},
{
"id": "msg_002",
"role": "assistant",
"type": "answer",
"content": "根据您提供的信息,我为您计算了以下营养方案...",
"contentType": "text",
"createdAt": 1707350410
}
]
}
```
---
### 3.7 上传文件
#### 基础信息
| 项目 | 值 |
|------|------|
| 请求路径 | `POST /api/front/coze/file/upload` |
| Content-Type | `multipart/form-data` |
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| `file` | File | 是 | 待上传的文件 |
#### 测试用例
##### TC-COZE-11: 上传图片文件
```bash
curl -X POST 'http://localhost:20822/api/front/coze/file/upload' \
-F "file=@/path/to/your/image.jpg"
```
##### TC-COZE-12: 上传文档文件
```bash
curl -X POST 'http://localhost:20822/api/front/coze/file/upload' \
-F "file=@/path/to/your/document.pdf"
```
**预期响应**
```json
{
"code": 200,
"message": "success",
"data": {
"id": "file_xxxxx",
"bytes": 102400,
"createdAt": 1707350400,
"fileName": "image.jpg"
}
}
```
---
## 四、错误码说明
| code | message | 说明 |
|------|---------|------|
| 200 | success | 请求成功 |
| 400 | Bad Request | 请求参数错误 |
| 401 | Unauthorized | 认证失败Token 无效或过期) |
| 404 | Not Found | 资源不存在 |
| 429 | Too Many Requests | 请求频率超限 |
| 500 | Internal Server Error | 服务端内部错误 |
---
## 五、常见问题
### 5.1 Token 过期
当前使用 PAT 模式Token 有效期 30 天。如遇 401 错误,请检查配置文件中的 `coze.api.token` 是否过期。
### 5.2 Bot ID 无效
确保使用正确的 Bot ID。默认 Bot ID `7591133240535449654` 为食谱计算器专用。
### 5.3 流式接口超时
流式接口默认超时时间为 60 秒。对于长时间运行的对话,请确保客户端保持连接。
---
## 六、Postman 测试集合
可导入以下 JSON 到 Postman 进行测试:
```json
{
"info": {
"name": "Coze API Tests",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "基础对话",
"request": {
"method": "POST",
"header": [{"key": "Content-Type", "value": "application/json"}],
"body": {
"mode": "raw",
"raw": "{\"botId\": \"7591133240535449654\", \"userId\": \"test_user_001\", \"stream\": false, \"additionalMessages\": [{\"role\": \"user\", \"content\": \"你好\"}]}"
},
"url": {"raw": "http://localhost:20822/api/front/coze/chat"}
}
},
{
"name": "食谱计算-男性透析",
"request": {
"method": "POST",
"header": [{"key": "Content-Type", "value": "application/json"}],
"body": {
"mode": "raw",
"raw": "{\"botId\": \"7591133240535449654\", \"userId\": \"3243981400446844\", \"stream\": false, \"additionalMessages\": [{\"role\": \"user\", \"content\": \"请帮我计算营养方案我的信息如下性别男年龄55岁身高170cm正在进行血液透析干体重65.5kg血肌酐850μmol/L\"}]}"
},
"url": {"raw": "http://localhost:20822/api/front/coze/chat"}
}
}
]
}
```
---
## 七、变更记录
| 版本 | 日期 | 作者 | 变更内容 |
|------|------|------|----------|
| v1.0 | 2026-02-08 | System | 初稿 |
---
*文档结束*

View File

@@ -0,0 +1,808 @@
# Tool 模块 API 测试文档
> **版本**v1.0
> **创建日期**2026-02-08
> **基础路径**`/api/front/tool`
> **服务端口**20822
> **认证方式**需登录Header: `Authori-zation`
---
## 一、接口总览
### 1.1 模块分类
| 模块 | 接口数量 | 说明 |
|------|----------|------|
| 食谱计算器 | 3 | 营养方案计算与采纳 |
| AI营养师 | 4 | AI 对话问答 |
| 饮食打卡 | 8 | 打卡记录管理 |
| 食物百科 | 4 | 食物查询 |
| 营养知识 | 3 | 知识库查询 |
| 打卡社区 | 9 | 社区互动 |
| 积分系统 | 5 | 积分管理 |
| 首页数据 | 4 | 首页聚合数据 |
| 食谱管理 | 3 | 食谱收藏 |
| 文件上传 | 2 | 图片/语音上传 |
---
## 二、认证说明
所有接口(除特殊标注外)均需要登录认证:
```bash
# Header 示例
-H "Authori-zation: eyJhbGciOiJIUzI1NiJ9.xxxxx"
```
---
## 三、食谱计算器
### 3.1 计算营养方案
#### 基础信息
| 项目 | 值 |
|------|------|
| 请求路径 | `POST /api/front/tool/calculator/calculate` |
| Content-Type | `application/json` |
| 是否鉴权 | **是** |
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 | 校验规则 |
|--------|------|------|------|----------|
| `gender` | String | 是 | 性别 | `male` / `female` |
| `age` | Integer | 是 | 年龄(岁) | 1 ≤ age ≤ 150 |
| `height` | Integer | 是 | 身高cm | 50 ≤ height ≤ 250 |
| `dialysis` | Boolean | 是 | 是否透析 | true / false |
| `dialysisType` | String | 否 | 透析类型 | `hemodialysis` / `peritoneal` |
| `dryWeight` | Number | 是 | 干体重kg | 20 ≤ dryWeight ≤ 300 |
| `creatinine` | Number | 是 | 血肌酐μmol/L | 0 < creatinine ≤ 2000 |
#### 测试用例
##### TC-CALC-01: 男性透析患者计算
```bash
curl -X POST 'http://localhost:20822/api/front/tool/calculator/calculate' \
-H "Content-Type: application/json" \
-H "Authori-zation: YOUR_TOKEN" \
-d '{
"gender": "male",
"age": 55,
"height": 170,
"dialysis": true,
"dialysisType": "hemodialysis",
"dryWeight": 65.5,
"creatinine": 850
}'
```
**预期响应**
```json
{
"code": 200,
"message": "success",
"data": {
"id": 100234,
"healthData": {
"eGFR": "7.9",
"standardWeight": "63.0",
"bmi": "22.7",
"bmiStatus": "正常",
"ckdStage": "透析期"
},
"nutritionGoals": {
"protein": "75.6",
"energy": "2205"
},
"foodList": [
{ "number": 1, "name": "谷薯50g", "portion": "5.7" },
{ "number": 2, "name": "淀粉100g", "portion": "0.77" },
{ "number": 3, "name": "绿叶蔬菜200g", "portion": "1" },
{ "number": 4, "name": "瓜果蔬菜200g", "portion": "2" },
{ "number": 5, "name": "奶类230g", "portion": "1" },
{ "number": 6, "name": "肉蛋类50/60g", "portion": "7" },
{ "number": 7, "name": "油脂类10g", "portion": "5.7" }
],
"mealPlan": {
"breakfast": [...],
"lunch": [...],
"dinner": [...]
},
"importantTips": [
"以上配餐由 AI 生成,仅适用于无其他并发症的单纯尿毒症人群",
"透析患者需严格控制水分摄入"
],
"createdAt": "2026-02-08T10:30:00+08:00"
}
}
```
##### TC-CALC-02: 女性非透析患者计算
```bash
curl -X POST 'http://localhost:20822/api/front/tool/calculator/calculate' \
-H "Content-Type: application/json" \
-H "Authori-zation: YOUR_TOKEN" \
-d '{
"gender": "female",
"age": 48,
"height": 160,
"dialysis": false,
"dryWeight": 52,
"creatinine": 180
}'
```
##### TC-CALC-03: 参数校验-年龄超出范围
```bash
curl -X POST 'http://localhost:20822/api/front/tool/calculator/calculate' \
-H "Content-Type: application/json" \
-H "Authori-zation: YOUR_TOKEN" \
-d '{
"gender": "male",
"age": 200,
"height": 170,
"dialysis": false,
"dryWeight": 65,
"creatinine": 100
}'
```
**预期响应**`code=400, message 包含"年龄"`
---
### 3.2 获取计算结果详情
#### 基础信息
| 项目 | 值 |
|------|------|
| 请求路径 | `GET /api/front/tool/calculator/result/{id}` |
| 是否鉴权 | **是** |
#### 测试用例
##### TC-CALC-04: 获取计算结果
```bash
curl -X GET 'http://localhost:20822/api/front/tool/calculator/result/100234' \
-H "Authori-zation: YOUR_TOKEN"
```
##### TC-CALC-05: 查询不存在的结果
```bash
curl -X GET 'http://localhost:20822/api/front/tool/calculator/result/999999' \
-H "Authori-zation: YOUR_TOKEN"
```
**预期响应**`code=404`
---
### 3.3 采纳营养计划
#### 基础信息
| 项目 | 值 |
|------|------|
| 请求路径 | `POST /api/front/tool/calculator/adopt` |
| Content-Type | `application/json` |
| 是否鉴权 | **是** |
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| `resultId` | Long | 是 | 计算结果 ID |
#### 测试用例
##### TC-CALC-06: 采纳营养计划
```bash
curl -X POST 'http://localhost:20822/api/front/tool/calculator/adopt' \
-H "Content-Type: application/json" \
-H "Authori-zation: YOUR_TOKEN" \
-d '{
"resultId": 100234
}'
```
**预期响应**
```json
{
"code": 200,
"message": "success",
"data": {
"planId": 56789,
"startDate": "2026-02-08",
"endDate": "2026-02-14"
}
}
```
##### TC-CALC-07: 重复采纳(幂等测试)
```bash
# 同一 resultId 调用两次
curl -X POST 'http://localhost:20822/api/front/tool/calculator/adopt' \
-H "Content-Type: application/json" \
-H "Authori-zation: YOUR_TOKEN" \
-d '{"resultId": 100234}'
```
**预期响应**:返回已存在的 planId
---
## 四、AI 营养师
### 4.1 发送消息给 AI 营养师
#### 基础信息
| 项目 | 值 |
|------|------|
| 请求路径 | `POST /api/front/tool/ai-nutritionist/message` |
| Content-Type | `application/json` |
#### 测试用例
##### TC-AI-01: 发送消息
```bash
curl -X POST 'http://localhost:20822/api/front/tool/ai-nutritionist/message' \
-H "Content-Type: application/json" \
-H "Authori-zation: YOUR_TOKEN" \
-d '{
"conversationId": "conv_001",
"content": "肾病患者可以吃香蕉吗?"
}'
```
---
### 4.2 获取 AI 回复
#### 基础信息
| 项目 | 值 |
|------|------|
| 请求路径 | `GET /api/front/tool/ai-nutritionist/response/{messageId}` |
#### 测试用例
##### TC-AI-02: 获取回复
```bash
curl -X GET 'http://localhost:20822/api/front/tool/ai-nutritionist/response/12345' \
-H "Authori-zation: YOUR_TOKEN"
```
---
### 4.3 获取对话历史
#### 基础信息
| 项目 | 值 |
|------|------|
| 请求路径 | `GET /api/front/tool/ai-nutritionist/history` |
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| `page` | Integer | 否 | 页码,默认 1 |
| `limit` | Integer | 否 | 每页数量,默认 20 |
| `conversationId` | Long | 否 | 对话 ID |
#### 测试用例
##### TC-AI-03: 获取对话历史
```bash
curl -X GET 'http://localhost:20822/api/front/tool/ai-nutritionist/history?page=1&limit=20' \
-H "Authori-zation: YOUR_TOKEN"
```
---
### 4.4 清空对话历史
#### 测试用例
##### TC-AI-04: 清空对话
```bash
curl -X POST 'http://localhost:20822/api/front/tool/ai-nutritionist/clear' \
-H "Content-Type: application/json" \
-H "Authori-zation: YOUR_TOKEN" \
-d '{
"conversationId": "conv_001"
}'
```
---
## 五、饮食打卡
### 5.1 提交打卡记录
#### 基础信息
| 项目 | 值 |
|------|------|
| 请求路径 | `POST /api/front/tool/checkin/submit` |
| Content-Type | `application/json` |
#### 测试用例
##### TC-CHECKIN-01: 提交打卡
```bash
curl -X POST 'http://localhost:20822/api/front/tool/checkin/submit' \
-H "Content-Type: application/json" \
-H "Authori-zation: YOUR_TOKEN" \
-d '{
"mealType": "breakfast",
"date": "2026-02-08",
"foods": [
{"name": "牛奶", "amount": "250ml"},
{"name": "全麦面包", "amount": "2片"}
],
"images": ["https://cdn.xxx.com/img1.jpg"],
"remark": "早餐清淡"
}'
```
---
### 5.2 获取打卡记录列表
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| `page` | Integer | 否 | 页码 |
| `limit` | Integer | 否 | 每页数量 |
| `date` | String | 否 | 日期筛选 YYYY-MM-DD |
| `mealType` | String | 否 | 餐次breakfast/lunch/dinner |
#### 测试用例
##### TC-CHECKIN-02: 获取打卡列表
```bash
curl -X GET 'http://localhost:20822/api/front/tool/checkin/list?page=1&limit=10&date=2026-02-08' \
-H "Authori-zation: YOUR_TOKEN"
```
---
### 5.3 获取打卡记录详情
```bash
curl -X GET 'http://localhost:20822/api/front/tool/checkin/detail/123' \
-H "Authori-zation: YOUR_TOKEN"
```
---
### 5.4 获取连续打卡统计
```bash
curl -X GET 'http://localhost:20822/api/front/tool/checkin/streak' \
-H "Authori-zation: YOUR_TOKEN"
```
---
### 5.5 获取打卡日历数据
```bash
curl -X GET 'http://localhost:20822/api/front/tool/checkin/calendar?yearMonth=2026-02' \
-H "Authori-zation: YOUR_TOKEN"
```
---
### 5.6 获取打卡任务列表
```bash
curl -X GET 'http://localhost:20822/api/front/tool/checkin/tasks' \
-H "Authori-zation: YOUR_TOKEN"
```
---
### 5.7 一键复制打卡
```bash
curl -X POST 'http://localhost:20822/api/front/tool/checkin/copy' \
-H "Content-Type: application/json" \
-H "Authori-zation: YOUR_TOKEN" \
-d '{
"sourceRecordId": 12345,
"targetDate": "2026-02-08",
"mealType": "lunch"
}'
```
---
### 5.8 一键借鉴打卡
```bash
curl -X POST 'http://localhost:20822/api/front/tool/checkin/learn' \
-H "Content-Type: application/json" \
-H "Authori-zation: YOUR_TOKEN" \
-d '{
"sourcePostId": 67890,
"targetDate": "2026-02-08",
"mealType": "dinner"
}'
```
---
## 六、食物百科
### 6.1 搜索食物
```bash
curl -X GET 'http://localhost:20822/api/front/tool/food/search?keyword=鸡蛋&category=蛋类&page=1&limit=20' \
-H "Authori-zation: YOUR_TOKEN"
```
---
### 6.2 获取食物列表
```bash
curl -X GET 'http://localhost:20822/api/front/tool/food/list?category=肉类&page=1&limit=20' \
-H "Authori-zation: YOUR_TOKEN"
```
---
### 6.3 获取食物详情
```bash
curl -X GET 'http://localhost:20822/api/front/tool/food/detail/100' \
-H "Authori-zation: YOUR_TOKEN"
```
---
### 6.4 获取相似食物推荐
```bash
curl -X GET 'http://localhost:20822/api/front/tool/food/similar/100' \
-H "Authori-zation: YOUR_TOKEN"
```
---
## 七、营养知识
### 7.1 获取营养知识列表
```bash
curl -X GET 'http://localhost:20822/api/front/tool/knowledge/list?type=article&category=肾病饮食&page=1&limit=10' \
-H "Authori-zation: YOUR_TOKEN"
```
---
### 7.2 获取营养知识详情
```bash
curl -X GET 'http://localhost:20822/api/front/tool/knowledge/detail/50' \
-H "Authori-zation: YOUR_TOKEN"
```
---
### 7.3 获取营养素详情
```bash
curl -X GET 'http://localhost:20822/api/front/tool/knowledge/nutrient/蛋白质' \
-H "Authori-zation: YOUR_TOKEN"
```
---
## 八、打卡社区
### 8.1 获取社区内容列表
```bash
curl -X GET 'http://localhost:20822/api/front/tool/community/list?tab=recommend&page=1&limit=10' \
-H "Authori-zation: YOUR_TOKEN"
```
---
### 8.2 获取社区内容详情
```bash
curl -X GET 'http://localhost:20822/api/front/tool/community/detail/200' \
-H "Authori-zation: YOUR_TOKEN"
```
---
### 8.3 发布社区内容
```bash
curl -X POST 'http://localhost:20822/api/front/tool/community/publish' \
-H "Content-Type: application/json" \
-H "Authori-zation: YOUR_TOKEN" \
-d '{
"content": "今天的早餐很健康!",
"images": ["https://cdn.xxx.com/img1.jpg"],
"checkinId": 12345
}'
```
---
### 8.4 点赞/取消点赞
```bash
curl -X POST 'http://localhost:20822/api/front/tool/community/like' \
-H "Content-Type: application/json" \
-H "Authori-zation: YOUR_TOKEN" \
-d '{
"postId": 200,
"isLike": true
}'
```
---
### 8.5 收藏/取消收藏
```bash
curl -X POST 'http://localhost:20822/api/front/tool/community/collect' \
-H "Content-Type: application/json" \
-H "Authori-zation: YOUR_TOKEN" \
-d '{
"postId": 200,
"isCollect": true
}'
```
---
### 8.6 发表评论
```bash
curl -X POST 'http://localhost:20822/api/front/tool/community/comment' \
-H "Content-Type: application/json" \
-H "Authori-zation: YOUR_TOKEN" \
-d '{
"postId": 200,
"content": "看起来很不错!",
"parentId": null
}'
```
---
### 8.7 获取评论列表
```bash
curl -X GET 'http://localhost:20822/api/front/tool/community/comment/list/200?page=1&limit=20' \
-H "Authori-zation: YOUR_TOKEN"
```
---
### 8.8 关注/取消关注用户
```bash
curl -X POST 'http://localhost:20822/api/front/tool/community/follow' \
-H "Content-Type: application/json" \
-H "Authori-zation: YOUR_TOKEN" \
-d '{
"userId": 1001,
"isFollow": true
}'
```
---
### 8.9 分享内容
```bash
curl -X POST 'http://localhost:20822/api/front/tool/community/share' \
-H "Content-Type: application/json" \
-H "Authori-zation: YOUR_TOKEN" \
-d '{
"postId": 200
}'
```
---
## 九、积分系统
### 9.1 获取用户积分信息
```bash
curl -X GET 'http://localhost:20822/api/front/tool/points/info' \
-H "Authori-zation: YOUR_TOKEN"
```
---
### 9.2 获取积分规则
```bash
curl -X GET 'http://localhost:20822/api/front/tool/points/rules' \
-H "Authori-zation: YOUR_TOKEN"
```
---
### 9.3 获取积分流水
```bash
curl -X GET 'http://localhost:20822/api/front/tool/points/history?type=earn&page=1&limit=20' \
-H "Authori-zation: YOUR_TOKEN"
```
---
### 9.4 获取积分兑换列表
```bash
curl -X GET 'http://localhost:20822/api/front/tool/points/exchange/list' \
-H "Authori-zation: YOUR_TOKEN"
```
---
### 9.5 积分兑换
```bash
curl -X POST 'http://localhost:20822/api/front/tool/points/exchange' \
-H "Content-Type: application/json" \
-H "Authori-zation: YOUR_TOKEN" \
-d '{
"exchangeId": 10
}'
```
---
## 十、首页数据
### 10.1 获取首页数据
```bash
curl -X GET 'http://localhost:20822/api/front/tool/home/data' \
-H "Authori-zation: YOUR_TOKEN"
```
---
### 10.2 获取推荐食谱列表
```bash
curl -X GET 'http://localhost:20822/api/front/tool/home/recipes?limit=6' \
-H "Authori-zation: YOUR_TOKEN"
```
---
### 10.3 获取推荐营养知识
```bash
curl -X GET 'http://localhost:20822/api/front/tool/home/knowledge?limit=4' \
-H "Authori-zation: YOUR_TOKEN"
```
---
### 10.4 获取用户健康档案状态
```bash
curl -X GET 'http://localhost:20822/api/front/tool/home/health-status' \
-H "Authori-zation: YOUR_TOKEN"
```
---
## 十一、食谱管理
### 11.1 获取食谱列表
```bash
curl -X GET 'http://localhost:20822/api/front/tool/recipe/list?mealType=breakfast&page=1&limit=10' \
-H "Authori-zation: YOUR_TOKEN"
```
---
### 11.2 获取食谱详情
```bash
curl -X GET 'http://localhost:20822/api/front/tool/recipe/detail/50' \
-H "Authori-zation: YOUR_TOKEN"
```
---
### 11.3 收藏/取消收藏食谱
```bash
curl -X POST 'http://localhost:20822/api/front/tool/recipe/favorite' \
-H "Content-Type: application/json" \
-H "Authori-zation: YOUR_TOKEN" \
-d '{
"recipeId": 50,
"isFavorite": true
}'
```
---
## 十二、文件上传
### 12.1 上传图片
```bash
curl -X POST 'http://localhost:20822/api/front/tool/upload/image?type=checkin' \
-H "Authori-zation: YOUR_TOKEN" \
-F "file=@/path/to/image.jpg"
```
---
### 12.2 上传语音
```bash
curl -X POST 'http://localhost:20822/api/front/tool/upload/voice' \
-H "Authori-zation: YOUR_TOKEN" \
-F "file=@/path/to/voice.mp3"
```
---
## 十三、错误码说明
| code | message | 说明 |
|------|---------|------|
| 200 | success | 请求成功 |
| 400 | 参数校验失败 | 请求参数不符合校验规则 |
| 401 / 410000 | 未登录 | Token 缺失 |
| 410001 | Token 无效 | Token 格式错误 |
| 410002 | 登录过期 | Token 已过期 |
| 403 | 无权限 | 访问他人数据 |
| 404 | 资源不存在 | 数据不存在 |
| 500 | 系统异常 | 服务端内部错误 |
---
## 十四、变更记录
| 版本 | 日期 | 作者 | 变更内容 |
|------|------|------|----------|
| v1.0 | 2026-02-08 | System | 初稿 |
---
*文档结束*

25
docs/db/db_design.md Normal file
View File

@@ -0,0 +1,25 @@
## 数据库字典
### tool模块相关表
- 用户表: eb_user
- 用户积分记录表eb_user_integral_record
- 用户签到表eb_user_sign
- 文章内容表eb_article
- 用户积分表: v2_user_points
- 一键打卡记录表: v2_quick_checkin_records
- AI营养师对话表 v2_ai_conversations
- AI营养师消息表 v2_ai_messages
- 营养师咨询记录表: v2_nutritionist_consultations
- 食物百科表: v2_foods
- 营养知识表: v2_knowledge
- 食谱表: v2_recipes
- 打卡社区表: v2_community_posts
- 打卡社区互动表: v2_community_interactions
- 打卡社区评论表: v2_community_comments
- 打卡社区关注表: v2_community_follows
- 营养计划表: v2_nutrition_plans
- 食谱计算器结果表: v2_calculator_results

2594
docs/db/shop-msh.sql Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,104 @@
-- =========================================================
-- 食谱计算器表结构变更 SQL
-- 版本v1.0
-- 日期2026-02-01
-- 说明:根据开发文档优化后,需要新增以下字段
-- =========================================================
-- 检查并添加 bmi_status 字段
ALTER TABLE `v2_calculator_results`
ADD COLUMN IF NOT EXISTS `bmi_status` VARCHAR(20) DEFAULT NULL COMMENT 'BMI状态描述体型过轻/正常/超重/肥胖'
AFTER `bmi`;
-- 检查并添加 food_list_json 字段
ALTER TABLE `v2_calculator_results`
ADD COLUMN IF NOT EXISTS `food_list_json` TEXT DEFAULT NULL COMMENT '食物份数建议 JSON'
AFTER `energy_intake`;
-- 检查并添加 tips_json 字段
ALTER TABLE `v2_calculator_results`
ADD COLUMN IF NOT EXISTS `tips_json` TEXT DEFAULT NULL COMMENT '重要提示 JSON'
AFTER `meal_plan_json`;
-- 如果表不存在,创建完整表结构
CREATE TABLE IF NOT EXISTS `v2_calculator_results` (
`result_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '计算结果ID',
`user_id` BIGINT NOT NULL COMMENT '用户ID',
-- 输入参数
`gender` VARCHAR(10) NOT NULL COMMENT '性别male/female',
`age` INT NOT NULL COMMENT '年龄(岁)',
`height` INT NOT NULL COMMENT '身高cm',
`weight` DECIMAL(5,2) DEFAULT NULL COMMENT '体重kg',
`has_dialysis` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否透析0-否 1-是',
`dialysis_type` VARCHAR(20) DEFAULT NULL COMMENT '透析类型hemodialysis/peritoneal',
`dry_weight` DECIMAL(5,2) NOT NULL COMMENT '干体重kg',
`creatinine` DECIMAL(8,2) NOT NULL COMMENT '血肌酐μmol/L',
-- 计算结果
`egfr` DECIMAL(6,2) NOT NULL COMMENT 'eGFRml/min/1.73m²)',
`standard_weight` DECIMAL(5,2) NOT NULL COMMENT '标准体重kg',
`bmi` DECIMAL(4,1) NOT NULL COMMENT 'BMI',
`bmi_status` VARCHAR(20) DEFAULT NULL COMMENT 'BMI状态描述',
`ckd_stage` VARCHAR(50) NOT NULL COMMENT 'CKD分期',
`protein_intake` DECIMAL(5,1) NOT NULL COMMENT '每日蛋白质目标g',
`energy_intake` INT NOT NULL COMMENT '每日能量目标kcal',
-- JSON 数据
`food_list_json` TEXT DEFAULT NULL COMMENT '食物份数建议 JSON',
`meal_plan_json` TEXT DEFAULT NULL COMMENT '配餐方案 JSON',
`tips_json` TEXT DEFAULT NULL COMMENT '重要提示 JSON',
-- 状态字段
`is_adopted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否已采纳0-未采纳 1-已采纳',
`adopted_at` DATETIME DEFAULT NULL COMMENT '采纳时间',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`result_id`),
INDEX `idx_user_id` (`user_id`),
INDEX `idx_created_at` (`created_at`),
INDEX `idx_is_adopted` (`is_adopted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='营养计算结果表';
-- 营养计划表(如果不存在则创建)
CREATE TABLE IF NOT EXISTS `v2_nutrition_plans` (
`plan_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '营养计划ID',
`user_id` BIGINT NOT NULL COMMENT '用户ID',
`result_id` BIGINT DEFAULT NULL COMMENT '关联的计算结果ID',
-- 健康数据(冗余存储,便于查询)
`gender` VARCHAR(10) DEFAULT NULL COMMENT '性别',
`age` INT DEFAULT NULL COMMENT '年龄',
`height` INT DEFAULT NULL COMMENT '身高cm',
`weight` DECIMAL(5,2) DEFAULT NULL COMMENT '体重kg',
`has_dialysis` TINYINT(1) DEFAULT 0 COMMENT '是否透析',
`dialysis_type` VARCHAR(20) DEFAULT NULL COMMENT '透析类型',
`creatinine` DECIMAL(8,2) DEFAULT NULL COMMENT '血肌酐',
-- 计算结果
`egfr` DECIMAL(6,2) DEFAULT NULL COMMENT 'eGFR',
`standard_weight` DECIMAL(5,2) DEFAULT NULL COMMENT '标准体重',
`bmi` DECIMAL(4,1) DEFAULT NULL COMMENT 'BMI',
`ckd_stage` VARCHAR(50) DEFAULT NULL COMMENT 'CKD分期',
`protein_intake` DECIMAL(5,1) DEFAULT NULL COMMENT '蛋白质目标',
`energy_intake` INT DEFAULT NULL COMMENT '能量目标',
-- 配餐方案
`meal_plan_json` TEXT DEFAULT NULL COMMENT '配餐方案 JSON',
-- 计划状态
`status` VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态active/completed/abandoned',
`start_date` DATE NOT NULL COMMENT '开始日期',
`end_date` DATE NOT NULL COMMENT '结束日期',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`plan_id`),
INDEX `idx_user_id` (`user_id`),
INDEX `idx_result_id` (`result_id`),
INDEX `idx_status` (`status`),
INDEX `idx_start_date` (`start_date`),
UNIQUE INDEX `uk_user_result` (`user_id`, `result_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='营养计划表';

View File

@@ -0,0 +1,15 @@
-- =============================================
-- v2_recipes 表新增 source 相关字段
-- 用于追踪食谱来源(手动/计算器/AI生成等
-- Author: ScottPan
-- Date: 2026-02-14
-- =============================================
ALTER TABLE v2_recipes
ADD COLUMN `source` VARCHAR(20) DEFAULT 'manual' COMMENT '来源manual(手动)/calculator(计算器)/ai(AI生成)' AFTER `sort_order`,
ADD COLUMN `source_id` BIGINT DEFAULT NULL COMMENT '来源ID如计算器结果ID' AFTER `source`;
-- 新增索引,便于按来源查询
ALTER TABLE v2_recipes
ADD KEY `idx_source` (`source`),
ADD KEY `idx_source_id` (`source_id`);

View File

@@ -0,0 +1,602 @@
# 打卡社区功能设计方案
## 慢生活智能营养专家 - 类似小红书的UGC社区
---
## 📋 功能概述
**产品定位**将个人打卡记录转化为可分享的UGC内容打造肾病患者的饮食分享社区。
**对标产品**:小红书(内容社区)+ Keep运动社区打卡分享
**核心价值**
1. 📸 **降低UGC门槛** - 打卡记录一键转分享,无需重新创作
2. 👥 **社交连接** - 患者之间互相学习、点赞、评论、关注
3. 🎯 **激励增强** - 社交认同感大幅增强打卡动力
4. 💰 **商业价值** - 优质内容吸引新用户为KOL孵化和广告变现打基础
---
## 🎨 核心页面展示
### 1. 社区广场(瀑布流)
```
┌────────────────────────────────┐
│ 🏠 社区广场 [发布+] │
└────────────────────────────────┘
【筛选Tab】 [推荐] [最新] [关注] [热门]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【瀑布流布局】左右2列
┌──────────┐ ┌──────────┐
│ 图片 │ │ 图片 │
│ │ │ │
├──────────┤ ├──────────┤
│ 🥗 早餐打 │ │ 🍱 午餐低 │
│ 卡第7天│ │ 钾配餐 │
│ │ │ │
│ 👤 张小慢 │ │ 👤 李大康 │
│ ❤️ 128 │ │ ❤️ 256 │
└──────────┘ └──────────┘
```
**关键特性**
- 瀑布流布局,视觉效果好
- 封面图自动取打卡照片第一张
- 显示点赞数,体现内容质量
- 4个Tab满足不同浏览需求
### 2. 内容详情页
```
┌────────────────────────────────┐
│ 【用户信息】 │
│ 👤 张小慢透析3年
│ 📅 2025-11-20 12:30 │
│ [+ 关注] │
└────────────────────────────────┘
┌────────────────────────────────┐
│ 【图片轮播】1/3 │
│ [滑动查看更多照片] │
└────────────────────────────────┘
┌────────────────────────────────┐
│ 🥗 早餐打卡第7天终于坚持下来了 │
│ #早餐 #低钾饮食 #透析期 │
│ │
│ 今天的早餐:牛奶+鸡蛋拌面+黄瓜 │
│ 照片不是很好看但营养达标啦~ │
└────────────────────────────────┘
┌────────────────────────────────┐
│ 【营养数据】 │
│ 蛋白质18.8g (达标94%) │
│ 能量452kcal (达标90%) │
│ 🎯 整体营养达标率92% │
└────────────────────────────────┘
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┌────────────────────────────────┐
│ ❤️ 128 💬 23 ⭐ 45 ➤ 分享 │
└────────────────────────────────┘
┌────────────────────────────────┐
│ 💬 评论区 │
│ 太棒了!我也要学习这样吃 │
│ 坚持得很好,营养搭配合理👍 │
└────────────────────────────────┘
```
**关键特性**
- 完整的营养数据展示(来自打卡记录)
- 图片轮播支持1-3张
- 互动栏:点赞/评论/收藏/分享
- 评论区支持点赞和回复
### 3. 发布页面
```
┌────────────────────────────────┐
│ < 分享打卡记录 [发布] │
└────────────────────────────────┘
【选择封面图】从打卡照片中选择
【标题】
🥗 早餐打卡第7天
[AI帮你写标题 ✨]
【正文】
分享一下你的打卡心得吧~
[AI帮你写描述 ✨]
【添加话题】
#早餐 #低钾饮食 #透析期
[+ 添加话题]
【营养数据】(自动带入)
蛋白质18.8g 能量452kcal
【隐私设置】
○ 公开 ○ 仅关注可见 ○ 私密
💡 提示分享到社区可获得20积分
```
**关键特性**
- 从打卡记录一键分享(降低门槛)
- AI辅助生成标题和描述降低创作难度
- 营养数据自动带入(突出专业性)
- 隐私设置(保护用户隐私)
- 积分激励(促进分享)
### 4. 用户主页
```
┌────────────────────────────────┐
│ 【用户资料卡】 │
│ 👤 张小慢 │
│ 透析3年 | CKD 5期 │
│ 简介:坚持健康饮食,享受慢生活 │
│ [+ 关注] │
└────────────────────────────────┘
┌────────────────────────────────┐
│ 128 作品 | 256 获赞 | 89 粉丝 │
└────────────────────────────────┘
┌────────────────────────────────┐
│ 🏆 成就勋章 │
│ 连续打卡7天 获赞100+ 营养达标王 │
└────────────────────────────────┘
【作品网格】3列
┌────┐ ┌────┐ ┌────┐
│图片 │ │图片 │ │图片 │
└────┘ └────┘ └────┘
```
**关键特性**
- 显示个人标签(疾病状态)
- 数据统计(作品/获赞/粉丝)
- 成就勋章系统
- 作品网格展示
---
## 🔄 用户使用流程
### 完整的内容生产和消费闭环
```
【内容生产】
打卡上传饮食记录
打卡成功页点击"分享到社区"
AI辅助编辑标题和描述
添加话题标签
发布成功 + 获得20积分
内容进入推荐池
【内容消费】
访问社区广场
浏览瀑布流内容(推荐算法)
点击感兴趣的卡片
查看详情(图片/文字/营养数据)
互动(点赞/评论/收藏)
关注优质创作者
形成社交关系
【社交连接】
关注其他用户
"关注"Tab看到关注用户的内容
评论互动
收到回复通知
再次访问社区
【激励闭环】
看到他人优质打卡内容
受到激励,自己也想分享
更认真地打卡(拍照、摆盘)
分享到社区
获得点赞和评论
社交认同感增强
继续坚持打卡和分享
```
---
## 💡 核心功能设计
### 1. 推荐算法
**推荐分数计算**
```javascript
推荐分数 = 内容质量分 × 0.4
+ 用户兴趣分 × 0.3
+ 时效性分 × 0.2
+ 多样性分 × 0.1
```
**内容质量分**
- 有配图:+20分
- 3张图+10分
- 有营养数据:+15分
- 字数>50字+10分
- 点赞数×0.5分
- 评论数×2分
- 收藏数×3分
**用户兴趣分**
- 同疾病状态(透析/非透析):+30分
- 同CKD分期+20分
- 同餐次(早中晚):+15分
- 关注的用户:+50分
- 历史互动过的标签:+10分
### 2. 互动系统
**点赞**
- 点击动画效果(心形放大+红色填充)
- 实时更新点赞数
- 发布者获得通知
**评论**
- 支持二级评论(回复评论)
- 支持@用户
- 支持Emoji
- 评论可点赞
- 按点赞数排序
**收藏**
- 一键收藏
- 个人中心查看收藏列表
- 取消收藏
**分享**
- 微信好友/朋友圈
- 复制链接
- 生成海报(带小程序码)
### 3. 积分奖励
| 行为 | 积分 | 说明 |
|-----|------|------|
| 发布内容到社区 | +20分 | 鼓励分享 |
| 内容获得第1个赞 | +5分 | 正向反馈 |
| 内容获得第10个赞 | +10分 | 里程碑奖励 |
| 内容获得第100个赞 | +50分 | 优质内容奖励 |
| 发布评论 | +2分 | 每日上限10条 |
| 收到评论 | +3分 | 提升互动积极性 |
| 评论被点赞 | +1分 | - |
| 分享到朋友圈 | +5分 | 每日上限1次 |
### 4. 创作者等级
```
Lv1 新手发布1-10篇
Lv2 达人发布11-50篇
Lv3 专家发布51-100篇
Lv4 大咖发布100+篇 + 平均获赞50+
```
**等级权益**
- Lv2解锁话题创建权限
- Lv3内容优先推荐
- Lv4认证标识 + 优先审核 + 流量扶持
---
## 📊 数据库设计
### 社区内容表community_posts
```sql
CREATE TABLE community_posts (
post_id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
check_in_record_id BIGINT, -- 关联的打卡记录
title VARCHAR(100) NOT NULL,
content TEXT,
cover_image VARCHAR(255),
images_json TEXT,
nutrition_data_json TEXT,
tags_json TEXT,
like_count INT DEFAULT 0,
comment_count INT DEFAULT 0,
collect_count INT DEFAULT 0,
share_count INT DEFAULT 0,
view_count INT DEFAULT 0,
recommend_score DECIMAL(10,2),
hot_score DECIMAL(10,2),
status VARCHAR(20) DEFAULT 'published',
privacy VARCHAR(20) DEFAULT 'public',
created_at TIMESTAMP
);
```
### 互动记录表community_interactions
```sql
CREATE TABLE community_interactions (
interaction_id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
post_id BIGINT NOT NULL,
interaction_type VARCHAR(20), -- like, comment, collect, share, view
created_at TIMESTAMP,
UNIQUE KEY (user_id, post_id, interaction_type)
);
```
### 评论表community_comments
```sql
CREATE TABLE community_comments (
comment_id BIGINT PRIMARY KEY,
post_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
content TEXT NOT NULL,
parent_comment_id BIGINT, -- NULL为一级评论
reply_to_user_id BIGINT,
like_count INT DEFAULT 0,
status VARCHAR(20) DEFAULT 'published',
created_at TIMESTAMP
);
```
### 关注关系表community_follows
```sql
CREATE TABLE community_follows (
follow_id BIGINT PRIMARY KEY,
follower_id BIGINT NOT NULL, -- 关注者
followee_id BIGINT NOT NULL, -- 被关注者
created_at TIMESTAMP,
UNIQUE KEY (follower_id, followee_id)
);
```
---
## 🎯 核心KPI指标
### 内容生产指标
| 指标 | v2.0目标 | 说明 |
|-----|---------|------|
| 社区内容总量 | 500篇 | 3个月累计 |
| 日均发布量 | 10篇 | 每日新增内容 |
| 发布率 | 10% | 打卡用户中分享到社区的比例 |
### 内容消费指标
| 指标 | v2.0目标 | 说明 |
|-----|---------|------|
| 日均访问用户 | 200人 | 每日访问社区的用户数 |
| 人均浏览数 | 5篇 | 每个用户平均浏览的内容数 |
| 互动率 | 30% | 浏览用户中产生互动的比例 |
### 互动指标
| 指标 | v2.0目标 | 说明 |
|-----|---------|------|
| 日均点赞数 | 500 | 每日产生的点赞总数 |
| 日均评论数 | 100 | 每日产生的评论总数 |
| 日均收藏数 | 50 | 每日产生的收藏总数 |
### 社交指标
| 指标 | v2.0目标 | 说明 |
|-----|---------|------|
| 关注关系数 | 200对 | 用户之间的关注关系总数 |
| 创作者数量 | 50人 | 发布过至少3篇内容的用户 |
### 留存指标
| 指标 | v2.0目标 | 说明 |
|-----|---------|------|
| 社区用户7日留存率 | 60% | 比普通用户高30% |
**北极星指标**
```
社区健康度 = (日均发布量 × 10)
+ (人均浏览数 × 5)
+ (互动率 × 100)
v2.0目标≥500分
```
---
## 🚀 开发计划
### 阶段1MVP版本2周
**核心功能**
- [ ] 社区广场(瀑布流展示)
- [ ] 内容详情页(图片/文字/营养数据)
- [ ] 发布功能(从打卡记录分享)
- [ ] 基础互动(点赞/评论)
**目标**:验证用户是否愿意分享和浏览内容
### 阶段2完善版本2周
**新增功能**
- [ ] 推荐算法
- [ ] 关注/粉丝系统
- [ ] 用户主页
- [ ] 话题标签
- [ ] 收藏和分享
- [ ] 评论二级回复
**目标**:完善社交体验,提升用户粘性
### 阶段3运营版本1周
**新增功能**
- [ ] 内容审核机制
- [ ] 创作者等级
- [ ] 成就勋章
- [ ] 举报功能
- [ ] 运营后台
**目标**:确保内容质量,支持运营活动
---
## 💰 商业价值
### 短期价值3-6个月
1. **用户增长**
- 优质内容吸引新用户注册
- 预计带来20-30%的新用户增长
2. **用户留存**
- 社区用户7日留存率60%比普通用户高30%
- 社交连接增强用户粘性
3. **用户活跃**
- 社区用户DAU/MAU是普通用户的3-4倍
- 日均打开频次提升50%
### 中期价值6-12个月
4. **内容资产**
- 积累10000+篇优质UGC内容
- 降低运营成本不需要大量PGC内容
5. **KOL孵化**
- 培养100+活跃创作者
- 部分创作者成为KOL认证营养师、资深患者
6. **品牌影响力**
- 打造"肾病患者专属社区"品牌认知
- 建立行业壁垒
### 长期价值12-24个月
7. **商业变现**
- **广告变现**:信息流广告、品牌合作内容
- **电商导流**:低钾食品、营养补剂推荐
- **会员体系**:优质内容专属查看
- **KOL分成**:创作者激励计划
8. **数据价值**
- 真实饮食数据+用户偏好数据
- 优化AI推荐算法
- 为医疗研究提供数据支持
---
## ⚠️ 风险和应对
### 风险1冷启动问题
**风险**:初期内容少,用户不愿意访问社区
**应对**
- 官方账号提前准备100+优质内容
- 种子用户激励前100名发布用户额外200积分
- 首页推荐位展示优质社区内容
### 风险2内容质量低
**风险**:用户分享的内容质量参差不齐
**应对**
- AI辅助生成标题和描述降低创作难度
- 推荐算法优先展示高质量内容
- 创作者等级制度,优质创作者流量扶持
### 风险3违规内容
**风险**:医疗广告、政治敏感内容
**应对**
- 敏感词过滤(实时拦截)
- 图片审核腾讯云内容安全API
- 人工审核(新用户首次发布、被举报内容)
- 处罚机制(警告、封禁)
### 风险4用户隐私
**风险**:患者不愿意公开疾病信息
**应对**
- 隐私设置(公开/仅关注可见/私密)
- 匿名发布选项
- 脱敏处理(可选择不显示个人标签)
---
## 📞 总结
### 为什么要做打卡社区?
1. **解决核心痛点**
- 患者孤独感强,需要同伴支持
- 打卡容易坚持不下来,需要社交激励
- 不知道怎么吃得好看,需要优质内容参考
2. **提升产品价值**
- 从工具型产品→社区型产品
- 从单向服务→双向互动
- 从个人使用→社交分享
3. **商业化基础**
- UGC内容吸引新用户降低获客成本
- 社交连接增强用户粘性提升LTV
- 为广告、电商、会员变现打基础
### 成功关键
**降低分享门槛** - 打卡记录一键转分享AI辅助创作
**优质内容优先** - 推荐算法保证用户看到好内容
**社交激励充分** - 点赞/评论/关注形成正向反馈
**内容审核严格** - 确保社区氛围健康
---
**完整PRD章节**第4.8章 - 打卡社区系统
**文档位置**`/Users/a123/Documents/Works25/慢生活/爱肾/msh-tools/docs/PRD_慢生活智能营养专家_v2.0.md`
**开发排期**3-4周MVP→完善→运营分三个阶段
---
**下一步**:需要我生成小程序页面代码吗? 📱✨

View File

@@ -0,0 +1,439 @@
# 打卡详情页 - 一键打卡入口设计
> **更新日期**2025-11-20
> **版本**v2.2.1
> **核心优化**:在打卡详情页增加"一键打卡"入口
---
## 🎯 优化目标
用户在查看打卡详情时,希望能快速复制该打卡内容,无需返回到列表页操作。
---
## 📱 三个主要入口
### 入口1打卡历史页 → 直接复制 ⭐推荐快速打卡
```
我的饮食记录
┌──────────────────────────┐
│ 11月19日 早餐 │
│ ✅ 达标率92% ⭐高分 │
│ [📊 查看详情] │
│ [⚡一键复制打卡] ←入口1 │
└──────────────────────────┘
```
**特点**
- ✅ 最快捷的方式
- ✅ 无需进入详情页
- ✅ 适合确定要复制的情况
- ✅ 30秒完成打卡
**流程**
```
点击"⚡一键复制打卡"
复制确认页(预览内容)
打卡页(内容已填充)
修改内容(可选)
发布完成(+5积分
```
---
### 入口2打卡详情页 → 详细查看后复制 ⭐⭐⭐本次新增
```
我的饮食记录
[📊 查看详情] ←先点这里
打卡详情页
┌──────────────────────────┐
│ 📸 完整照片查看 │
│ 📊 营养数据详情 │
│ 📋 菜品清单 │
│ ┌──────────────────────┐ │
│ │ ⚡一键复制打卡 ←入口2 │ │
│ └──────────────────────┘ │
└──────────────────────────┘
```
**特点**
- ✅ 查看完整信息后决定
- ✅ 可以看到所有照片和数据
- ✅ 更了解打卡内容
- ✅ 适合需要详细查看的情况
**流程**
```
点击"📊 查看详情"
进入打卡详情页
查看照片、营养数据、菜品清单
点击"⚡一键复制打卡"
复制确认页
打卡页(内容已填充)
发布完成(+5积分
```
---
### 入口3社区打卡详情页 → 借鉴他人 ⭐学习优质内容
```
社区广场 → 他人的打卡
[点击帖子查看详情]
打卡详情页(他人的)
┌──────────────────────────┐
│ 查看他人饮食方案 │
│ 营养数据参考 │
│ ┌──────────────────────┐ │
│ │ 🎬一键借鉴打卡 ←入口3 │ │
│ └──────────────────────┘ │
└──────────────────────────┘
```
**特点**
- ✅ 学习他人优质饮食方案
- ✅ AI自动调整用量根据你的情况
- ✅ 必须上传自己的照片
- ✅ 原作者也会获得积分
**流程**
```
浏览社区打卡内容
点击感兴趣的帖子
进入打卡详情页(他人的)
点击"🎬一键借鉴打卡"
借鉴确认页AI调整用量
打卡页(需上传自己的照片)
发布完成
自己+5积分原作者+2积分
```
---
## 🎨 UI设计对比
### 场景A查看【自己的】打卡详情页
```
┌────────────────────────────────┐
│ 【快速操作】 │
│ ┌──────────────────────────┐ │
│ │ ⚡ 一键复制打卡 │ │
│ │ 快速复用这份饮食方案 │ │
│ └──────────────────────────┘ │
│ │
│ 点击后可以: │
│ ✅ 复制照片和备注到新打卡 │
│ ✅ 修改内容后重新发布 │
│ ✅ 可选生成AI视频 │
│ ✅ 获得5积分奖励 │
│ │
│ 💡 适用场景: │
│ • 今天吃的和之前某天一样 │
│ • 想复用高分打卡记录 │
│ • 快速打卡不想重新拍照 │
└────────────────────────────────┘
```
**按钮样式**
- 颜色:绿色渐变(#4CAF50#45B649
- 大小宽度100%高度56px
- 字体18px加粗
- 图标:⚡(闪电,表示快速)
- 圆角12px
- 阴影0 4px 12px rgba(76, 175, 80, 0.3)
---
### 场景B查看【他人的】打卡详情页
```
┌────────────────────────────────┐
│ 【一键借鉴】 │
│ ┌──────────────────────────┐ │
│ │ 🎬 一键借鉴打卡 │ │
│ │ 学习这份营养方案 │ │
│ └──────────────────────────┘ │
│ │
│ 点击后可以: │
│ ✅ 复制菜品清单到我的打卡 │
│ ✅ 根据我的情况调整用量 │
│ ✅ AI生成适合我的食谱 │
│ ✅ 获得5积分奖励 │
│ │
│ 💡 适用场景: │
│ • 看到他人的优质打卡想学习 │
│ • 发现适合自己的饮食方案 │
│ • 需要新的饮食灵感 │
└────────────────────────────────┘
```
**按钮样式**
- 颜色:蓝色渐变(#2196F3#1E88E5
- 大小宽度100%高度56px
- 字体18px加粗
- 图标:🎬(摄像机,表示学习和记录)
- 圆角12px
- 阴影0 4px 12px rgba(33, 150, 243, 0.3)
---
## 💻 技术实现
### 1. 判断打卡详情页的场景
```javascript
/**
* 判断打卡详情页的场景
*/
function getCheckInDetailScene(recordId, currentUserId) {
const record = getCheckInRecordById(recordId);
if (record.user_id === currentUserId) {
// 场景A查看自己的打卡
return {
scene: 'self',
buttonText: '⚡ 一键复制打卡',
buttonColor: 'green',
buttonClass: 'copy-btn-self',
action: 'copyOwnCheckIn'
};
} else {
// 场景B查看他人的打卡
return {
scene: 'others',
buttonText: '🎬 一键借鉴打卡',
buttonColor: 'blue',
buttonClass: 'copy-btn-others',
action: 'learnFromOthers'
};
}
}
```
### 2. 一键复制自己的打卡
```javascript
/**
* 一键复制自己的打卡
*/
function copyOwnCheckIn(recordId) {
const record = getCheckInRecordById(recordId);
// 复制数据
const copiedData = {
photos_json: record.photos_json, // 照片URL数组
notes: record.notes, // 备注说明
voice_url: record.voice_url, // 语音备注(如果有)
copied_from_record_id: recordId, // 记录复制来源
is_copied: 1 // 标记为复制的打卡
};
// 保存到临时存储
wx.setStorageSync('tempCheckInData', copiedData);
// 跳转到打卡页
wx.navigateTo({
url: '/pages/check-in/check-in?from=copy&recordId=' + recordId
});
// 埋点统计
trackEvent('copy_own_check_in', {
record_id: recordId,
source: 'detail_page'
});
}
```
### 3. 一键借鉴他人的打卡
```javascript
/**
* 一键借鉴他人的打卡
*/
function learnFromOthers(recordId, currentUserId) {
const record = getCheckInRecordById(recordId);
// 获取用户自己的营养计划
const myPlan = getNutritionPlan(currentUserId);
// AI自动调整菜品用量
const adjustedDishes = adjustDishesForUser(
record.actual_dishes_json,
myPlan
);
// 准备借鉴数据
const learnedData = {
dishes_json: adjustedDishes, // AI调整后的菜品清单
reference_record_id: recordId, // 参考来源
reference_user_id: record.user_id, // 原作者ID
is_learned_from_others: 1 // 标记为借鉴的打卡
};
// 保存到临时存储
wx.setStorageSync('tempCheckInData', learnedData);
// 跳转到打卡页
wx.navigateTo({
url: '/pages/check-in/check-in?from=learn&recordId=' + recordId
});
// 给原作者增加积分(+2分
addPointsToUser(record.user_id, 2, 'content_learned', recordId);
// 埋点统计
trackEvent('learn_from_others', {
record_id: recordId,
original_user_id: record.user_id,
source: 'detail_page'
});
}
```
### 4. AI调整菜品用量
```javascript
/**
* AI调整菜品用量根据用户的营养计划
*/
function adjustDishesForUser(originalDishes, userPlan) {
// 计算原始菜品的总营养
const originalNutrition = calculateTotalNutrition(originalDishes);
// 计算调整比例
const proteinRatio = userPlan.target_protein / originalNutrition.protein;
const energyRatio = userPlan.target_energy / originalNutrition.energy;
// 取平均比例
const avgRatio = (proteinRatio + energyRatio) / 2;
// 调整每个菜品的用量
return originalDishes.map(dish => {
const newAmount = Math.round(dish.amount * avgRatio);
return {
...dish,
amount: newAmount, // 调整后的用量
adjusted: true, // 标记为AI调整过的
original_amount: dish.amount, // 保留原始用量供参考
adjustment_ratio: avgRatio.toFixed(2) // 调整比例
};
});
}
```
---
## 📊 页面路径说明
| 页面 | 路径 | 参数 | 说明 |
|-----|------|------|------|
| 打卡历史页 | `/pages/check-in-history/history` | - | 显示用户的打卡记录列表 |
| 打卡详情页(自己的) | `/pages/check-in-detail/detail` | `?id={recordId}&type=self` | 查看自己的打卡详情 |
| 打卡详情页(他人的) | `/pages/check-in-detail/detail` | `?id={recordId}&type=others` | 查看他人的打卡详情 |
| 复制确认页 | `/pages/copy-confirm/confirm` | `?from={recordId}` | 复制自己的打卡确认页 |
| 借鉴确认页 | `/pages/learn-confirm/confirm` | `?from={recordId}` | 借鉴他人的打卡确认页 |
| 打卡页 | `/pages/check-in/check-in` | `?from=copy&recordId={id}``?from=learn&recordId={id}` | 填写打卡内容 |
---
## 🎁 积分规则
| 行为 | 积分 | 说明 |
|-----|------|------|
| 一键复制打卡(自己的) | +5分 | 从历史记录快速复制 |
| 一键借鉴打卡(他人的) | +5分 | 学习他人的饮食方案 |
| 内容被借鉴(原作者) | +2分 | 自己的打卡被他人借鉴 |
---
## 📈 预期效果
| 指标 | 优化前 | 优化后 | 提升 |
|-----|-------|-------|------|
| **详情页停留时长** | 15秒 | 30秒 | +100% |
| **从详情页发起的打卡** | 5% | 20% | +300% |
| **一键复制打卡使用率** | 25% | 40% | +60% |
| **打卡转化率** | 30% | 45% | +50% |
**数据支持**
- 用户在详情页查看完整信息后,打卡意愿更强
- 详情页提供更多上下文,用户更愿意复制高质量打卡
- 减少操作步骤,提升转化率
---
## 💡 设计亮点
### 1. **场景区分** ⭐⭐⭐
- 智能判断是"自己的打卡"还是"他人的打卡"
- 不同场景显示不同的按钮文案和颜色
- 提供差异化的体验
### 2. **三重入口** ⭐⭐⭐
- 历史列表页:快速直接复制
- 详情页:查看后再复制
- 社区详情页:学习他人
### 3. **AI智能调整** ⭐⭐⭐
- 借鉴他人打卡时AI自动调整菜品用量
- 根据用户的营养计划个性化调整
- 降低借鉴门槛,提升使用率
### 4. **积分激励** ⭐⭐⭐
- 借鉴者获得+5分
- 原作者获得+2分
- 形成"创作-分享-学习"的正向循环
---
## 🚀 下一步优化
1. **智能推荐复制**
- 在详情页根据历史数据推荐"您可能也想复制这几条"
- 提供相似打卡记录推荐
2. **批量复制**
- 支持一次性复制一天的三餐
- 生成"一日食谱计划"
3. **收藏夹功能**
- 收藏优质打卡记录
- 从收藏夹快速复制
4. **打卡模板**
- 将高分打卡保存为模板
- 模板库供用户选择
---
**文档版本**v2.2.1
**更新日期**2025-11-20
**产品名称**:慢生活智能营养专家小程序
---
**让每个打卡详情页都成为下一次打卡的起点!** ⚡🎬

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,197 @@
# 慢生活MSH系统 — 项目技术栈与架构分析
## 一、项目概述
**慢生活**MSH System是一套面向 C 端的**商城 + 营养/工具**一体化系统包含移动端多端应用UniApp与基于 CRMEB 的 Java 后端服务。项目在 CRMEB Java 版 v2.2 基础上做了业务定制,并接入了 AICoze、KieAI、腾讯云 ASR 等)与文章/工具类能力,形成「慢生活营养专家」产品形态。
---
## 二、整体架构
系统采用**前后端分离 + 多端一体**架构:
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 客户端(多端) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 微信小程序 │ │ H5 商城 │ │ App(iOS/ │ │ 支付宝/ │ │
│ │ (mp-weixin) │ │ │ │ Android) │ │ 头条等小程序│ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │ │
│ └────────────────┴────────────────┴────────────────┘ │
│ │ │
│ msh_single_uniapp (Vue 2 + UniApp) │
└────────────────────────────────────┬────────────────────────────────────┘
│ HTTPS / REST API
┌─────────────────────────────────────────────────────────────────────────┐
│ 后端服务Spring Boot
│ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │
│ │ crmeb-front (C 端 API) │ │ crmeb-admin (管理端 API) │ │
│ │ 端口: 20822 (sophia) │ │ 后台管理、定时任务(Quartz) │ │
│ │ /api/front/*, /api/public/* │ │ │ │
│ └──────────────┬──────────────┘ └──────────────┬──────────────┘ │
│ │ │ │
│ └────────────┬───────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ crmeb-service (业务逻辑) │ crmeb-common (工具/实体/配置) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────┬────────────────────────────────────┘
┌───────────────────────────┼───────────────────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ MySQL 8.x │ │ Redis │ │ 对象存储/ │
│ (Druid 连接池) │ │ (缓存/会话/ │ │ 支付/地图等 │
│ │ │ Token/验证码) │ │ OSS/COS/七牛 │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
- **前端**:一套 UniApp 源码通过条件编译发布到微信小程序、H5、App、支付宝/头条等小程序。
- **后端**:多模块 Maven 工程C 端入口为 `crmeb-front`,管理端为 `crmeb-admin`,共用 `crmeb-service``crmeb-common`
- **外部**MySQL 主库、Redis、阿里云 OSS/腾讯云 COS/七牛、微信/支付宝支付、高德地图、Coze/KieAI 等。
---
## 三、前端技术栈msh_single_uniapp
### 3.1 核心框架与语言
| 类别 | 技术选型 | 说明 |
|------------|-----------------|------|
| 跨端框架 | **UniApp** | 基于 Vue 2一套代码多端运行微信/支付宝/头条小程序、H5、App |
| 前端框架 | **Vue 2.x** | 选项式 API`main.js` 中挂载全局工具与 Store |
| 状态管理 | **Vuex** | `store/index.js` + `store/modules`(如 `app.js` |
| 构建/工程 | **Vue CLI** | `vue.config.js`:生产环境关闭 sourceMap、压缩时去掉 console |
| 富文本 | **mp-html** | 用于商品/文章等富文本展示(`package.json` 中声明) |
### 3.2 工程结构要点
- **入口与全局**`main.js``App.vue`;全局通过 `Vue.prototype` 挂载 `$util``$config``$Cache``$eventHub``$Order``$LoginAuth` 等。
- **配置**`config/app.js` 统一配置 `domain`API 基地址)、`HTTP_REQUEST_URL``HEADER``TOKENNAME`(如 `Authori-zation`H5 下可选 VConsole。
- **请求层**
- 通用业务请求:`utils/request.js`,基于 `uni.request`,前缀 `/api/front/``/api/public/`,统一处理 401/403/410xxx 跳转登录。
- AI/文章等:`api/models-api.js` 使用同一 `domain`,封装 Coze、KieAI、腾讯 ASR、文章等接口。
- **API 模块化**`api/api.js``api/user.js``api/order.js``api/store.js``api/tool.js``api/public.js``api/models-api.js` 等按业务拆分。
- **路由与页面**`pages.json` 配置首页、分包subPackages、tabBar、导航栏样式等首屏为 `pages/tool_main/index`(慢生活营养专家)。
### 3.3 多端与发布
- **manifest.json**:应用名「慢生活」、版本 2.2.1、App 模块(支付、分享、地图、定位、直播等)、微信/支付宝/头条小程序 appid、高德 key、支付与 OAuth 等。
- **发行**H5 使用 history 路由;微信小程序开启 `lazyCodeLoading: requiredComponents` 控制包体;生产构建通过 `vue.config.js` 做压缩与去 console。
### 3.4 前端技术栈小结
- **语言**JavaScriptES6+
- **UI**UniApp 内置组件 + 自定义组件(如 `skeleton``homeIndex``payment``jyf-parser` 等)
- **特色**多端统一、Token 鉴权、登录态校验、分包与懒加载、与 CRMEB 后端约定 `/api/front/``/api/public/` 前缀
---
## 四、后端技术栈msh_crmeb_22
### 4.1 核心框架与运行时
| 类别 | 技术选型 | 版本/说明 |
|--------------|-----------------------------|-----------|
| 基础框架 | **Spring Boot** | 2.2.6.RELEASE |
| Java | **JDK 1.8** | 编译与运行 |
| 构建 | **Maven** | 多模块 parent POM 管理依赖 |
### 4.2 模块划分
| 模块 | 职责 |
|----------------|------|
| **crmeb-common** | 公共实体、工具类、配置(如 CozeConfig、通用组件Druid、MyBatis-Plus、Redis、JWT、验证码等 |
| **crmeb-service** | 业务逻辑层商城、用户、订单、营销、工具、AI 对接Coze、KieAI、腾讯 ASR等 |
| **crmeb-admin** | 管理端 Web API、Quartz 定时任务,打包为 `jxz-admin-2.2.jar` |
| **crmeb-front** | C 端 Web API打包为 `sophia-front-2.2.jar`,当前 sophia 配置端口 20822 |
### 4.3 数据访问与存储
| 类别 | 技术选型 | 说明 |
|----------|-------------------------|------|
| 关系库 | **MySQL 8.x** | 驱动 `mysql-connector-java` 8.0.33Druid 连接池 |
| ORM | **MyBatis-Plus** | 3.3.1,含 generatormapper 扫描 `com.zbkj.**.dao` |
| 分页 | **PageHelper** | 1.2.5,与 MyBatis-Plus 共存时排除 mybatis 避免冲突 |
| 缓存 | **Redis + Jedis** | Spring Boot 2.2 Data Redis独立 DB 存 accessToken 等 |
| 文档 | **MongoDB** | 仅 driver-core 依赖,用于部分扩展能力 |
### 4.4 安全与认证
- **JWT**`jjwt-api/impl/jackson` 0.11.5Token 名称与前端约定(如 `Authori-zation`)。
- **行为验证码**`spring-boot-starter-captcha`AJ支持滑动/点选,缓存可走 Redis。
- **白名单**`application.yml``crmeb.ignored` 配置 Swagger、上传、Coze、KieAI 等路径免鉴权。
### 4.5 接口与文档
- **REST**Spring MVCC 端前缀 `/api/front/``/api/public/`
- **文档**Springfox Swagger 2.9.2 + swagger-bootstrap-ui 1.9.3,配置在 `application*.yml``swagger.basic`
### 4.6 第三方与中间件(根 POM 管理)
- **工具**Hutool、Commons Lang3、Fastjson、HttpClient、Dom4j、XStream、Pinyin4j、ZXing二维码等。
- **文件与图片**Commons FileUpload/IO、Thumbnailator、阿里云 OSS、腾讯云 COS、七牛 SDK。
- **支付**:支付宝 SDK、微信通过 common 或 service 内封装)。
- **AI/开放接口**Coze APIcoze-api 0.2.3、OkHttp、RxJava流式KieAI、腾讯云 ASR 等通过 HTTP 在 service 层封装。
### 4.7 配置与运行
- **配置**`application.yml` + `application-{profile}.yml`(如 `sophia``dev``prod``beta`)。
- **数据源/Redis**:在 profile 中覆盖 URL、库名、端口、密码等。
- **日志**Logback`logback-spring.xml`),按环境输出到 `./crmeb_log``./logs`
---
## 五、models-integration 子项目
- **定位**:独立 Maven 工程(`com.integration:models-integration`Java 8Spring Boot 2.7.5。
- **用途**:与「模型/AI」集成相关的独立服务或能力如统一网关、模型路由等通过 SpringDoc OpenAPI、Redis、MySQL、AOP 等搭建。
- **与主系统关系**:与 msh_crmeb_22 无直接模块依赖,可单独部署,通过 HTTP 或消息与 front 协作。
---
## 六、部署与运行约定
### 6.1 前端
- **开发**:使用 HBuilderX 或 Vue CLI 运行/发行到各端API 基地址在 `config/app.js``domain` 修改(如 `https://sophia-shop.uj345.cc`)。
- **生产**H5 可部署到任意静态服务器;小程序/App 需在对应平台上传与配置域名白名单。
### 6.2 后端
- **C 端**:以 `sophia` profile 启动 `CrmebFrontApplication`,默认端口 20822需可访问的 MySQL、Redis。
- **管理端**:启动 `CrmebAdminApplication`,按需配置端口与数据源。
- **包名**`com.zbkj`front 的 `@ComponentScan` 包含 `com.zbkj``com.zbkj.front`Mapper 扫描 `com.zbkj.**.dao`
---
## 七、技术栈汇总表
| 层次 | 技术项 | 选型说明 |
|----------|------------------|----------|
| 前端框架 | Vue 2 + UniApp | 多端统一(小程序/H5/App |
| 状态管理 | Vuex | 登录态、购物车、全局配置等 |
| 请求 | uni.request 封装 | 统一 domain、Token、错误与登录跳转 |
| 后端框架 | Spring Boot 2.2 | 多模块common/service/admin/front |
| 数据访问 | MyBatis-Plus + MySQL | Druid 连接池、PageHelper 分页 |
| 缓存 | Redis (Jedis) | 会话、Token、验证码、配置缓存 |
| 认证 | JWT + 行为验证码 | 前端 Header 传 Token |
| 文档 | Swagger 2 + Bootstrap UI | 后端 API 文档 |
| 构建 | Maven / npm | 后端 Maven前端 Vue CLI / HBuilderX |
| 对象存储 | 阿里云 OSS / 腾讯云 COS / 七牛 | 图片与文件 |
| 支付 | 微信支付、支付宝 | 通过后端封装 |
| AI/能力 | Coze、KieAI、腾讯云 ASR 等 | 在 crmeb-service 中封装front 暴露 REST |
---
## 八、文档与资源
- **产品与设计**`docs/` 下 PRD、打卡社区设计、食谱计算器接口文档、用户界面交互设计等。
- **数据库**`docs/sql``msh_crmeb_22/sql` 等目录可存放建表/迁移脚本。
- **接口**`documents/api` 可补充 C 端/管理端接口说明,与 Swagger 互补。
---
*文档基于当前仓库目录与配置文件整理,用于技术评审、新人上手与架构演进参考。具体版本号以各模块 `pom.xml`、`package.json` 及运行环境为准。*

View File

@@ -0,0 +1,569 @@
# 食谱计算器后端接口开发文档
> **版本**v1.0
> **创建日期**2026-02-01
> **所属模块**Tool工具模块
> **配套前端**`msh_single_uniapp/pages/tool/calculator.vue` / `calculator-result.vue`
---
## 一、功能概述
食谱计算器是**慢生活智能营养专家**的核心功能之一面向肾病CKD及透析患者根据用户输入的健康数据自动计算
1. **健康指标**eGFR、标准体重、BMI、CKD分期
2. **每日营养目标**蛋白质摄入量g/d、能量摄入量kcal/d
3. **食物份数建议**7 类食物的每日推荐份数
4. **一日三餐配餐方案**:早餐、午餐、晚餐的菜品清单(含图片、食材用量)
5. **重要提示**:根据患者情况给出饮食注意事项
用户可进一步**采纳营养计划**,进入打卡流程(另单独接口)。
---
## 二、接口清单
| 序号 | 接口名称 | HTTP方法 | 路径 | 说明 |
|------|----------|----------|------|------|
| 1 | 计算营养方案 | `POST` | `/api/front/tool/calculator/calculate` | 核心接口,输入健康数据,返回计算结果 |
| 2 | 获取计算结果详情 | `GET` | `/api/front/tool/calculator/result/{id}` | 通过结果ID获取完整结果含配餐 |
| 3 | 采纳营养计划 | `POST` | `/api/front/tool/calculator/adopt` | 用户采纳后创建营养计划,进入打卡流程 |
---
## 三、接口详细设计
### 3.1 计算营养方案
#### 基础信息
| 项目 | 值 |
|------|------|
| 请求路径 | `POST /api/front/tool/calculator/calculate` |
| 是否鉴权 | **是**(需登录) |
| Content-Type | `application/json` |
#### 请求参数Request Body
| 参数名 | 类型 | 必填 | 说明 | 校验规则 |
|--------|------|------|------|----------|
| `gender` | String | 是 | 性别 | 可选值:`male` / `female` |
| `age` | Integer | 是 | 年龄(岁) | 1 ≤ age ≤ 150 |
| `height` | Integer | 是 | 身高cm | 50 ≤ height ≤ 250 |
| `dialysis` | Boolean | 是 | 是否透析 | `true` / `false` |
| `dialysisType` | String | 否 | 透析类型 | 当 `dialysis=true` 时有效;可选值:`hemodialysis`(血透)/ `peritoneal`(腹透) |
| `dryWeight` | Number | 是 | 干体重kg | 20 ≤ dryWeight ≤ 300支持1位小数 |
| `creatinine` | Number | 是 | 血肌酐μmol/L | 0 < creatinine ≤ 2000支持2位小数 |
**请求示例**
```json
{
"gender": "male",
"age": 55,
"height": 170,
"dialysis": true,
"dialysisType": "hemodialysis",
"dryWeight": 65.5,
"creatinine": 850
}
```
#### 响应参数Response Body
| 参数名 | 类型 | 说明 |
|--------|------|------|
| `code` | Integer | 状态码,`200` 表示成功 |
| `message` | String | 提示信息 |
| `data` | Object | 计算结果对象 |
**`data` 结构**
| 字段名 | 类型 | 说明 |
|--------|------|------|
| `id` / `resultId` | Long | 计算结果唯一ID用于后续查询/采纳 |
| `healthData` | Object | 健康数据计算结果 |
| `nutritionGoals` | Object | 每日营养目标 |
| `foodList` | Array | 食物份数建议列表 |
| `mealPlan` | Object | 一日三餐配餐方案 |
| `importantTips` | Array\<String\> | 重要提示文案列表 |
| `createdAt` | String | 创建时间 ISO8601 |
**`healthData` 结构**
| 字段名 | 类型 | 说明 | 计算公式 |
|--------|------|------|----------|
| `eGFR` | String | 肾小球滤过率ml/min/1.73m²) | CKD-EPI 公式(见附录 A |
| `standardWeight` | String | 标准体重kg | 男:`(height - 80) × 0.7`;女:`(height - 70) × 0.6` |
| `bmi` | String | 体重指数 | `dryWeight / (height/100)²` |
| `bmiStatus` | String | BMI 状态描述 | 见附录 B |
| `ckdStage` | String | CKD 分期 | 见附录 C |
**`nutritionGoals` 结构**
| 字段名 | 类型 | 说明 | 计算公式 |
|--------|------|------|----------|
| `protein` | String | 每日蛋白质目标(克) | 透析期:`standardWeight × 1.2`;非透析期:`standardWeight × 0.8` |
| `energy` | String | 每日能量目标(千卡) | `standardWeight × 35` |
**`foodList` 结构**数组7 项)
| 字段名 | 类型 | 说明 |
|--------|------|------|
| `number` | Integer | 序号 1-7 |
| `name` | String | 食物类别名称(如"谷薯50g" |
| `portion` | String | 推荐份数 |
**`mealPlan` 结构**
| 字段名 | 类型 | 说明 |
|--------|------|------|
| `breakfast` | Array | 早餐菜品列表 |
| `lunch` | Array | 午餐菜品列表 |
| `dinner` | Array | 晚餐菜品列表 |
**菜品对象结构**
| 字段名 | 类型 | 说明 |
|--------|------|------|
| `name` | String | 菜品名称 |
| `image` | String | 菜品图片 URL |
| `ingredients` | Array\<String\> | 食材列表(如 `["牛奶 120g", "面条 90g"]` |
**响应示例**
```json
{
"code": 200,
"message": "success",
"data": {
"id": 100234,
"healthData": {
"eGFR": "7.9",
"standardWeight": "63.0",
"bmi": "22.7",
"bmiStatus": "正常",
"ckdStage": "CKD 5期"
},
"nutritionGoals": {
"protein": "75.6",
"energy": "2205"
},
"foodList": [
{ "number": 1, "name": "谷薯50g", "portion": "5.7" },
{ "number": 2, "name": "淀粉100g", "portion": "0.77" },
{ "number": 3, "name": "绿叶蔬菜200g", "portion": "1" },
{ "number": 4, "name": "瓜果蔬菜200g", "portion": "2" },
{ "number": 5, "name": "奶类230g", "portion": "1" },
{ "number": 6, "name": "肉蛋类50/60g", "portion": "7" },
{ "number": 7, "name": "油脂类10g", "portion": "5.7" }
],
"mealPlan": {
"breakfast": [
{
"name": "牛奶",
"image": "https://cdn.xxx.com/images/milk.jpg",
"ingredients": ["牛奶 120g"]
},
{
"name": "鸡蛋拌面",
"image": "https://cdn.xxx.com/images/egg-noodle.jpg",
"ingredients": ["面条 90g", "鸡蛋 120g", "葱花 5g"]
},
{
"name": "凉拌黄瓜",
"image": "https://cdn.xxx.com/images/cucumber.jpg",
"ingredients": ["黄瓜 100g"]
}
],
"lunch": [
{
"name": "米饭",
"image": "https://cdn.xxx.com/images/rice.jpg",
"ingredients": ["大米 100g"]
},
{
"name": "清蒸鲈鱼",
"image": "https://cdn.xxx.com/images/bass.jpg",
"ingredients": ["鲈鱼 120g", "生姜 5g", "葱 5g"]
},
{
"name": "蒜蓉西兰花",
"image": "https://cdn.xxx.com/images/broccoli.jpg",
"ingredients": ["西兰花 150g", "大蒜 5g", "植物油 8g"]
},
{
"name": "冬瓜汤",
"image": "https://cdn.xxx.com/images/wax-gourd-soup.jpg",
"ingredients": ["冬瓜 150g"]
}
],
"dinner": [
{
"name": "杂粮饭",
"image": "https://cdn.xxx.com/images/mixed-rice.jpg",
"ingredients": ["大米 70g", "小米 30g"]
},
{
"name": "香菇炒鸡丁",
"image": "https://cdn.xxx.com/images/chicken-mushroom.jpg",
"ingredients": ["鸡胸肉 100g", "香菇 50g", "植物油 8g"]
},
{
"name": "清炒油菜",
"image": "https://cdn.xxx.com/images/bok-choy.jpg",
"ingredients": ["油菜 150g", "植物油 5g"]
},
{
"name": "番茄蛋花汤",
"image": "https://cdn.xxx.com/images/tomato-egg-soup.jpg",
"ingredients": ["番茄 100g", "鸡蛋 60g"]
}
]
},
"importantTips": [
"以上配餐由 AI 生成,仅适用于无其他并发症的单纯尿毒症人群",
"透析患者需严格控制水分摄入",
"建议低盐饮食(每日少于 5g",
"注意限制高钾食物(香蕉、橙子、土豆等)",
"限制高磷食物(坚果、动物内脏等)",
"特别提醒:尿毒症合并其他并发症人群不适用此配餐,请务必咨询专业营养师调整方案"
],
"createdAt": "2026-02-01T10:30:00+08:00"
}
}
```
#### 错误码
| code | message | 说明 |
|------|---------|------|
| 400 | 参数校验失败 | 请求参数不符合校验规则 |
| 401 / 410000-410002 | 未登录/登录过期 | 需重新登录 |
| 500 | 系统异常 | 服务端内部错误 |
---
### 3.2 获取计算结果详情
#### 基础信息
| 项目 | 值 |
|------|------|
| 请求路径 | `GET /api/front/tool/calculator/result/{id}` |
| 是否鉴权 | **是** |
| 路径参数 | `id`计算结果IDLong |
#### 响应
与 3.1 的 `data` 结构相同,用于结果页刷新或分享后重新加载。
---
### 3.3 采纳营养计划
#### 基础信息
| 项目 | 值 |
|------|------|
| 请求路径 | `POST /api/front/tool/calculator/adopt` |
| 是否鉴权 | **是** |
| Content-Type | `application/json` |
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| `resultId` | Long | 是 | 计算结果ID |
**请求示例**
```json
{
"resultId": 100234
}
```
#### 响应
| 字段名 | 类型 | 说明 |
|--------|------|------|
| `code` | Integer | 200 成功 |
| `message` | String | 提示信息 |
| `data.planId` | Long | 新创建的营养计划ID |
| `data.startDate` | String | 计划开始日期 |
| `data.endDate` | String | 计划结束日期(默认 +7 天) |
**响应示例**
```json
{
"code": 200,
"message": "采纳成功",
"data": {
"planId": 56789,
"startDate": "2026-02-01",
"endDate": "2026-02-07"
}
}
```
#### 业务规则
1. 同一用户同一时间只能有 **1 个激活状态(`status=active`** 的营养计划;若已存在则自动将旧计划标记为 `abandoned`
2. 采纳成功后赠送 **+20 积分**(积分任务:采纳营养方案)。
---
## 四、数据库表设计
### 4.1 计算结果表calculator_results
> 存储每次计算的输入与输出,方便查询历史、分析
```sql
CREATE TABLE calculator_results (
result_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
user_id BIGINT NOT NULL COMMENT '用户ID',
-- 输入参数
gender VARCHAR(10) NOT NULL COMMENT '性别male/female',
age INT NOT NULL COMMENT '年龄(岁)',
height INT NOT NULL COMMENT '身高cm',
has_dialysis TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否透析0-否 1-是',
dialysis_type VARCHAR(20) COMMENT '透析类型hemodialysis/peritoneal',
dry_weight DECIMAL(5,2) NOT NULL COMMENT '干体重kg',
creatinine DECIMAL(8,2) NOT NULL COMMENT '血肌酐μmol/L',
-- 计算结果
egfr DECIMAL(6,2) NOT NULL COMMENT 'eGFRml/min/1.73m²)',
standard_weight DECIMAL(5,2) NOT NULL COMMENT '标准体重kg',
bmi DECIMAL(4,1) NOT NULL COMMENT 'BMI',
bmi_status VARCHAR(20) NOT NULL COMMENT 'BMI状态描述',
ckd_stage VARCHAR(50) NOT NULL COMMENT 'CKD分期',
protein_intake DECIMAL(5,1) NOT NULL COMMENT '每日蛋白质目标g',
energy_intake INT NOT NULL COMMENT '每日能量目标kcal',
-- 配餐方案JSON
food_list_json TEXT COMMENT '食物份数建议 JSON',
meal_plan_json TEXT COMMENT '配餐方案 JSON',
tips_json TEXT COMMENT '重要提示 JSON',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='营养计算结果表';
```
### 4.2 营养计划表nutrition_plans
> PRD 中已定义,补充关联
```sql
-- 见 PRD 4.6.5 节
-- 关键字段:
-- plan_id, user_id, gender, age, height, weight, has_dialysis, dialysis_type, creatinine,
-- egfr, standard_weight, bmi, ckd_stage, protein_intake, energy_intake,
-- meal_plan_json, status, start_date, end_date, created_at, updated_at
```
关联关系:`nutrition_plans.source_result_id → calculator_results.id`(可选字段,用于溯源)
---
## 五、核心算法
### 5.1 eGFR 计算CKD-EPI 2021 公式)
```java
/**
* CKD-EPI 2021 公式(无种族系数)
* @param creatinine 血肌酐 (μmol/L)
* @param age 年龄 (岁)
* @param isFemale 是否女性
* @return eGFR (ml/min/1.73m²)
*/
public static double calculateEGFR(double creatinine, int age, boolean isFemale) {
// 将 μmol/L 转换为 mg/dL
double scr = creatinine / 88.4;
double kappa = isFemale ? 0.7 : 0.9;
double alpha = isFemale ? -0.241 : -0.302;
double multi = isFemale ? 1.012 : 1.0;
double minRatio = Math.min(scr / kappa, 1.0);
double maxRatio = Math.max(scr / kappa, 1.0);
double eGFR = 142 * Math.pow(minRatio, alpha) * Math.pow(maxRatio, -1.200)
* Math.pow(0.9938, age) * multi;
return Math.round(eGFR * 100.0) / 100.0; // 保留2位小数
}
```
### 5.2 标准体重
```java
public static double calculateStandardWeight(int height, boolean isFemale) {
return isFemale
? (height - 70) * 0.6
: (height - 80) * 0.7;
}
```
### 5.3 BMI
```java
public static double calculateBMI(double weight, int height) {
double heightM = height / 100.0;
return Math.round(weight / (heightM * heightM) * 10.0) / 10.0;
}
```
### 5.4 BMI 状态
| BMI 范围 | 状态描述 |
|----------|----------|
| < 18.5 | 体型过轻 |
| 18.5 - 23.9 | 正常 |
| 24.0 - 27.9 | 超重 |
| ≥ 28.0 | 肥胖 |
### 5.5 CKD 分期
| eGFR 范围 | 分期 |
|-----------|------|
| ≥ 90 | CKD 1期 |
| 60 - 89 | CKD 2期 |
| 45 - 59 | CKD 3a期 |
| 30 - 44 | CKD 3b期 |
| 15 - 29 | CKD 4期 |
| < 15非透析 | CKD 5期 |
| 透析患者 | 透析期 |
### 5.6 营养目标
```java
public static double calculateProtein(double standardWeight, boolean hasDialysis) {
return hasDialysis
? standardWeight * 1.2 // 透析期
: standardWeight * 0.8; // 非透析期
}
public static int calculateEnergy(double standardWeight) {
return (int) Math.round(standardWeight * 35);
}
```
### 5.7 食物份数
基于能量和蛋白质目标按照《慢性肾脏病患者膳食指导2017》推荐比例分配具体算法可参考专业营养软件或配置表。
### 5.8 配餐方案生成
1. **规则匹配**:基于 CKD 分期、透析类型、营养目标,从预置的 **配餐模板库recipes** 中匹配合适方案。
2. **AI 生成(可选)**:调用 AI 模型根据营养目标动态生成菜品及食材用量。
3. **输出**:早/午/晚三餐各 2-4 道菜,包含图片 URL 和食材列表。
---
## 六、性能与安全
### 6.1 缓存策略
| 数据 | 缓存方式 | 过期时间 | 说明 |
|------|----------|----------|------|
| 计算结果 | Redis Hash | 24 小时 | key = `calc_result:{id}` |
| 配餐模板库 | 本地缓存 / Redis | 1 小时 | 减少 DB 查询 |
### 6.2 限流
| 接口 | QPS 限制 | 说明 |
|------|----------|------|
| calculate | 10/用户/分钟 | 防止恶意刷算 |
| result/{id} | 60/用户/分钟 | 允许刷新 |
| adopt | 5/用户/分钟 | 防重复提交 |
### 6.3 幂等性
- **calculate**:每次计算生成新记录,允许重复调用。
- **adopt**:采纳接口需幂等设计,同一 `resultId` 多次调用只创建一个计划;可用 `user_id + result_id` 唯一索引或分布式锁。
### 6.4 数据安全
- 所有接口均需登录鉴权Header: `Authori-zation`)。
- 用户只能查询/采纳自己的计算结果(`user_id` 校验)。
- 敏感健康数据传输全程 HTTPS。
---
## 七、测试用例
### 7.1 正常场景
| 用例编号 | 描述 | 输入 | 预期输出 |
|----------|------|------|----------|
| TC01 | 男性透析患者计算 | gender=male, age=55, height=170, dialysis=true, dryWeight=65.5, creatinine=850 | 返回 code=200healthData/nutritionGoals/mealPlan 完整 |
| TC02 | 女性非透析患者 | gender=female, age=48, height=160, dialysis=false, dryWeight=52, creatinine=180 | 返回 code=200ckdStage 按 eGFR 判断 |
| TC03 | 查询结果详情 | GET /result/100234 | 返回与 calculate 相同结构 |
| TC04 | 采纳营养计划 | resultId=100234 | 返回 planId计划 status=active |
### 7.2 异常场景
| 用例编号 | 描述 | 输入 | 预期输出 |
|----------|------|------|----------|
| TC11 | 年龄超出范围 | age=200 | code=400, message 包含"年龄" |
| TC12 | 未登录调用 | 无 Token | code=401/410000 |
| TC13 | 查询他人结果 | id 属于其他用户 | code=403 |
| TC14 | 重复采纳 | 同一 resultId 调用两次 | 第二次返回已存在的 planId幂等 |
---
## 八、附录
### 附录 ACKD-EPI 2021 公式
> 无种族系数版本,适用于中国人群
```
eGFR = 142 × min(Scr/κ, 1)^α × max(Scr/κ, 1)^(-1.200) × 0.9938^Age × (1.012 if female)
其中:
- Scr = 血肌酐 (mg/dL) = μmol/L ÷ 88.4
- κ = 0.7 (女) / 0.9 (男)
- α = -0.241 (女) / -0.302 (男)
```
### 附录 BBMI 分类标准(中国)
| BMI | 分类 |
|-----|------|
| < 18.5 | 体型过轻 |
| 18.5 - 23.9 | 正常 |
| 24.0 - 27.9 | 超重 |
| ≥ 28.0 | 肥胖 |
### 附录 CCKD 分期标准
| 分期 | eGFR (ml/min/1.73m²) | 描述 |
|------|----------------------|------|
| 1期 | ≥ 90 | 肾功能正常或升高 |
| 2期 | 60-89 | 轻度下降 |
| 3a期 | 45-59 | 轻-中度下降 |
| 3b期 | 30-44 | 中-重度下降 |
| 4期 | 15-29 | 重度下降 |
| 5期 | < 15 | 肾衰竭 |
| 透析期 | — | 已接受透析治疗 |
---
## 九、变更记录
| 版本 | 日期 | 作者 | 变更内容 |
|------|------|------|----------|
| v1.0 | 2026-02-01 | — | 初稿 |
---
*文档结束*

BIN
models-integration/.DS_Store vendored Normal file

Binary file not shown.

BIN
models-integration/.idea/.DS_Store generated vendored Normal file

Binary file not shown.

8
models-integration/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="com.codeverse.userSettings.MarscodeWorkspaceAppSettingsState">
<option name="chatAppRouterInfo" value="builder/68ec7201f33b556a21fc615e" />
<option name="progress" value="1.0" />
</component>
</project>

16
models-integration/.idea/compiler.xml generated Normal file
View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<option name="BUILD_PROCESS_HEAP_SIZE" value="8700" />
<annotationProcessing>
<profile default="true" name="Default" enabled="true" />
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="model.integration" />
<module name="models-integration" />
</profile>
</annotationProcessing>
</component>
</project>

13
models-integration/.idea/easycode.ignore generated Normal file
View File

@@ -0,0 +1,13 @@
.idea
.vscode
node_modules/
dist/
vendor/
cache/
.*/
*.min.*
*.test.*
*.spec.*
*.bundle.*
*.bundle-min.*
*.log

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="com.obiscr.chatgpt.settings.EasyCodeState">
<option name="projectFiles" value="$PROJECT_DIR$/src/main/java/com/xbongbong/api/demo/communicate/CommunicateApi.java;/Users/a123/Documents/UthinkJava2025/xbb-api-demo/src/main/java/com/xbongbong/api/demo/contact/ContactApi.java;/Users/a123/Documents/UthinkJava2025/xbb-api-demo/src/main/java/com/xbongbong/api/demo/customer/CustomerApi.java;/Users/a123/Documents/UthinkJava2025/xbb-api-demo/src/main/java/com/xbongbong/api/demo/form/FormApi.java;/Users/a123/Documents/UthinkJava2025/xbb-api-demo/src/main/java/com/xbongbong/api/demo/helper/ConfigConstant.java;/Users/a123/Documents/UthinkJava2025/xbb-api-demo/src/main/java/com/xbongbong/api/demo/helper/DigestUtil.java;/Users/a123/Documents/UthinkJava2025/xbb-api-demo/src/main/java/com/xbongbong/api/demo/helper/HttpRequestUtils.java;/Users/a123/Documents/UthinkJava2025/xbb-api-demo/src/main/java/com/xbongbong/api/demo/helper/XbbException.java;/Users/a123/Documents/UthinkJava2025/xbb-api-demo/src/main/java/com/xbongbong/api/demo/Demo.java;/Users/a123/Documents/UthinkJava2025/xbb-api-demo/coding-tip.md;/Users/a123/Documents/UthinkJava2025/xbb-api-demo/readme-pom.md;/Users/a123/Documents/UthinkJava2025/xbb-api-demo/readme.md" />
</component>
</project>

7
models-integration/.idea/encodings.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>

View File

@@ -0,0 +1,36 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="JavaDoc" enabled="true" level="WARNING" enabled_by_default="true">
<option name="TOP_LEVEL_CLASS_OPTIONS">
<value>
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
<option name="REQUIRED_TAGS" value="" />
</value>
</option>
<option name="INNER_CLASS_OPTIONS">
<value>
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
<option name="REQUIRED_TAGS" value="" />
</value>
</option>
<option name="METHOD_OPTIONS">
<value>
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
<option name="REQUIRED_TAGS" value="@return@param@throws or @exception" />
</value>
</option>
<option name="FIELD_OPTIONS">
<value>
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
<option name="REQUIRED_TAGS" value="" />
</value>
</option>
<option name="IGNORE_DEPRECATED" value="false" />
<option name="IGNORE_JAVADOC_PERIOD" value="true" />
<option name="IGNORE_DUPLICATED_THROWS" value="false" />
<option name="IGNORE_POINT_TO_ITSELF" value="false" />
<option name="myAdditionalJavadocTags" value="date" />
</inspection_tool>
</profile>
</component>

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="nexus" />
<option name="name" value="local private nexus" />
<option name="url" value="https://maven.aliyun.com/nexus/content/groups/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="local-nexus" />
<option name="name" value="local-nexus" />
<option name="url" value="http://192.168.10.4:8081/nexus/content/groups/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="http://maven.oschina.net/content/groups/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://maven.aliyun.com/repository/public" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://maven.oschina.net/content/groups/public/" />
</remote-repository>
</component>
</project>

23
models-integration/.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FindBugsConfigurable">
<option name="make" value="true" />
<option name="effort" value="default" />
<option name="priority" value="Medium" />
<option name="excludeFilter" value="" />
</component>
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
<component name="SuppressionsComponent">
<option name="suppComments" value="[]" />
</component>
</project>

View File

@@ -0,0 +1,465 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AnalysisProjectProfileManager">
<option name="PROJECT_PROFILE" value="Project Default" />
<option name="USE_PROJECT_LEVEL_SETTINGS" value="false" />
<scopes />
<profiles>
<profile profile_name="Project Default" version="1.0" is_locked="false">
<coding_rule class="AM_CREATES_EMPTY_JAR_FILE_ENTRY" level="MAJOR" enabled="true" />
<coding_rule class="AM_CREATES_EMPTY_ZIP_FILE_ENTRY" level="MAJOR" enabled="true" />
<coding_rule class="AT_OPERATION_SEQUENCE_ON_CONCURRENT_ABSTRACTION" level="CRITICAL" enabled="false" />
<coding_rule class="BAC_BAD_APPLET_CONSTRUCTOR" level="MAJOR" enabled="false" />
<coding_rule class="BC_BAD_CAST_TO_ABSTRACT_COLLECTION" level="MAJOR" enabled="true" />
<coding_rule class="BC_BAD_CAST_TO_CONCRETE_COLLECTION" level="CRITICAL" enabled="true" />
<coding_rule class="BC_EQUALS_METHOD_SHOULD_WORK_FOR_ALL_OBJECTS" level="CRITICAL" enabled="true" />
<coding_rule class="BC_IMPOSSIBLE_CAST" level="BLOCKER" enabled="true" />
<coding_rule class="BC_IMPOSSIBLE_DOWNCAST" level="MAJOR" enabled="false" />
<coding_rule class="BC_IMPOSSIBLE_DOWNCAST_OF_TOARRAY" level="MAJOR" enabled="false" />
<coding_rule class="BC_IMPOSSIBLE_INSTANCEOF" level="CRITICAL" enabled="true" />
<coding_rule class="BC_UNCONFIRMED_CAST" level="CRITICAL" enabled="true" />
<coding_rule class="BC_UNCONFIRMED_CAST_OF_RETURN_VALUE" level="CRITICAL" enabled="false" />
<coding_rule class="BC_VACUOUS_INSTANCEOF" level="CRITICAL" enabled="true" />
<coding_rule class="BIT_ADD_OF_SIGNED_BYTE" level="CRITICAL" enabled="true" />
<coding_rule class="BIT_AND" level="CRITICAL" enabled="true" />
<coding_rule class="BIT_AND_ZZ" level="CRITICAL" enabled="true" />
<coding_rule class="BIT_IOR" level="CRITICAL" enabled="true" />
<coding_rule class="BIT_IOR_OF_SIGNED_BYTE" level="CRITICAL" enabled="true" />
<coding_rule class="BIT_SIGNED_CHECK" level="CRITICAL" enabled="true" />
<coding_rule class="BIT_SIGNED_CHECK_HIGH_BIT" level="CRITICAL" enabled="true" />
<coding_rule class="BOA_BADLY_OVERRIDDEN_ADAPTER" level="CRITICAL" enabled="true" />
<coding_rule class="BSHIFT_WRONG_ADD_PRIORITY" level="MAJOR" enabled="false" />
<coding_rule class="BX_BOXING_IMMEDIATELY_UNBOXED" level="MAJOR" enabled="true" />
<coding_rule class="BX_BOXING_IMMEDIATELY_UNBOXED_TO_PERFORM_COERCION" level="MAJOR" enabled="true" />
<coding_rule class="BX_UNBOXED_AND_COERCED_FOR_TERNARY_OPERATOR" level="MAJOR" enabled="true" />
<coding_rule class="BX_UNBOXING_IMMEDIATELY_REBOXED" level="CRITICAL" enabled="false" />
<coding_rule class="CAA_COVARIANT_ARRAY_ELEMENT_STORE" level="MAJOR" enabled="false" />
<coding_rule class="CAA_COVARIANT_ARRAY_FIELD" level="MAJOR" enabled="false" />
<coding_rule class="CAA_COVARIANT_ARRAY_LOCAL" level="MAJOR" enabled="false" />
<coding_rule class="CAA_COVARIANT_ARRAY_RETURN" level="MAJOR" enabled="false" />
<coding_rule class="CD_CIRCULAR_DEPENDENCY" level="MAJOR" enabled="false" />
<coding_rule class="CI_CONFUSED_INHERITANCE" level="MINOR" enabled="true" />
<coding_rule class="CNT_ROUGH_CONSTANT_VALUE" level="MAJOR" enabled="false" />
<coding_rule class="CN_IDIOM" level="MAJOR" enabled="true" />
<coding_rule class="CN_IDIOM_NO_SUPER_CALL" level="MAJOR" enabled="true" />
<coding_rule class="CN_IMPLEMENTS_CLONE_BUT_NOT_CLONEABLE" level="MAJOR" enabled="true" />
<coding_rule class="CO_ABSTRACT_SELF" level="MAJOR" enabled="true" />
<coding_rule class="CO_COMPARETO_INCORRECT_FLOATING" level="MAJOR" enabled="false" />
<coding_rule class="CO_COMPARETO_RESULTS_MIN_VALUE" level="CRITICAL" enabled="false" />
<coding_rule class="CO_SELF_NO_OBJECT" level="MAJOR" enabled="true" />
<coding_rule class="DB_DUPLICATE_BRANCHES" level="CRITICAL" enabled="true" />
<coding_rule class="DB_DUPLICATE_SWITCH_CLAUSES" level="CRITICAL" enabled="true" />
<coding_rule class="DC_DOUBLECHECK" level="MAJOR" enabled="true" />
<coding_rule class="DC_PARTIALLY_CONSTRUCTED" level="MAJOR" enabled="false" />
<coding_rule class="DE_MIGHT_DROP" level="MAJOR" enabled="true" />
<coding_rule class="DE_MIGHT_IGNORE" level="MAJOR" enabled="true" />
<coding_rule class="DLS_DEAD_LOCAL_INCREMENT_IN_RETURN" level="MAJOR" enabled="false" />
<coding_rule class="DLS_DEAD_LOCAL_STORE" level="CRITICAL" enabled="true" />
<coding_rule class="DLS_DEAD_LOCAL_STORE_IN_RETURN" level="CRITICAL" enabled="true" />
<coding_rule class="DLS_DEAD_LOCAL_STORE_OF_NULL" level="CRITICAL" enabled="true" />
<coding_rule class="DLS_DEAD_LOCAL_STORE_SHADOWS_FIELD" level="CRITICAL" enabled="false" />
<coding_rule class="DLS_DEAD_STORE_OF_CLASS_LITERAL" level="CRITICAL" enabled="true" />
<coding_rule class="DLS_OVERWRITTEN_INCREMENT" level="CRITICAL" enabled="true" />
<coding_rule class="DL_SYNCHRONIZATION_ON_BOOLEAN" level="CRITICAL" enabled="true" />
<coding_rule class="DL_SYNCHRONIZATION_ON_BOXED_PRIMITIVE" level="CRITICAL" enabled="true" />
<coding_rule class="DL_SYNCHRONIZATION_ON_SHARED_CONSTANT" level="CRITICAL" enabled="true" />
<coding_rule class="DL_SYNCHRONIZATION_ON_UNSHARED_BOXED_PRIMITIVE" level="CRITICAL" enabled="true" />
<coding_rule class="DMI_ANNOTATION_IS_NOT_VISIBLE_TO_REFLECTION" level="MAJOR" enabled="true" />
<coding_rule class="DMI_ARGUMENTS_WRONG_ORDER" level="CRITICAL" enabled="false" />
<coding_rule class="DMI_BAD_MONTH" level="CRITICAL" enabled="true" />
<coding_rule class="DMI_BIGDECIMAL_CONSTRUCTED_FROM_DOUBLE" level="CRITICAL" enabled="false" />
<coding_rule class="DMI_BLOCKING_METHODS_ON_URL" level="BLOCKER" enabled="true" />
<coding_rule class="DMI_CALLING_NEXT_FROM_HASNEXT" level="CRITICAL" enabled="true" />
<coding_rule class="DMI_COLLECTIONS_SHOULD_NOT_CONTAIN_THEMSELVES" level="CRITICAL" enabled="true" />
<coding_rule class="DMI_COLLECTION_OF_URLS" level="BLOCKER" enabled="true" />
<coding_rule class="DMI_CONSTANT_DB_PASSWORD" level="BLOCKER" enabled="true" />
<coding_rule class="DMI_DOH" level="CRITICAL" enabled="false" />
<coding_rule class="DMI_EMPTY_DB_PASSWORD" level="CRITICAL" enabled="true" />
<coding_rule class="DMI_ENTRY_SETS_MAY_REUSE_ENTRY_OBJECTS" level="CRITICAL" enabled="false" />
<coding_rule class="DMI_FUTILE_ATTEMPT_TO_CHANGE_MAXPOOL_SIZE_OF_SCHEDULED_THREAD_POOL_EXECUTOR" level="MINOR" enabled="true" />
<coding_rule class="DMI_HARDCODED_ABSOLUTE_FILENAME" level="CRITICAL" enabled="true" />
<coding_rule class="DMI_INVOKING_HASHCODE_ON_ARRAY" level="CRITICAL" enabled="true" />
<coding_rule class="DMI_INVOKING_TOSTRING_ON_ANONYMOUS_ARRAY" level="CRITICAL" enabled="true" />
<coding_rule class="DMI_INVOKING_TOSTRING_ON_ARRAY" level="CRITICAL" enabled="true" />
<coding_rule class="DMI_LONG_BITS_TO_DOUBLE_INVOKED_ON_INT" level="CRITICAL" enabled="true" />
<coding_rule class="DMI_NONSERIALIZABLE_OBJECT_WRITTEN" level="CRITICAL" enabled="true" />
<coding_rule class="DMI_RANDOM_USED_ONLY_ONCE" level="CRITICAL" enabled="true" />
<coding_rule class="DMI_SCHEDULED_THREAD_POOL_EXECUTOR_WITH_ZERO_CORE_THREADS" level="MINOR" enabled="true" />
<coding_rule class="DMI_THREAD_PASSED_WHERE_RUNNABLE_EXPECTED" level="MAJOR" enabled="true" />
<coding_rule class="DMI_UNSUPPORTED_METHOD" level="MAJOR" enabled="true" />
<coding_rule class="DMI_USELESS_SUBSTRING" level="CRITICAL" enabled="true" />
<coding_rule class="DMI_USING_REMOVEALL_TO_CLEAR_COLLECTION" level="CRITICAL" enabled="true" />
<coding_rule class="DMI_VACUOUS_CALL_TO_EASYMOCK_METHOD" level="MINOR" enabled="true" />
<coding_rule class="DMI_VACUOUS_SELF_COLLECTION_CALL" level="CRITICAL" enabled="true" />
<coding_rule class="DM_BOOLEAN_CTOR" level="MAJOR" enabled="true" />
<coding_rule class="DM_BOXED_PRIMITIVE_FOR_COMPARE" level="MAJOR" enabled="false" />
<coding_rule class="DM_BOXED_PRIMITIVE_FOR_PARSING" level="MAJOR" enabled="false" />
<coding_rule class="DM_BOXED_PRIMITIVE_TOSTRING" level="MAJOR" enabled="true" />
<coding_rule class="DM_CONVERT_CASE" level="INFO" enabled="true" />
<coding_rule class="DM_DEFAULT_ENCODING" level="CRITICAL" enabled="false" />
<coding_rule class="DM_EXIT" level="MAJOR" enabled="true" />
<coding_rule class="DM_FP_NUMBER_CTOR" level="MAJOR" enabled="true" />
<coding_rule class="DM_GC" level="MAJOR" enabled="true" />
<coding_rule class="DM_INVALID_MIN_MAX" level="MAJOR" enabled="false" />
<coding_rule class="DM_MONITOR_WAIT_ON_CONDITION" level="MAJOR" enabled="true" />
<coding_rule class="DM_NEW_FOR_GETCLASS" level="MAJOR" enabled="true" />
<coding_rule class="DM_NEXTINT_VIA_NEXTDOUBLE" level="MAJOR" enabled="true" />
<coding_rule class="DM_NUMBER_CTOR" level="CRITICAL" enabled="true" />
<coding_rule class="DM_RUN_FINALIZERS_ON_EXIT" level="MAJOR" enabled="true" />
<coding_rule class="DM_STRING_CTOR" level="MAJOR" enabled="true" />
<coding_rule class="DM_STRING_TOSTRING" level="INFO" enabled="true" />
<coding_rule class="DM_STRING_VOID_CTOR" level="MAJOR" enabled="true" />
<coding_rule class="DM_USELESS_THREAD" level="MAJOR" enabled="true" />
<coding_rule class="DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED" level="MAJOR" enabled="true" />
<coding_rule class="DP_DO_INSIDE_DO_PRIVILEGED" level="MAJOR" enabled="true" />
<coding_rule class="EC_ARRAY_AND_NONARRAY" level="CRITICAL" enabled="true" />
<coding_rule class="EC_BAD_ARRAY_COMPARE" level="CRITICAL" enabled="true" />
<coding_rule class="EC_INCOMPATIBLE_ARRAY_COMPARE" level="MAJOR" enabled="false" />
<coding_rule class="EC_NULL_ARG" level="CRITICAL" enabled="true" />
<coding_rule class="EC_UNRELATED_CLASS_AND_INTERFACE" level="CRITICAL" enabled="true" />
<coding_rule class="EC_UNRELATED_INTERFACES" level="CRITICAL" enabled="true" />
<coding_rule class="EC_UNRELATED_TYPES" level="CRITICAL" enabled="true" />
<coding_rule class="EC_UNRELATED_TYPES_USING_POINTER_EQUALITY" level="CRITICAL" enabled="true" />
<coding_rule class="EI_EXPOSE_REP" level="MAJOR" enabled="true" />
<coding_rule class="EI_EXPOSE_REP2" level="MAJOR" enabled="true" />
<coding_rule class="EI_EXPOSE_STATIC_REP2" level="MAJOR" enabled="true" />
<coding_rule class="EQ_ABSTRACT_SELF" level="MAJOR" enabled="true" />
<coding_rule class="EQ_ALWAYS_FALSE" level="BLOCKER" enabled="true" />
<coding_rule class="EQ_ALWAYS_TRUE" level="BLOCKER" enabled="true" />
<coding_rule class="EQ_CHECK_FOR_OPERAND_NOT_COMPATIBLE_WITH_THIS" level="MAJOR" enabled="true" />
<coding_rule class="EQ_COMPARETO_USE_OBJECT_EQUALS" level="CRITICAL" enabled="true" />
<coding_rule class="EQ_COMPARING_CLASS_NAMES" level="MAJOR" enabled="true" />
<coding_rule class="EQ_DOESNT_OVERRIDE_EQUALS" level="MAJOR" enabled="false" />
<coding_rule class="EQ_DONT_DEFINE_EQUALS_FOR_ENUM" level="MAJOR" enabled="true" />
<coding_rule class="EQ_GETCLASS_AND_CLASS_CONSTANT" level="CRITICAL" enabled="true" />
<coding_rule class="EQ_OTHER_NO_OBJECT" level="MAJOR" enabled="true" />
<coding_rule class="EQ_OTHER_USE_OBJECT" level="MAJOR" enabled="true" />
<coding_rule class="EQ_OVERRIDING_EQUALS_NOT_SYMMETRIC" level="MAJOR" enabled="true" />
<coding_rule class="EQ_SELF_NO_OBJECT" level="MAJOR" enabled="true" />
<coding_rule class="EQ_SELF_USE_OBJECT" level="MAJOR" enabled="true" />
<coding_rule class="EQ_UNUSUAL" level="MINOR" enabled="true" />
<coding_rule class="ES_COMPARING_PARAMETER_STRING_WITH_EQ" level="MAJOR" enabled="true" />
<coding_rule class="ES_COMPARING_STRINGS_WITH_EQ" level="MAJOR" enabled="true" />
<coding_rule class="ESync_EMPTY_SYNC" level="MAJOR" enabled="true" />
<coding_rule class="FB_MISSING_EXPECTED_WARNING" level="CRITICAL" enabled="false" />
<coding_rule class="FB_UNEXPECTED_WARNING" level="CRITICAL" enabled="false" />
<coding_rule class="FE_FLOATING_POINT_EQUALITY" level="CRITICAL" enabled="true" />
<coding_rule class="FE_TEST_IF_EQUAL_TO_NOT_A_NUMBER" level="CRITICAL" enabled="true" />
<coding_rule class="FI_EMPTY" level="MAJOR" enabled="true" />
<coding_rule class="FI_EXPLICIT_INVOCATION" level="MAJOR" enabled="true" />
<coding_rule class="FI_FINALIZER_NULLS_FIELDS" level="MAJOR" enabled="true" />
<coding_rule class="FI_FINALIZER_ONLY_NULLS_FIELDS" level="MAJOR" enabled="true" />
<coding_rule class="FI_MISSING_SUPER_CALL" level="MAJOR" enabled="true" />
<coding_rule class="FI_NULLIFY_SUPER" level="CRITICAL" enabled="true" />
<coding_rule class="FI_PUBLIC_SHOULD_BE_PROTECTED" level="MAJOR" enabled="true" />
<coding_rule class="FI_USELESS" level="MINOR" enabled="true" />
<coding_rule class="FL_MATH_USING_FLOAT_PRECISION" level="CRITICAL" enabled="true" />
<coding_rule class="GC_UNCHECKED_TYPE_IN_GENERIC_CALL" level="CRITICAL" enabled="true" />
<coding_rule class="GC_UNRELATED_TYPES" level="CRITICAL" enabled="true" />
<coding_rule class="HE_EQUALS_NO_HASHCODE" level="MAJOR" enabled="true" />
<coding_rule class="HE_EQUALS_USE_HASHCODE" level="CRITICAL" enabled="true" />
<coding_rule class="HE_HASHCODE_NO_EQUALS" level="CRITICAL" enabled="true" />
<coding_rule class="HE_HASHCODE_USE_OBJECT_EQUALS" level="CRITICAL" enabled="true" />
<coding_rule class="HE_INHERITS_EQUALS_USE_HASHCODE" level="CRITICAL" enabled="true" />
<coding_rule class="HE_SIGNATURE_DECLARES_HASHING_OF_UNHASHABLE_CLASS" level="CRITICAL" enabled="true" />
<coding_rule class="HE_USE_OF_UNHASHABLE_CLASS" level="CRITICAL" enabled="true" />
<coding_rule class="HRS_REQUEST_PARAMETER_TO_COOKIE" level="MAJOR" enabled="true" />
<coding_rule class="HRS_REQUEST_PARAMETER_TO_HTTP_HEADER" level="MAJOR" enabled="true" />
<coding_rule class="HSC_HUGE_SHARED_STRING_CONSTANT" level="CRITICAL" enabled="true" />
<coding_rule class="IA_AMBIGUOUS_INVOCATION_OF_INHERITED_OR_OUTER_METHOD" level="MAJOR" enabled="true" />
<coding_rule class="ICAST_BAD_SHIFT_AMOUNT" level="CRITICAL" enabled="true" />
<coding_rule class="ICAST_IDIV_CAST_TO_DOUBLE" level="CRITICAL" enabled="true" />
<coding_rule class="ICAST_INTEGER_MULTIPLY_CAST_TO_LONG" level="CRITICAL" enabled="true" />
<coding_rule class="ICAST_INT_2_LONG_AS_INSTANT" level="CRITICAL" enabled="false" />
<coding_rule class="ICAST_INT_CAST_TO_DOUBLE_PASSED_TO_CEIL" level="CRITICAL" enabled="true" />
<coding_rule class="ICAST_INT_CAST_TO_FLOAT_PASSED_TO_ROUND" level="CRITICAL" enabled="true" />
<coding_rule class="ICAST_QUESTIONABLE_UNSIGNED_RIGHT_SHIFT" level="CRITICAL" enabled="true" />
<coding_rule class="IC_INIT_CIRCULARITY" level="CRITICAL" enabled="true" />
<coding_rule class="IC_SUPERCLASS_USES_SUBCLASS_DURING_INITIALIZATION" level="MAJOR" enabled="true" />
<coding_rule class="IIL_ELEMENTS_GET_LENGTH_IN_LOOP" level="MAJOR" enabled="false" />
<coding_rule class="IIL_PATTERN_COMPILE_IN_LOOP" level="MAJOR" enabled="false" />
<coding_rule class="IIL_PATTERN_COMPILE_IN_LOOP_INDIRECT" level="MAJOR" enabled="false" />
<coding_rule class="IIL_PREPARE_STATEMENT_IN_LOOP" level="MAJOR" enabled="false" />
<coding_rule class="IIO_INEFFICIENT_INDEX_OF" level="MAJOR" enabled="false" />
<coding_rule class="IIO_INEFFICIENT_LAST_INDEX_OF" level="MAJOR" enabled="false" />
<coding_rule class="IJU_ASSERT_METHOD_INVOKED_FROM_RUN_METHOD" level="CRITICAL" enabled="true" />
<coding_rule class="IJU_BAD_SUITE_METHOD" level="CRITICAL" enabled="true" />
<coding_rule class="IJU_NO_TESTS" level="CRITICAL" enabled="true" />
<coding_rule class="IJU_SETUP_NO_SUPER" level="CRITICAL" enabled="true" />
<coding_rule class="IJU_SUITE_NOT_STATIC" level="CRITICAL" enabled="true" />
<coding_rule class="IJU_TEARDOWN_NO_SUPER" level="CRITICAL" enabled="true" />
<coding_rule class="IL_CONTAINER_ADDED_TO_ITSELF" level="CRITICAL" enabled="true" />
<coding_rule class="IL_INFINITE_LOOP" level="CRITICAL" enabled="true" />
<coding_rule class="IL_INFINITE_RECURSIVE_LOOP" level="CRITICAL" enabled="true" />
<coding_rule class="IMA_INEFFICIENT_MEMBER_ACCESS" level="MAJOR" enabled="false" />
<coding_rule class="IMSE_DONT_CATCH_IMSE" level="MAJOR" enabled="true" />
<coding_rule class="IM_AVERAGE_COMPUTATION_COULD_OVERFLOW" level="CRITICAL" enabled="true" />
<coding_rule class="IM_BAD_CHECK_FOR_ODD" level="CRITICAL" enabled="true" />
<coding_rule class="IM_MULTIPLYING_RESULT_OF_IREM" level="CRITICAL" enabled="true" />
<coding_rule class="INT_BAD_COMPARISON_WITH_INT_VALUE" level="CRITICAL" enabled="false" />
<coding_rule class="INT_BAD_COMPARISON_WITH_NONNEGATIVE_VALUE" level="CRITICAL" enabled="true" />
<coding_rule class="INT_BAD_COMPARISON_WITH_SIGNED_BYTE" level="CRITICAL" enabled="true" />
<coding_rule class="INT_BAD_REM_BY_1" level="CRITICAL" enabled="true" />
<coding_rule class="INT_VACUOUS_BIT_OPERATION" level="CRITICAL" enabled="true" />
<coding_rule class="INT_VACUOUS_COMPARISON" level="CRITICAL" enabled="true" />
<coding_rule class="IO_APPENDING_TO_OBJECT_OUTPUT_STREAM" level="CRITICAL" enabled="true" />
<coding_rule class="IP_PARAMETER_IS_DEAD_BUT_OVERWRITTEN" level="CRITICAL" enabled="true" />
<coding_rule class="IS2_INCONSISTENT_SYNC" level="CRITICAL" enabled="true" />
<coding_rule class="ISC_INSTANTIATE_STATIC_CLASS" level="MAJOR" enabled="true" />
<coding_rule class="IS_FIELD_NOT_GUARDED" level="CRITICAL" enabled="true" />
<coding_rule class="ITA_INEFFICIENT_TO_ARRAY" level="CRITICAL" enabled="true" />
<coding_rule class="IT_NO_SUCH_ELEMENT" level="MINOR" enabled="true" />
<coding_rule class="J2EE_STORE_OF_NON_SERIALIZABLE_OBJECT_INTO_SESSION" level="CRITICAL" enabled="true" />
<coding_rule class="JCIP_FIELD_ISNT_FINAL_IN_IMMUTABLE_CLASS" level="MINOR" enabled="true" />
<coding_rule class="JLM_JSR166_LOCK_MONITORENTER" level="CRITICAL" enabled="true" />
<coding_rule class="JLM_JSR166_UTILCONCURRENT_MONITORENTER" level="CRITICAL" enabled="false" />
<coding_rule class="JML_JSR166_CALLING_WAIT_RATHER_THAN_AWAIT" level="CRITICAL" enabled="false" />
<coding_rule class="LG_LOST_LOGGER_DUE_TO_WEAK_REFERENCE" level="MAJOR" enabled="false" />
<coding_rule class="LI_LAZY_INIT_STATIC" level="CRITICAL" enabled="true" />
<coding_rule class="LI_LAZY_INIT_UPDATE_STATIC" level="CRITICAL" enabled="true" />
<coding_rule class="ME_ENUM_FIELD_SETTER" level="MAJOR" enabled="false" />
<coding_rule class="ME_MUTABLE_ENUM_FIELD" level="MAJOR" enabled="false" />
<coding_rule class="MF_CLASS_MASKS_FIELD" level="MAJOR" enabled="true" />
<coding_rule class="MF_METHOD_MASKS_FIELD" level="MAJOR" enabled="true" />
<coding_rule class="ML_SYNC_ON_FIELD_TO_GUARD_CHANGING_THAT_FIELD" level="MAJOR" enabled="true" />
<coding_rule class="ML_SYNC_ON_UPDATED_FIELD" level="MAJOR" enabled="true" />
<coding_rule class="MSF_MUTABLE_SERVLET_FIELD" level="MAJOR" enabled="true" />
<coding_rule class="MS_CANNOT_BE_FINAL" level="MAJOR" enabled="true" />
<coding_rule class="MS_EXPOSE_REP" level="CRITICAL" enabled="true" />
<coding_rule class="MS_FINAL_PKGPROTECT" level="MAJOR" enabled="true" />
<coding_rule class="MS_MUTABLE_ARRAY" level="MAJOR" enabled="true" />
<coding_rule class="MS_MUTABLE_COLLECTION" level="MAJOR" enabled="false" />
<coding_rule class="MS_MUTABLE_COLLECTION_PKGPROTECT" level="MAJOR" enabled="false" />
<coding_rule class="MS_MUTABLE_HASHTABLE" level="MAJOR" enabled="true" />
<coding_rule class="MS_OOI_PKGPROTECT" level="MAJOR" enabled="true" />
<coding_rule class="MS_PKGPROTECT" level="MAJOR" enabled="true" />
<coding_rule class="MS_SHOULD_BE_FINAL" level="MAJOR" enabled="true" />
<coding_rule class="MS_SHOULD_BE_REFACTORED_TO_BE_FINAL" level="CRITICAL" enabled="false" />
<coding_rule class="MTIA_SUSPECT_SERVLET_INSTANCE_FIELD" level="CRITICAL" enabled="true" />
<coding_rule class="MTIA_SUSPECT_STRUTS_INSTANCE_FIELD" level="CRITICAL" enabled="true" />
<coding_rule class="MWN_MISMATCHED_NOTIFY" level="CRITICAL" enabled="true" />
<coding_rule class="MWN_MISMATCHED_WAIT" level="CRITICAL" enabled="true" />
<coding_rule class="NM_BAD_EQUAL" level="MAJOR" enabled="false" />
<coding_rule class="NM_CLASS_NAMING_CONVENTION" level="MAJOR" enabled="false" />
<coding_rule class="NM_CLASS_NOT_EXCEPTION" level="MAJOR" enabled="true" />
<coding_rule class="NM_CONFUSING" level="MAJOR" enabled="true" />
<coding_rule class="NM_FIELD_NAMING_CONVENTION" level="MAJOR" enabled="false" />
<coding_rule class="NM_FUTURE_KEYWORD_USED_AS_IDENTIFIER" level="MAJOR" enabled="true" />
<coding_rule class="NM_FUTURE_KEYWORD_USED_AS_MEMBER_IDENTIFIER" level="MAJOR" enabled="true" />
<coding_rule class="NM_LCASE_HASHCODE" level="MAJOR" enabled="false" />
<coding_rule class="NM_LCASE_TOSTRING" level="MAJOR" enabled="false" />
<coding_rule class="NM_METHOD_CONSTRUCTOR_CONFUSION" level="MAJOR" enabled="true" />
<coding_rule class="NM_METHOD_NAMING_CONVENTION" level="MAJOR" enabled="false" />
<coding_rule class="NM_SAME_SIMPLE_NAME_AS_INTERFACE" level="MAJOR" enabled="true" />
<coding_rule class="NM_SAME_SIMPLE_NAME_AS_SUPERCLASS" level="MAJOR" enabled="true" />
<coding_rule class="NM_VERY_CONFUSING" level="MAJOR" enabled="true" />
<coding_rule class="NM_VERY_CONFUSING_INTENTIONAL" level="MAJOR" enabled="true" />
<coding_rule class="NM_WRONG_PACKAGE" level="MAJOR" enabled="true" />
<coding_rule class="NM_WRONG_PACKAGE_INTENTIONAL" level="MAJOR" enabled="true" />
<coding_rule class="NN_NAKED_NOTIFY" level="CRITICAL" enabled="true" />
<coding_rule class="NOISE_FIELD_REFERENCE" level="MAJOR" enabled="false" />
<coding_rule class="NOISE_METHOD_CALL" level="MAJOR" enabled="false" />
<coding_rule class="NOISE_NULL_DEREFERENCE" level="MAJOR" enabled="false" />
<coding_rule class="NOISE_OPERATION" level="MAJOR" enabled="false" />
<coding_rule class="NO_NOTIFY_NOT_NOTIFYALL" level="CRITICAL" enabled="true" />
<coding_rule class="NP_ALWAYS_NULL" level="CRITICAL" enabled="true" />
<coding_rule class="NP_ALWAYS_NULL_EXCEPTION" level="CRITICAL" enabled="true" />
<coding_rule class="NP_ARGUMENT_MIGHT_BE_NULL" level="MAJOR" enabled="true" />
<coding_rule class="NP_BOOLEAN_RETURN_NULL" level="MAJOR" enabled="true" />
<coding_rule class="NP_CLONE_COULD_RETURN_NULL" level="CRITICAL" enabled="true" />
<coding_rule class="NP_CLOSING_NULL" level="MAJOR" enabled="false" />
<coding_rule class="NP_DEREFERENCE_OF_READLINE_VALUE" level="CRITICAL" enabled="true" />
<coding_rule class="NP_EQUALS_SHOULD_HANDLE_NULL_ARGUMENT" level="CRITICAL" enabled="true" />
<coding_rule class="NP_GUARANTEED_DEREF" level="BLOCKER" enabled="true" />
<coding_rule class="NP_GUARANTEED_DEREF_ON_EXCEPTION_PATH" level="CRITICAL" enabled="true" />
<coding_rule class="NP_IMMEDIATE_DEREFERENCE_OF_READLINE" level="CRITICAL" enabled="true" />
<coding_rule class="NP_LOAD_OF_KNOWN_NULL_VALUE" level="CRITICAL" enabled="true" />
<coding_rule class="NP_METHOD_PARAMETER_TIGHTENS_ANNOTATION" level="MAJOR" enabled="false" />
<coding_rule class="NP_METHOD_RETURN_RELAXING_ANNOTATION" level="MAJOR" enabled="false" />
<coding_rule class="NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR" level="CRITICAL" enabled="false" />
<coding_rule class="NP_NONNULL_PARAM_VIOLATION" level="CRITICAL" enabled="true" />
<coding_rule class="NP_NONNULL_RETURN_VIOLATION" level="CRITICAL" enabled="true" />
<coding_rule class="NP_NULL_INSTANCEOF" level="BLOCKER" enabled="true" />
<coding_rule class="NP_NULL_ON_SOME_PATH" level="CRITICAL" enabled="true" />
<coding_rule class="NP_NULL_ON_SOME_PATH_EXCEPTION" level="CRITICAL" enabled="true" />
<coding_rule class="NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE" level="CRITICAL" enabled="true" />
<coding_rule class="NP_NULL_ON_SOME_PATH_MIGHT_BE_INFEASIBLE" level="CRITICAL" enabled="true" />
<coding_rule class="NP_NULL_PARAM_DEREF" level="CRITICAL" enabled="true" />
<coding_rule class="NP_NULL_PARAM_DEREF_ALL_TARGETS_DANGEROUS" level="CRITICAL" enabled="true" />
<coding_rule class="NP_NULL_PARAM_DEREF_NONVIRTUAL" level="CRITICAL" enabled="true" />
<coding_rule class="NP_OPTIONAL_RETURN_NULL" level="MAJOR" enabled="false" />
<coding_rule class="NP_PARAMETER_MUST_BE_NONNULL_BUT_MARKED_AS_NULLABLE" level="CRITICAL" enabled="true" />
<coding_rule class="NP_STORE_INTO_NONNULL_FIELD" level="CRITICAL" enabled="true" />
<coding_rule class="NP_SYNC_AND_NULL_CHECK_FIELD" level="MAJOR" enabled="true" />
<coding_rule class="NP_TOSTRING_COULD_RETURN_NULL" level="CRITICAL" enabled="true" />
<coding_rule class="NP_UNWRITTEN_FIELD" level="MAJOR" enabled="false" />
<coding_rule class="NP_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD" level="CRITICAL" enabled="false" />
<coding_rule class="NS_DANGEROUS_NON_SHORT_CIRCUIT" level="CRITICAL" enabled="true" />
<coding_rule class="NS_NON_SHORT_CIRCUIT" level="MAJOR" enabled="true" />
<coding_rule class="OBL_UNSATISFIED_OBLIGATION" level="CRITICAL" enabled="false" />
<coding_rule class="OBL_UNSATISFIED_OBLIGATION_EXCEPTION_EDGE" level="CRITICAL" enabled="false" />
<coding_rule class="ODR_OPEN_DATABASE_RESOURCE" level="CRITICAL" enabled="true" />
<coding_rule class="ODR_OPEN_DATABASE_RESOURCE_EXCEPTION_PATH" level="CRITICAL" enabled="true" />
<coding_rule class="OS_OPEN_STREAM" level="CRITICAL" enabled="true" />
<coding_rule class="OS_OPEN_STREAM_EXCEPTION_PATH" level="CRITICAL" enabled="true" />
<coding_rule class="PS_PUBLIC_SEMAPHORES" level="CRITICAL" enabled="true" />
<coding_rule class="PT_ABSOLUTE_PATH_TRAVERSAL" level="CRITICAL" enabled="false" />
<coding_rule class="PT_RELATIVE_PATH_TRAVERSAL" level="CRITICAL" enabled="false" />
<coding_rule class="PZLA_PREFER_ZERO_LENGTH_ARRAYS" level="MAJOR" enabled="true" />
<coding_rule class="PZ_DONT_REUSE_ENTRY_OBJECTS_IN_ITERATORS" level="CRITICAL" enabled="false" />
<coding_rule class="QBA_QUESTIONABLE_BOOLEAN_ASSIGNMENT" level="CRITICAL" enabled="true" />
<coding_rule class="QF_QUESTIONABLE_FOR_LOOP" level="CRITICAL" enabled="true" />
<coding_rule class="RANGE_ARRAY_INDEX" level="MAJOR" enabled="false" />
<coding_rule class="RANGE_ARRAY_LENGTH" level="MAJOR" enabled="false" />
<coding_rule class="RANGE_ARRAY_OFFSET" level="MAJOR" enabled="false" />
<coding_rule class="RANGE_STRING_INDEX" level="MAJOR" enabled="false" />
<coding_rule class="RCN_REDUNDANT_COMPARISON_OF_NULL_AND_NONNULL_VALUE" level="CRITICAL" enabled="true" />
<coding_rule class="RCN_REDUNDANT_COMPARISON_TWO_NULL_VALUES" level="CRITICAL" enabled="true" />
<coding_rule class="RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE" level="CRITICAL" enabled="true" />
<coding_rule class="RCN_REDUNDANT_NULLCHECK_OF_NULL_VALUE" level="CRITICAL" enabled="true" />
<coding_rule class="RCN_REDUNDANT_NULLCHECK_WOULD_HAVE_BEEN_A_NPE" level="CRITICAL" enabled="true" />
<coding_rule class="RC_REF_COMPARISON" level="CRITICAL" enabled="true" />
<coding_rule class="RC_REF_COMPARISON_BAD_PRACTICE" level="MAJOR" enabled="false" />
<coding_rule class="RC_REF_COMPARISON_BAD_PRACTICE_BOOLEAN" level="MAJOR" enabled="false" />
<coding_rule class="REC_CATCH_EXCEPTION" level="MAJOR" enabled="true" />
<coding_rule class="RE_BAD_SYNTAX_FOR_REGULAR_EXPRESSION" level="CRITICAL" enabled="true" />
<coding_rule class="RE_CANT_USE_FILE_SEPARATOR_AS_REGULAR_EXPRESSION" level="CRITICAL" enabled="true" />
<coding_rule class="RE_POSSIBLE_UNINTENDED_PATTERN" level="CRITICAL" enabled="true" />
<coding_rule class="RI_REDUNDANT_INTERFACES" level="MAJOR" enabled="true" />
<coding_rule class="RR_NOT_CHECKED" level="MAJOR" enabled="true" />
<coding_rule class="RS_READOBJECT_SYNC" level="CRITICAL" enabled="true" />
<coding_rule class="RU_INVOKE_RUN" level="MAJOR" enabled="true" />
<coding_rule class="RV_01_TO_INT" level="MAJOR" enabled="true" />
<coding_rule class="RV_ABSOLUTE_VALUE_OF_HASHCODE" level="CRITICAL" enabled="true" />
<coding_rule class="RV_ABSOLUTE_VALUE_OF_RANDOM_INT" level="CRITICAL" enabled="true" />
<coding_rule class="RV_CHECK_COMPARETO_FOR_SPECIFIC_RETURN_VALUE" level="CRITICAL" enabled="false" />
<coding_rule class="RV_CHECK_FOR_POSITIVE_INDEXOF" level="MINOR" enabled="true" />
<coding_rule class="RV_DONT_JUST_NULL_CHECK_READLINE" level="MAJOR" enabled="true" />
<coding_rule class="RV_EXCEPTION_NOT_THROWN" level="CRITICAL" enabled="true" />
<coding_rule class="RV_NEGATING_RESULT_OF_COMPARETO" level="CRITICAL" enabled="false" />
<coding_rule class="RV_REM_OF_HASHCODE" level="CRITICAL" enabled="true" />
<coding_rule class="RV_REM_OF_RANDOM_INT" level="CRITICAL" enabled="true" />
<coding_rule class="RV_RETURN_VALUE_IGNORED" level="MINOR" enabled="true" />
<coding_rule class="RV_RETURN_VALUE_IGNORED_BAD_PRACTICE" level="MAJOR" enabled="true" />
<coding_rule class="RV_RETURN_VALUE_IGNORED_INFERRED" level="CRITICAL" enabled="false" />
<coding_rule class="RV_RETURN_VALUE_IGNORED_NO_SIDE_EFFECT" level="MAJOR" enabled="false" />
<coding_rule class="RV_RETURN_VALUE_OF_PUTIFABSENT_IGNORED" level="MAJOR" enabled="false" />
<coding_rule class="RpC_REPEATED_CONDITIONAL_TEST" level="MAJOR" enabled="true" />
<coding_rule class="SA_FIELD_DOUBLE_ASSIGNMENT" level="CRITICAL" enabled="true" />
<coding_rule class="SA_FIELD_SELF_ASSIGNMENT" level="CRITICAL" enabled="true" />
<coding_rule class="SA_FIELD_SELF_COMPARISON" level="CRITICAL" enabled="true" />
<coding_rule class="SA_FIELD_SELF_COMPUTATION" level="CRITICAL" enabled="true" />
<coding_rule class="SA_LOCAL_DOUBLE_ASSIGNMENT" level="CRITICAL" enabled="true" />
<coding_rule class="SA_LOCAL_SELF_ASSIGNMENT" level="CRITICAL" enabled="true" />
<coding_rule class="SA_LOCAL_SELF_ASSIGNMENT_INSTEAD_OF_FIELD" level="CRITICAL" enabled="false" />
<coding_rule class="SA_LOCAL_SELF_COMPARISON" level="CRITICAL" enabled="true" />
<coding_rule class="SA_LOCAL_SELF_COMPUTATION" level="CRITICAL" enabled="true" />
<coding_rule class="SBSC_USE_STRINGBUFFER_CONCATENATION" level="CRITICAL" enabled="true" />
<coding_rule class="SC_START_IN_CTOR" level="CRITICAL" enabled="true" />
<coding_rule class="SE_BAD_FIELD" level="MINOR" enabled="false" />
<coding_rule class="SE_BAD_FIELD_INNER_CLASS" level="MINOR" enabled="true" />
<coding_rule class="SE_BAD_FIELD_STORE" level="CRITICAL" enabled="true" />
<coding_rule class="SE_COMPARATOR_SHOULD_BE_SERIALIZABLE" level="MAJOR" enabled="true" />
<coding_rule class="SE_INNER_CLASS" level="MAJOR" enabled="true" />
<coding_rule class="SE_METHOD_MUST_BE_PRIVATE" level="MAJOR" enabled="true" />
<coding_rule class="SE_NONFINAL_SERIALVERSIONID" level="CRITICAL" enabled="true" />
<coding_rule class="SE_NONLONG_SERIALVERSIONID" level="MAJOR" enabled="true" />
<coding_rule class="SE_NONSTATIC_SERIALVERSIONID" level="MAJOR" enabled="true" />
<coding_rule class="SE_NO_SERIALVERSIONID" level="MAJOR" enabled="true" />
<coding_rule class="SE_NO_SUITABLE_CONSTRUCTOR" level="MAJOR" enabled="true" />
<coding_rule class="SE_NO_SUITABLE_CONSTRUCTOR_FOR_EXTERNALIZATION" level="MAJOR" enabled="true" />
<coding_rule class="SE_PRIVATE_READ_RESOLVE_NOT_INHERITED" level="MAJOR" enabled="true" />
<coding_rule class="SE_READ_RESOLVE_IS_STATIC" level="MAJOR" enabled="true" />
<coding_rule class="SE_READ_RESOLVE_MUST_RETURN_OBJECT" level="MAJOR" enabled="true" />
<coding_rule class="SE_TRANSIENT_FIELD_NOT_RESTORED" level="MAJOR" enabled="true" />
<coding_rule class="SE_TRANSIENT_FIELD_OF_NONSERIALIZABLE_CLASS" level="MAJOR" enabled="true" />
<coding_rule class="SF_DEAD_STORE_DUE_TO_SWITCH_FALLTHROUGH" level="MAJOR" enabled="false" />
<coding_rule class="SF_DEAD_STORE_DUE_TO_SWITCH_FALLTHROUGH_TO_THROW" level="MAJOR" enabled="false" />
<coding_rule class="SF_SWITCH_FALLTHROUGH" level="MAJOR" enabled="false" />
<coding_rule class="SF_SWITCH_NO_DEFAULT" level="MAJOR" enabled="false" />
<coding_rule class="SIC_INNER_SHOULD_BE_STATIC" level="MAJOR" enabled="true" />
<coding_rule class="SIC_INNER_SHOULD_BE_STATIC_ANON" level="MAJOR" enabled="true" />
<coding_rule class="SIC_INNER_SHOULD_BE_STATIC_NEEDS_THIS" level="MAJOR" enabled="true" />
<coding_rule class="SIC_THREADLOCAL_DEADLY_EMBRACE" level="MAJOR" enabled="false" />
<coding_rule class="SIO_SUPERFLUOUS_INSTANCEOF" level="CRITICAL" enabled="true" />
<coding_rule class="SI_INSTANCE_BEFORE_FINALS_ASSIGNED" level="CRITICAL" enabled="true" />
<coding_rule class="SP_SPIN_ON_FIELD" level="MAJOR" enabled="true" />
<coding_rule class="SQL_BAD_PREPARED_STATEMENT_ACCESS" level="CRITICAL" enabled="true" />
<coding_rule class="SQL_BAD_RESULTSET_ACCESS" level="CRITICAL" enabled="true" />
<coding_rule class="SQL_NONCONSTANT_STRING_PASSED_TO_EXECUTE" level="CRITICAL" enabled="true" />
<coding_rule class="SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING" level="CRITICAL" enabled="true" />
<coding_rule class="SR_NOT_CHECKED" level="MAJOR" enabled="true" />
<coding_rule class="SS_SHOULD_BE_STATIC" level="MAJOR" enabled="true" />
<coding_rule class="STCAL_INVOKE_ON_STATIC_CALENDAR_INSTANCE" level="CRITICAL" enabled="true" />
<coding_rule class="STCAL_INVOKE_ON_STATIC_DATE_FORMAT_INSTANCE" level="CRITICAL" enabled="true" />
<coding_rule class="STCAL_STATIC_CALENDAR_INSTANCE" level="CRITICAL" enabled="true" />
<coding_rule class="STCAL_STATIC_SIMPLE_DATE_FORMAT_INSTANCE" level="CRITICAL" enabled="true" />
<coding_rule class="STI_INTERRUPTED_ON_CURRENTTHREAD" level="CRITICAL" enabled="true" />
<coding_rule class="STI_INTERRUPTED_ON_UNKNOWNTHREAD" level="CRITICAL" enabled="true" />
<coding_rule class="ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD" level="CRITICAL" enabled="true" />
<coding_rule class="SWL_SLEEP_WITH_LOCK_HELD" level="CRITICAL" enabled="true" />
<coding_rule class="SW_SWING_METHODS_INVOKED_IN_SWING_THREAD" level="MAJOR" enabled="true" />
<coding_rule class="TLW_TWO_LOCK_WAIT" level="MAJOR" enabled="true" />
<coding_rule class="TQ_ALWAYS_VALUE_USED_WHERE_NEVER_REQUIRED" level="CRITICAL" enabled="true" />
<coding_rule class="TQ_COMPARING_VALUES_WITH_INCOMPATIBLE_TYPE_QUALIFIERS" level="CRITICAL" enabled="false" />
<coding_rule class="TQ_EXPLICIT_UNKNOWN_SOURCE_VALUE_REACHES_ALWAYS_SINK" level="CRITICAL" enabled="true" />
<coding_rule class="TQ_EXPLICIT_UNKNOWN_SOURCE_VALUE_REACHES_NEVER_SINK" level="CRITICAL" enabled="true" />
<coding_rule class="TQ_MAYBE_SOURCE_VALUE_REACHES_ALWAYS_SINK" level="CRITICAL" enabled="true" />
<coding_rule class="TQ_MAYBE_SOURCE_VALUE_REACHES_NEVER_SINK" level="CRITICAL" enabled="true" />
<coding_rule class="TQ_NEVER_VALUE_USED_WHERE_ALWAYS_REQUIRED" level="CRITICAL" enabled="true" />
<coding_rule class="TQ_UNKNOWN_VALUE_USED_WHERE_ALWAYS_STRICTLY_REQUIRED" level="CRITICAL" enabled="false" />
<coding_rule class="UCF_USELESS_CONTROL_FLOW" level="CRITICAL" enabled="true" />
<coding_rule class="UCF_USELESS_CONTROL_FLOW_NEXT_LINE" level="CRITICAL" enabled="true" />
<coding_rule class="UC_USELESS_CONDITION" level="MAJOR" enabled="false" />
<coding_rule class="UC_USELESS_CONDITION_TYPE" level="MAJOR" enabled="false" />
<coding_rule class="UC_USELESS_OBJECT" level="MAJOR" enabled="false" />
<coding_rule class="UC_USELESS_OBJECT_STACK" level="MAJOR" enabled="false" />
<coding_rule class="UC_USELESS_VOID_METHOD" level="MAJOR" enabled="false" />
<coding_rule class="UG_SYNC_SET_UNSYNC_GET" level="MAJOR" enabled="true" />
<coding_rule class="UI_INHERITANCE_UNSAFE_GETRESOURCE" level="MAJOR" enabled="true" />
<coding_rule class="UL_UNRELEASED_LOCK" level="CRITICAL" enabled="true" />
<coding_rule class="UL_UNRELEASED_LOCK_EXCEPTION_PATH" level="CRITICAL" enabled="true" />
<coding_rule class="UMAC_UNCALLABLE_METHOD_OF_ANONYMOUS_CLASS" level="CRITICAL" enabled="true" />
<coding_rule class="UM_UNNECESSARY_MATH" level="CRITICAL" enabled="true" />
<coding_rule class="UPM_UNCALLED_PRIVATE_METHOD" level="CRITICAL" enabled="true" />
<coding_rule class="URF_UNREAD_FIELD" level="MAJOR" enabled="true" />
<coding_rule class="URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD" level="CRITICAL" enabled="false" />
<coding_rule class="UR_UNINIT_READ" level="MAJOR" enabled="true" />
<coding_rule class="UR_UNINIT_READ_CALLED_FROM_SUPER_CONSTRUCTOR" level="MAJOR" enabled="false" />
<coding_rule class="USM_USELESS_ABSTRACT_METHOD" level="MAJOR" enabled="false" />
<coding_rule class="USM_USELESS_SUBCLASS_METHOD" level="MAJOR" enabled="false" />
<coding_rule class="UUF_UNUSED_FIELD" level="MAJOR" enabled="true" />
<coding_rule class="UUF_UNUSED_PUBLIC_OR_PROTECTED_FIELD" level="CRITICAL" enabled="false" />
<coding_rule class="UWF_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR" level="MAJOR" enabled="false" />
<coding_rule class="UWF_NULL_FIELD" level="CRITICAL" enabled="true" />
<coding_rule class="UWF_UNWRITTEN_FIELD" level="MAJOR" enabled="false" />
<coding_rule class="UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD" level="CRITICAL" enabled="false" />
<coding_rule class="UW_UNCOND_WAIT" level="MAJOR" enabled="true" />
<coding_rule class="VA_FORMAT_STRING_BAD_ARGUMENT" level="CRITICAL" enabled="true" />
<coding_rule class="VA_FORMAT_STRING_BAD_CONVERSION" level="CRITICAL" enabled="true" />
<coding_rule class="VA_FORMAT_STRING_BAD_CONVERSION_FROM_ARRAY" level="MAJOR" enabled="true" />
<coding_rule class="VA_FORMAT_STRING_BAD_CONVERSION_TO_BOOLEAN" level="MAJOR" enabled="true" />
<coding_rule class="VA_FORMAT_STRING_EXPECTED_MESSAGE_FORMAT_SUPPLIED" level="MAJOR" enabled="false" />
<coding_rule class="VA_FORMAT_STRING_EXTRA_ARGUMENTS_PASSED" level="MAJOR" enabled="true" />
<coding_rule class="VA_FORMAT_STRING_ILLEGAL" level="CRITICAL" enabled="true" />
<coding_rule class="VA_FORMAT_STRING_MISSING_ARGUMENT" level="CRITICAL" enabled="true" />
<coding_rule class="VA_FORMAT_STRING_NO_PREVIOUS_ARGUMENT" level="CRITICAL" enabled="true" />
<coding_rule class="VA_FORMAT_STRING_USES_NEWLINE" level="CRITICAL" enabled="false" />
<coding_rule class="VA_PRIMITIVE_ARRAY_PASSED_TO_OBJECT_VARARG" level="CRITICAL" enabled="true" />
<coding_rule class="VO_VOLATILE_INCREMENT" level="CRITICAL" enabled="false" />
<coding_rule class="VO_VOLATILE_REFERENCE_TO_ARRAY" level="MAJOR" enabled="true" />
<coding_rule class="VR_UNRESOLVABLE_REFERENCE" level="MAJOR" enabled="false" />
<coding_rule class="WA_AWAIT_NOT_IN_LOOP" level="CRITICAL" enabled="true" />
<coding_rule class="WA_NOT_IN_LOOP" level="CRITICAL" enabled="true" />
<coding_rule class="WL_USING_GETCLASS_RATHER_THAN_CLASS_LITERAL" level="CRITICAL" enabled="true" />
<coding_rule class="WMI_WRONG_MAP_ITERATOR" level="CRITICAL" enabled="true" />
<coding_rule class="WS_WRITEOBJECT_SYNC" level="CRITICAL" enabled="true" />
<coding_rule class="XFB_XML_FACTORY_BYPASS" level="CRITICAL" enabled="true" />
<coding_rule class="XSS_REQUEST_PARAMETER_TO_JSP_WRITER" level="CRITICAL" enabled="true" />
<coding_rule class="XSS_REQUEST_PARAMETER_TO_SEND_ERROR" level="CRITICAL" enabled="true" />
<coding_rule class="XSS_REQUEST_PARAMETER_TO_SERVLET_WRITER" level="CRITICAL" enabled="true" />
</profile>
</profiles>
<list size="0" />
</component>
</project>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
</set>
</option>
</component>
</project>

124
models-integration/.idea/uiDesigner.xml generated Normal file
View File

@@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Palette2">
<group name="Swing">
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
</item>
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
</item>
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.svg" removable="false" auto-create-binding="false" can-attach-label="true">
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
</item>
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
<initial-values>
<property name="text" value="Button" />
</initial-values>
</item>
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="RadioButton" />
</initial-values>
</item>
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="CheckBox" />
</initial-values>
</item>
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
<initial-values>
<property name="text" value="Label" />
</initial-values>
</item>
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
</item>
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
</item>
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
<preferred-size width="-1" height="20" />
</default-constraints>
</item>
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
</item>
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
</item>
</group>
</component>
</project>

View File

@@ -0,0 +1,66 @@
# 项目对话规则
## 1. 项目上下文约束
- 所有对话必须围绕当前销帮帮Java API集成项目展开不回应无关技术或业务问题
- 始终默认使用销帮帮API最新稳定版本当前为v2如需使用旧版本需明确标注
- 涉及代码实现时必须遵循项目使用的Java版本JDK 8+)及编码规范
## 2. 对话交互规范
- 对用户需求先进行业务场景识别,再提供技术实现方案,避免直接跳跃到代码层面
- 当用户需求不明确时,按以下优先级提问澄清:
1. 具体业务操作目标(如"是需要查询客户还是创建客户?"
2. 必要参数信息(如"请提供客户的所属部门ID"
3. 特殊处理要求(如"是否需要同步更新关联的商机信息?"
- 技术方案回复需包含业务对应API映射、核心参数说明、调用注意事项三部分
## 3. API调用规范
- 所有API调用示例必须包含完整请求结构
- 请求头含Authorization、Content-Type
- 请求方法与路径
- 请求体JSON格式标注必填字段
- 涉及认证相关操作时,必须提醒用户:
- token的有效期默认2小时
- 权限范围限制
- 安全存储要求
- 批量操作API调用必须提示分页处理建议避免单次请求数据量过大
## 4. 代码实现规范
- 提供的Java代码必须
- 包含必要的异常处理
- 使用项目指定的HTTP客户端RestTemplate
- 遵循阿里巴巴Java开发手册规范
- 包含关键业务注释(无需注释基础语法)
- 涉及实体类定义时需使用Lombok注解简化代码
- 工具类调用需优先使用项目已封装的销帮帮API工具包
## 5. 数据安全规范
- 对话中禁止出现真实的敏感数据(如客户手机号、身份证号等)
- 示例数据需使用明显的测试标识如手机号使用13800000000
- 涉及数据权限问题时,必须提示用户检查当前账号的角色权限配置
## 6. 错误处理规范
- 收到API错误响应时需按以下格式回复
1. 错误码与描述直接引用销帮帮API文档
2. 可能的原因分析列出2-3个最可能的原因
3. 具体解决方案(含操作步骤)
- 对于未明确的错误,需建议用户提供完整的请求日志以便进一步分析
## 7. 版本兼容说明
- 当API版本存在差异时需明确标注不同版本的区别
- 字段变更(新增/移除/重命名)
- 路径变更
- 功能逻辑调整
- 推荐使用版本兼容的实现方式,避免锁定特定版本
## 8. 文档引用规范
- 涉及API细节时需指明参考销帮帮开放平台文档的具体章节
- 重要概念需提供官方定义,再补充项目实践说明
- 第三方工具使用需参考其最新官方文档

View File

@@ -0,0 +1,4 @@
{
"java.compile.nullAnalysis.mode": "automatic",
"java.configuration.updateBuildConfiguration": "interactive"
}

Binary file not shown.

View File

@@ -0,0 +1 @@
ssmgcqtsrbfxtc7jwtlnpxpwj4!Å]ürì”7§Kc©<.[ˆclasses·¿Â©Õ'î<>R36ó ‡4ˆsourcesr~öûqrRæÝÔ¤ƒÃ>

View File

@@ -0,0 +1,361 @@
package org.gradle.accessors.dm;
import org.gradle.api.NonNullApi;
import org.gradle.api.artifacts.MinimalExternalModuleDependency;
import org.gradle.plugin.use.PluginDependency;
import org.gradle.api.artifacts.ExternalModuleDependencyBundle;
import org.gradle.api.artifacts.MutableVersionConstraint;
import org.gradle.api.provider.Provider;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.provider.ProviderFactory;
import org.gradle.api.internal.catalog.AbstractExternalDependencyFactory;
import org.gradle.api.internal.catalog.DefaultVersionCatalog;
import java.util.Map;
import org.gradle.api.internal.attributes.ImmutableAttributesFactory;
import org.gradle.api.internal.artifacts.dsl.CapabilityNotationParser;
import javax.inject.Inject;
/**
* A catalog of dependencies accessible via the {@code libs} extension.
*/
@NonNullApi
public class LibrariesForLibs extends AbstractExternalDependencyFactory {
private final AbstractExternalDependencyFactory owner = this;
private final CozeLibraryAccessors laccForCozeLibraryAccessors = new CozeLibraryAccessors(owner);
private final JacksonLibraryAccessors laccForJacksonLibraryAccessors = new JacksonLibraryAccessors(owner);
private final JunitLibraryAccessors laccForJunitLibraryAccessors = new JunitLibraryAccessors(owner);
private final SpringLibraryAccessors laccForSpringLibraryAccessors = new SpringLibraryAccessors(owner);
private final VersionAccessors vaccForVersionAccessors = new VersionAccessors(providers, config);
private final BundleAccessors baccForBundleAccessors = new BundleAccessors(objects, providers, config, attributesFactory, capabilityNotationParser);
private final PluginAccessors paccForPluginAccessors = new PluginAccessors(providers, config);
@Inject
public LibrariesForLibs(DefaultVersionCatalog config, ProviderFactory providers, ObjectFactory objects, ImmutableAttributesFactory attributesFactory, CapabilityNotationParser capabilityNotationParser) {
super(config, providers, objects, attributesFactory, capabilityNotationParser);
}
/**
* Dependency provider for <b>lombok</b> with <b>org.projectlombok:lombok</b> coordinates and
* with <b>no version specified</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> getLombok() {
return create("lombok");
}
/**
* Dependency provider for <b>snakeyaml</b> with <b>org.yaml:snakeyaml</b> coordinates and
* with version reference <b>snakeyaml</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> getSnakeyaml() {
return create("snakeyaml");
}
/**
* Group of libraries at <b>coze</b>
*/
public CozeLibraryAccessors getCoze() {
return laccForCozeLibraryAccessors;
}
/**
* Group of libraries at <b>jackson</b>
*/
public JacksonLibraryAccessors getJackson() {
return laccForJacksonLibraryAccessors;
}
/**
* Group of libraries at <b>junit</b>
*/
public JunitLibraryAccessors getJunit() {
return laccForJunitLibraryAccessors;
}
/**
* Group of libraries at <b>spring</b>
*/
public SpringLibraryAccessors getSpring() {
return laccForSpringLibraryAccessors;
}
/**
* Group of versions at <b>versions</b>
*/
public VersionAccessors getVersions() {
return vaccForVersionAccessors;
}
/**
* Group of bundles at <b>bundles</b>
*/
public BundleAccessors getBundles() {
return baccForBundleAccessors;
}
/**
* Group of plugins at <b>plugins</b>
*/
public PluginAccessors getPlugins() {
return paccForPluginAccessors;
}
public static class CozeLibraryAccessors extends SubDependencyFactory {
public CozeLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
/**
* Dependency provider for <b>api</b> with <b>com.coze:coze-api</b> coordinates and
* with version reference <b>coze</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> getApi() {
return create("coze.api");
}
}
public static class JacksonLibraryAccessors extends SubDependencyFactory {
public JacksonLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
/**
* Dependency provider for <b>databind</b> with <b>com.fasterxml.jackson.core:jackson-databind</b> coordinates and
* with <b>no version specified</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> getDatabind() {
return create("jackson.databind");
}
/**
* Dependency provider for <b>yaml</b> with <b>com.fasterxml.jackson.dataformat:jackson-dataformat-yaml</b> coordinates and
* with <b>no version specified</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> getYaml() {
return create("jackson.yaml");
}
}
public static class JunitLibraryAccessors extends SubDependencyFactory {
public JunitLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
/**
* Dependency provider for <b>bom</b> with <b>org.junit:junit-bom</b> coordinates and
* with version reference <b>junit</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> getBom() {
return create("junit.bom");
}
/**
* Dependency provider for <b>jupiter</b> with <b>org.junit.jupiter:junit-jupiter</b> coordinates and
* with <b>no version specified</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> getJupiter() {
return create("junit.jupiter");
}
}
public static class SpringLibraryAccessors extends SubDependencyFactory {
private final SpringBootLibraryAccessors laccForSpringBootLibraryAccessors = new SpringBootLibraryAccessors(owner);
public SpringLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
/**
* Group of libraries at <b>spring.boot</b>
*/
public SpringBootLibraryAccessors getBoot() {
return laccForSpringBootLibraryAccessors;
}
}
public static class SpringBootLibraryAccessors extends SubDependencyFactory {
private final SpringBootStarterLibraryAccessors laccForSpringBootStarterLibraryAccessors = new SpringBootStarterLibraryAccessors(owner);
public SpringBootLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
/**
* Dependency provider for <b>configuration</b> with <b>org.springframework.boot:spring-boot-configuration-processor</b> coordinates and
* with <b>no version specified</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> getConfiguration() {
return create("spring.boot.configuration");
}
/**
* Dependency provider for <b>test</b> with <b>org.springframework.boot:spring-boot-starter-test</b> coordinates and
* with <b>no version specified</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> getTest() {
return create("spring.boot.test");
}
/**
* Group of libraries at <b>spring.boot.starter</b>
*/
public SpringBootStarterLibraryAccessors getStarter() {
return laccForSpringBootStarterLibraryAccessors;
}
}
public static class SpringBootStarterLibraryAccessors extends SubDependencyFactory {
public SpringBootStarterLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
/**
* Dependency provider for <b>actuator</b> with <b>org.springframework.boot:spring-boot-starter-actuator</b> coordinates and
* with <b>no version specified</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> getActuator() {
return create("spring.boot.starter.actuator");
}
/**
* Dependency provider for <b>web</b> with <b>org.springframework.boot:spring-boot-starter-web</b> coordinates and
* with <b>no version specified</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> getWeb() {
return create("spring.boot.starter.web");
}
}
public static class VersionAccessors extends VersionFactory {
private final SpringVersionAccessors vaccForSpringVersionAccessors = new SpringVersionAccessors(providers, config);
public VersionAccessors(ProviderFactory providers, DefaultVersionCatalog config) { super(providers, config); }
/**
* Version alias <b>coze</b> with value <b>0.2.0</b>
* <p>
* If the version is a rich version and cannot be represented as a
* single version string, an empty string is returned.
* <p>
* This version was declared in catalog libs.versions.toml
*/
public Provider<String> getCoze() { return getVersion("coze"); }
/**
* Version alias <b>junit</b> with value <b>5.10.0</b>
* <p>
* If the version is a rich version and cannot be represented as a
* single version string, an empty string is returned.
* <p>
* This version was declared in catalog libs.versions.toml
*/
public Provider<String> getJunit() { return getVersion("junit"); }
/**
* Version alias <b>snakeyaml</b> with value <b>2.0</b>
* <p>
* If the version is a rich version and cannot be represented as a
* single version string, an empty string is returned.
* <p>
* This version was declared in catalog libs.versions.toml
*/
public Provider<String> getSnakeyaml() { return getVersion("snakeyaml"); }
/**
* Group of versions at <b>versions.spring</b>
*/
public SpringVersionAccessors getSpring() {
return vaccForSpringVersionAccessors;
}
}
public static class SpringVersionAccessors extends VersionFactory {
public SpringVersionAccessors(ProviderFactory providers, DefaultVersionCatalog config) { super(providers, config); }
/**
* Version alias <b>spring.boot</b> with value <b>3.2.3</b>
* <p>
* If the version is a rich version and cannot be represented as a
* single version string, an empty string is returned.
* <p>
* This version was declared in catalog libs.versions.toml
*/
public Provider<String> getBoot() { return getVersion("spring.boot"); }
/**
* Version alias <b>spring.dependency</b> with value <b>1.1.4</b>
* <p>
* If the version is a rich version and cannot be represented as a
* single version string, an empty string is returned.
* <p>
* This version was declared in catalog libs.versions.toml
*/
public Provider<String> getDependency() { return getVersion("spring.dependency"); }
}
public static class BundleAccessors extends BundleFactory {
public BundleAccessors(ObjectFactory objects, ProviderFactory providers, DefaultVersionCatalog config, ImmutableAttributesFactory attributesFactory, CapabilityNotationParser capabilityNotationParser) { super(objects, providers, config, attributesFactory, capabilityNotationParser); }
}
public static class PluginAccessors extends PluginFactory {
private final SpringPluginAccessors paccForSpringPluginAccessors = new SpringPluginAccessors(providers, config);
public PluginAccessors(ProviderFactory providers, DefaultVersionCatalog config) { super(providers, config); }
/**
* Group of plugins at <b>plugins.spring</b>
*/
public SpringPluginAccessors getSpring() {
return paccForSpringPluginAccessors;
}
}
public static class SpringPluginAccessors extends PluginFactory {
public SpringPluginAccessors(ProviderFactory providers, DefaultVersionCatalog config) { super(providers, config); }
/**
* Plugin provider for <b>spring.boot</b> with plugin id <b>org.springframework.boot</b> and
* with version reference <b>spring.boot</b>
* <p>
* This plugin was declared in catalog libs.versions.toml
*/
public Provider<PluginDependency> getBoot() { return createPlugin("spring.boot"); }
/**
* Plugin provider for <b>spring.dependency</b> with plugin id <b>io.spring.dependency-management</b> and
* with version reference <b>spring.dependency</b>
* <p>
* This plugin was declared in catalog libs.versions.toml
*/
public Provider<PluginDependency> getDependency() { return createPlugin("spring.dependency"); }
}
}

View File

@@ -0,0 +1,461 @@
package org.gradle.accessors.dm;
import org.gradle.api.NonNullApi;
import org.gradle.api.artifacts.MinimalExternalModuleDependency;
import org.gradle.plugin.use.PluginDependency;
import org.gradle.api.artifacts.ExternalModuleDependencyBundle;
import org.gradle.api.artifacts.MutableVersionConstraint;
import org.gradle.api.provider.Provider;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.provider.ProviderFactory;
import org.gradle.api.internal.catalog.AbstractExternalDependencyFactory;
import org.gradle.api.internal.catalog.DefaultVersionCatalog;
import java.util.Map;
import org.gradle.api.internal.attributes.ImmutableAttributesFactory;
import org.gradle.api.internal.artifacts.dsl.CapabilityNotationParser;
import javax.inject.Inject;
/**
* A catalog of dependencies accessible via the {@code libs} extension.
*/
@NonNullApi
public class LibrariesForLibsInPluginsBlock extends AbstractExternalDependencyFactory {
private final AbstractExternalDependencyFactory owner = this;
private final CozeLibraryAccessors laccForCozeLibraryAccessors = new CozeLibraryAccessors(owner);
private final JacksonLibraryAccessors laccForJacksonLibraryAccessors = new JacksonLibraryAccessors(owner);
private final JunitLibraryAccessors laccForJunitLibraryAccessors = new JunitLibraryAccessors(owner);
private final SpringLibraryAccessors laccForSpringLibraryAccessors = new SpringLibraryAccessors(owner);
private final VersionAccessors vaccForVersionAccessors = new VersionAccessors(providers, config);
private final BundleAccessors baccForBundleAccessors = new BundleAccessors(objects, providers, config, attributesFactory, capabilityNotationParser);
private final PluginAccessors paccForPluginAccessors = new PluginAccessors(providers, config);
@Inject
public LibrariesForLibsInPluginsBlock(DefaultVersionCatalog config, ProviderFactory providers, ObjectFactory objects, ImmutableAttributesFactory attributesFactory, CapabilityNotationParser capabilityNotationParser) {
super(config, providers, objects, attributesFactory, capabilityNotationParser);
}
/**
* Dependency provider for <b>lombok</b> with <b>org.projectlombok:lombok</b> coordinates and
* with <b>no version specified</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*
* @deprecated Will be removed in Gradle 9.0.
*/
@Deprecated
public Provider<MinimalExternalModuleDependency> getLombok() {
org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser();
return create("lombok");
}
/**
* Dependency provider for <b>snakeyaml</b> with <b>org.yaml:snakeyaml</b> coordinates and
* with version reference <b>snakeyaml</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*
* @deprecated Will be removed in Gradle 9.0.
*/
@Deprecated
public Provider<MinimalExternalModuleDependency> getSnakeyaml() {
org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser();
return create("snakeyaml");
}
/**
* Group of libraries at <b>coze</b>
*
* @deprecated Will be removed in Gradle 9.0.
*/
@Deprecated
public CozeLibraryAccessors getCoze() {
org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser();
return laccForCozeLibraryAccessors;
}
/**
* Group of libraries at <b>jackson</b>
*
* @deprecated Will be removed in Gradle 9.0.
*/
@Deprecated
public JacksonLibraryAccessors getJackson() {
org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser();
return laccForJacksonLibraryAccessors;
}
/**
* Group of libraries at <b>junit</b>
*
* @deprecated Will be removed in Gradle 9.0.
*/
@Deprecated
public JunitLibraryAccessors getJunit() {
org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser();
return laccForJunitLibraryAccessors;
}
/**
* Group of libraries at <b>spring</b>
*
* @deprecated Will be removed in Gradle 9.0.
*/
@Deprecated
public SpringLibraryAccessors getSpring() {
org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser();
return laccForSpringLibraryAccessors;
}
/**
* Group of versions at <b>versions</b>
*/
public VersionAccessors getVersions() {
return vaccForVersionAccessors;
}
/**
* Group of bundles at <b>bundles</b>
*
* @deprecated Will be removed in Gradle 9.0.
*/
@Deprecated
public BundleAccessors getBundles() {
org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser();
return baccForBundleAccessors;
}
/**
* Group of plugins at <b>plugins</b>
*/
public PluginAccessors getPlugins() {
return paccForPluginAccessors;
}
/**
* @deprecated Will be removed in Gradle 9.0.
*/
@Deprecated
public static class CozeLibraryAccessors extends SubDependencyFactory {
public CozeLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
/**
* Dependency provider for <b>api</b> with <b>com.coze:coze-api</b> coordinates and
* with version reference <b>coze</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*
* @deprecated Will be removed in Gradle 9.0.
*/
@Deprecated
public Provider<MinimalExternalModuleDependency> getApi() {
org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser();
return create("coze.api");
}
}
/**
* @deprecated Will be removed in Gradle 9.0.
*/
@Deprecated
public static class JacksonLibraryAccessors extends SubDependencyFactory {
public JacksonLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
/**
* Dependency provider for <b>databind</b> with <b>com.fasterxml.jackson.core:jackson-databind</b> coordinates and
* with <b>no version specified</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*
* @deprecated Will be removed in Gradle 9.0.
*/
@Deprecated
public Provider<MinimalExternalModuleDependency> getDatabind() {
org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser();
return create("jackson.databind");
}
/**
* Dependency provider for <b>yaml</b> with <b>com.fasterxml.jackson.dataformat:jackson-dataformat-yaml</b> coordinates and
* with <b>no version specified</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*
* @deprecated Will be removed in Gradle 9.0.
*/
@Deprecated
public Provider<MinimalExternalModuleDependency> getYaml() {
org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser();
return create("jackson.yaml");
}
}
/**
* @deprecated Will be removed in Gradle 9.0.
*/
@Deprecated
public static class JunitLibraryAccessors extends SubDependencyFactory {
public JunitLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
/**
* Dependency provider for <b>bom</b> with <b>org.junit:junit-bom</b> coordinates and
* with version reference <b>junit</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*
* @deprecated Will be removed in Gradle 9.0.
*/
@Deprecated
public Provider<MinimalExternalModuleDependency> getBom() {
org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser();
return create("junit.bom");
}
/**
* Dependency provider for <b>jupiter</b> with <b>org.junit.jupiter:junit-jupiter</b> coordinates and
* with <b>no version specified</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*
* @deprecated Will be removed in Gradle 9.0.
*/
@Deprecated
public Provider<MinimalExternalModuleDependency> getJupiter() {
org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser();
return create("junit.jupiter");
}
}
/**
* @deprecated Will be removed in Gradle 9.0.
*/
@Deprecated
public static class SpringLibraryAccessors extends SubDependencyFactory {
private final SpringBootLibraryAccessors laccForSpringBootLibraryAccessors = new SpringBootLibraryAccessors(owner);
public SpringLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
/**
* Group of libraries at <b>spring.boot</b>
*
* @deprecated Will be removed in Gradle 9.0.
*/
@Deprecated
public SpringBootLibraryAccessors getBoot() {
org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser();
return laccForSpringBootLibraryAccessors;
}
}
/**
* @deprecated Will be removed in Gradle 9.0.
*/
@Deprecated
public static class SpringBootLibraryAccessors extends SubDependencyFactory {
private final SpringBootStarterLibraryAccessors laccForSpringBootStarterLibraryAccessors = new SpringBootStarterLibraryAccessors(owner);
public SpringBootLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
/**
* Dependency provider for <b>configuration</b> with <b>org.springframework.boot:spring-boot-configuration-processor</b> coordinates and
* with <b>no version specified</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*
* @deprecated Will be removed in Gradle 9.0.
*/
@Deprecated
public Provider<MinimalExternalModuleDependency> getConfiguration() {
org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser();
return create("spring.boot.configuration");
}
/**
* Dependency provider for <b>test</b> with <b>org.springframework.boot:spring-boot-starter-test</b> coordinates and
* with <b>no version specified</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*
* @deprecated Will be removed in Gradle 9.0.
*/
@Deprecated
public Provider<MinimalExternalModuleDependency> getTest() {
org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser();
return create("spring.boot.test");
}
/**
* Group of libraries at <b>spring.boot.starter</b>
*
* @deprecated Will be removed in Gradle 9.0.
*/
@Deprecated
public SpringBootStarterLibraryAccessors getStarter() {
org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser();
return laccForSpringBootStarterLibraryAccessors;
}
}
/**
* @deprecated Will be removed in Gradle 9.0.
*/
@Deprecated
public static class SpringBootStarterLibraryAccessors extends SubDependencyFactory {
public SpringBootStarterLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
/**
* Dependency provider for <b>actuator</b> with <b>org.springframework.boot:spring-boot-starter-actuator</b> coordinates and
* with <b>no version specified</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*
* @deprecated Will be removed in Gradle 9.0.
*/
@Deprecated
public Provider<MinimalExternalModuleDependency> getActuator() {
org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser();
return create("spring.boot.starter.actuator");
}
/**
* Dependency provider for <b>web</b> with <b>org.springframework.boot:spring-boot-starter-web</b> coordinates and
* with <b>no version specified</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*
* @deprecated Will be removed in Gradle 9.0.
*/
@Deprecated
public Provider<MinimalExternalModuleDependency> getWeb() {
org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser();
return create("spring.boot.starter.web");
}
}
public static class VersionAccessors extends VersionFactory {
private final SpringVersionAccessors vaccForSpringVersionAccessors = new SpringVersionAccessors(providers, config);
public VersionAccessors(ProviderFactory providers, DefaultVersionCatalog config) { super(providers, config); }
/**
* Version alias <b>coze</b> with value <b>0.2.0</b>
* <p>
* If the version is a rich version and cannot be represented as a
* single version string, an empty string is returned.
* <p>
* This version was declared in catalog libs.versions.toml
*/
public Provider<String> getCoze() { return getVersion("coze"); }
/**
* Version alias <b>junit</b> with value <b>5.10.0</b>
* <p>
* If the version is a rich version and cannot be represented as a
* single version string, an empty string is returned.
* <p>
* This version was declared in catalog libs.versions.toml
*/
public Provider<String> getJunit() { return getVersion("junit"); }
/**
* Version alias <b>snakeyaml</b> with value <b>2.0</b>
* <p>
* If the version is a rich version and cannot be represented as a
* single version string, an empty string is returned.
* <p>
* This version was declared in catalog libs.versions.toml
*/
public Provider<String> getSnakeyaml() { return getVersion("snakeyaml"); }
/**
* Group of versions at <b>versions.spring</b>
*/
public SpringVersionAccessors getSpring() {
return vaccForSpringVersionAccessors;
}
}
public static class SpringVersionAccessors extends VersionFactory {
public SpringVersionAccessors(ProviderFactory providers, DefaultVersionCatalog config) { super(providers, config); }
/**
* Version alias <b>spring.boot</b> with value <b>3.2.3</b>
* <p>
* If the version is a rich version and cannot be represented as a
* single version string, an empty string is returned.
* <p>
* This version was declared in catalog libs.versions.toml
*/
public Provider<String> getBoot() { return getVersion("spring.boot"); }
/**
* Version alias <b>spring.dependency</b> with value <b>1.1.4</b>
* <p>
* If the version is a rich version and cannot be represented as a
* single version string, an empty string is returned.
* <p>
* This version was declared in catalog libs.versions.toml
*/
public Provider<String> getDependency() { return getVersion("spring.dependency"); }
}
/**
* @deprecated Will be removed in Gradle 9.0.
*/
@Deprecated
public static class BundleAccessors extends BundleFactory {
public BundleAccessors(ObjectFactory objects, ProviderFactory providers, DefaultVersionCatalog config, ImmutableAttributesFactory attributesFactory, CapabilityNotationParser capabilityNotationParser) { super(objects, providers, config, attributesFactory, capabilityNotationParser); }
}
public static class PluginAccessors extends PluginFactory {
private final SpringPluginAccessors paccForSpringPluginAccessors = new SpringPluginAccessors(providers, config);
public PluginAccessors(ProviderFactory providers, DefaultVersionCatalog config) { super(providers, config); }
/**
* Group of plugins at <b>plugins.spring</b>
*/
public SpringPluginAccessors getSpring() {
return paccForSpringPluginAccessors;
}
}
public static class SpringPluginAccessors extends PluginFactory {
public SpringPluginAccessors(ProviderFactory providers, DefaultVersionCatalog config) { super(providers, config); }
/**
* Plugin provider for <b>spring.boot</b> with plugin id <b>org.springframework.boot</b> and
* with version reference <b>spring.boot</b>
* <p>
* This plugin was declared in catalog libs.versions.toml
*/
public Provider<PluginDependency> getBoot() { return createPlugin("spring.boot"); }
/**
* Plugin provider for <b>spring.dependency</b> with plugin id <b>io.spring.dependency-management</b> and
* with version reference <b>spring.dependency</b>
* <p>
* This plugin was declared in catalog libs.versions.toml
*/
public Provider<PluginDependency> getDependency() { return createPlugin("spring.dependency"); }
}
}

View File

@@ -0,0 +1,2 @@
#Sat Jan 10 11:40:33 CST 2026
gradle.version=8.10

View File

@@ -0,0 +1,40 @@
# Coze OAuth Examples
This repository contains examples of different OAuth flows for Coze API authentication.
## Prerequisites
- Java 11 or higher
- Gradle
- A Coze API account with client credentials
## Configuration
Each example requires config file to be set with your Coze API credentials:
### JWT OAuth
### Set Environment Variables
To run the JWT OAuth example, set the following config file:
The configuration file should be a JSON file, named coze_oauth_config.json with the following format:
```json
{
"client_type": "server",
"client_id": "{client_id}",
"private_key": "{private_key}",
"public_key_id": "{public_key_id}",
"coze_www_base": "https://www.coze.cn",
"coze_api_base": "https://api.coze.cn"
}
```
This file should be placed in the jwt-auth directory.
#### Running the Examples
After configuring the config file, you can run the JWT OAuth example using:
```bash
sh bootstrap.sh
```

View File

@@ -0,0 +1,3 @@
# PowerShell script to run gradle project
$CurrentPath = $PSScriptRoot
Start-Process -FilePath "cmd.exe" -ArgumentList "/c pushd `"$CurrentPath`" && call gradlew.bat run && popd" -Wait -NoNewWindow

View File

@@ -0,0 +1,4 @@
#!/bin/bash
chmod +x ./gradlew
./gradlew run

View File

@@ -0,0 +1,92 @@
plugins {
id("java")
id("application")
id("com.diffplug.spotless") version "6.11.0"
}
group = "com.coze"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
maven {
url = uri("https://maven.pkg.github.com/coze-dev/coze-api")
}
}
dependencies {
// Javalin 和 Jetty 依赖
implementation("io.javalin:javalin:4.6.8")
implementation("org.eclipse.jetty:jetty-server:9.4.51.v20230217")
implementation("org.eclipse.jetty:jetty-webapp:9.4.51.v20230217")
implementation("org.eclipse.jetty:jetty-util:9.4.51.v20230217")
implementation("org.eclipse.jetty:jetty-servlet:9.4.51.v20230217")
implementation("org.eclipse.jetty:jetty-security:9.4.51.v20230217")
implementation("org.eclipse.jetty:jetty-http:9.4.51.v20230217")
implementation("org.eclipse.jetty:jetty-io:9.4.51.v20230217")
implementation("org.slf4j:slf4j-simple:2.0.7")
// Lombok 支持 - 添加版本号
compileOnly("org.projectlombok:lombok:1.18.30")
annotationProcessor("org.projectlombok:lombok:1.18.30")
// YAML 支持
implementation("org.yaml:snakeyaml")
// JSON 处理
implementation("com.fasterxml.jackson.core:jackson-databind")
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml")
// coze api
implementation("com.coze:coze-api:0.2.3")
implementation("commons-io:commons-io:2.11.0")
// 测试依赖
testImplementation(platform("org.junit:junit-bom:5.10.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
}
application {
mainClass.set("com.coze.jwt.Main")
}
tasks.register<Jar>("uberJar") {
archiveClassifier.set("uber")
from(sourceSets.main.get().output)
dependsOn(configurations.runtimeClasspath)
from({
configurations.runtimeClasspath.get()
.filter { it.name.endsWith("jar") }
.map { zipTree(it) }
})
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(8))
}
}
tasks.named<Test>("test") {
useJUnitPlatform()
}
spotless {
java {
// 使用 Google Java 格式化规则
googleJavaFormat()
// 移除未使用的 imports
removeUnusedImports()
// 确保文件以新行结束
endWithNewline()
// 自定义导入顺序
importOrder("java", "javax", "org", "com", "")
}
}

View File

@@ -0,0 +1,8 @@
{
"client_type": "jwt",
"client_id": "1167741924603",
"coze_www_base": "https://www.coze.cn",
"coze_api_base": "https://api.coze.cn",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCqdnAKFZJ6lD5f\nzic42jpzHlabKGZoM1uhO+4qDwsEMtnCe4pdBPB0SDv5oK50atV3oDxrGbii2zPX\nCRABr7zEbFCoL6I2elVxDlQodFRsyQdOMJqJdROVu26uNTf5yCG1g2ZysKTSELk3\nPGzFJBExqcectjj67wojZltBbF3SMxYF7EK4bM4txAjOPr3k9+XlX4M8gDTlT6ZY\nwyZM6XFkUM3HX49yTtLqUORWdj29UEvtBG56Qwq+rDzXc1/u0OaL5DAwuWwn1nrU\ntIMUPvw6w7E3XbQsAatmoeuCaIsZSVX02qxNtjMktGwXT+YE/xWEQUM5c2ODRrMq\nOteW3frVAgMBAAECggEAE34sZ8Akb6deI/SIywPhj+KAmDOcKlRH09NCSUWjxod4\nxrhROpE4XK5vSqd+QPUqk13GgEorsVaMbLhY6pMfF1Mk52azD7RuQC8ZnpqsQNho\nNnUbvED1dreSpjyTDxmSCUC3yWu7yAMo5aZ08Gz5w7Uc4iXQXu77jiVupWJFWaZV\naR1AF6xnO/9iwTGHy+fDHYy5pCypVrEvg7UkaPw0bUC5x1nlFhLtXd2lnRLssY8l\nEH9HpckrkaqD3zYROh3nBysoZkkfJCj9+KGyVYAlAlGoLKbFv/KTfSIhXdlNlSg9\ngOQPeH8hnpJArTZGt2lb4+DN6P1FlGmEjzSjOj7eTQKBgQDX8Yc0kC2ET51Ims50\nEKy+dT1rp5n2DACBW+qzYKD8w67NTnViaZ2nc6NLu0x9/qwAarRm1X23U91HOEt9\nRa69PTrVAxmaYIXruDGAvu2h580IddmvUHjFVN8RWednFlltPgESErA+W81yTfKe\n82W5ZNnmSZh+MHnGSXDQJMO04wKBgQDKFStRvOzPCmOYwAclmw7UC36Q1rDh/gMn\n9pJrLQcbQGPR2pY+S/lGrDdmhuZTqPe2ircj1YWp8qr4tJNeht4N8YDS8qLsu12m\nHQNBTfrg7e6jQdSkxBz8iILmjBzHGwxUZApV0ERK6+iJ4HI4OF37IBjt8xUj9ZaX\nKNL2x7DW5wKBgHTH56ijeBofvB1xqsjV47W0TZ0UrIyFfRh4Dvsm/Kj1YmkhTxYD\nrADM5rij+AADZB1tl1YtiqlEL1y+swRyVMd+f3yHCqeUH5iUqiDIIFb4tscmhKzs\nxgNhnKkTh7MWQRJ0/7s4ZBu1Jev0/4q6cn7KbZS+pDtKF/EF8n5+A2ClAoGALzVO\nz2OBNsTIi5CgmspkZmjhAlkFLWY3uohBoEP+mwEp0IZt/tOkwjvNHWh6OiUI6V3y\ndq6U8SS9iCg3HSgIi91VKPKB2SfUMtAoSIM9DnrG+uCQGoWt99i0K8OjnWfWM4jD\nWVSz/4So6DzEshmI6veOm1fkImhm650f24K+7xUCgYBEH8EffQsbH/48HikJYWg2\naHVD9uxUT6n7bPHuKMRVfVTgAeXMn1/OBd23fyablmnQAARZFanx/smiUu3Bgyv3\nmV1IYfRo9s9ob+r+Kg7j3lyVy2GXTrojOT8O6ThwO/ImaEO+rImG0bpSgk/LzwWP\n9ZYdCpNH7uFPj/AIbwGizg==\n-----END PRIVATE KEY-----",
"public_key_id": "aEFyH76ZupmgX46sNfh03MYcGscFpwPRSM4U3wOnYcM"
}

View File

@@ -0,0 +1,23 @@
[versions]
spring-boot = "3.2.3"
spring-dependency = "1.1.4"
junit = "5.10.0"
snakeyaml = "2.0"
coze = "0.2.0"
[libraries]
spring-boot-starter-web = { group = "org.springframework.boot", name = "spring-boot-starter-web" }
spring-boot-starter-actuator = { group = "org.springframework.boot", name = "spring-boot-starter-actuator" }
spring-boot-configuration = { group = "org.springframework.boot", name = "spring-boot-configuration-processor" }
lombok = { group = "org.projectlombok", name = "lombok" }
snakeyaml = { group = "org.yaml", name = "snakeyaml", version.ref = "snakeyaml" }
jackson-databind = { group = "com.fasterxml.jackson.core", name = "jackson-databind" }
jackson-yaml = { group = "com.fasterxml.jackson.dataformat", name = "jackson-dataformat-yaml" }
coze-api = { group = "com.coze", name = "coze-api", version.ref = "coze" }
spring-boot-test = { group = "org.springframework.boot", name = "spring-boot-starter-test" }
junit-bom = { group = "org.junit", name = "junit-bom", version.ref = "junit" }
junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter" }
[plugins]
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" }
spring-dependency = { id = "io.spring.dependency-management", version.ref = "spring-dependency" }

View File

@@ -0,0 +1,6 @@
#Wed Jan 15 10:45:35 CST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -0,0 +1,234 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

View File

@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1,9 @@
Run this command for linux/macos:
```bash
bash bootstrap.sh
```
Run this command for windows powershell:
```powershell
.\bootstrap.ps1
```

View File

@@ -0,0 +1 @@
rootProject.name = "jwt-oauth"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More