Compare commits

...

6 Commits

Author SHA1 Message Date
cf235d38bb feat(模板): 为模板查找方法添加scanSource参数
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
在findFirstAvailableTemplate方法中新增scanSource参数,用于控制模板生成时的来源检查逻辑。调用方TaskTaskServiceImpl在强制创建vlog时传入false以跳过来源检查。
2025-09-23 13:50:26 +08:00
8903818cb0 订单详情 2025-09-23 12:21:34 +08:00
ae0cf56216 content返回url 2025-09-23 10:40:04 +08:00
90b6f53986 兜底1个 2025-09-23 10:38:23 +08:00
80b4508211 docs 2025-09-23 10:07:14 +08:00
57b8d90d5e 名称 2025-09-23 10:04:05 +08:00
10 changed files with 128 additions and 545 deletions

40
AGENTS.md Normal file
View File

@@ -0,0 +1,40 @@
# Repository Guidelines
## Project Structure & Module Organization
- Application code: `src/main/java/com/ycwl/basic/**` (controllers, services, mapper/repository, dto/model, config, util).
- Resources: `src/main/resources/**` (Spring configs, `mapper/*.xml`, static assets, logging).
- Tests: `src/test/java/**` mirrors main packages.
- Build output: `target/` (never commit).
## Build, Test, and Development Commands
- Build artifact: `mvn clean package` (tests are skipped by default via `pom.xml`).
- Run locally (dev): `mvn spring-boot:run -Dspring-boot.run.profiles=dev`.
- Run jar: `java -jar target/basic21-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev`.
- Execute tests: `mvn -DskipTests=false test` (note: `pom.xml` excludes `**/*Test.java` from test-compile; temporarily remove/override that config if you need to compile and run tests).
## Coding Style & Naming Conventions
- Java 21. Use 4-space indentation; UTF-8; no wildcard imports.
- Packages: `com.ycwl.basic.*`; classes PascalCase; methods/fields camelCase; constants UPPER_SNAKE_CASE.
- Controllers in `controller`, business logic in `service`, persistence in `mapper` + `resources/mapper/*.xml`.
- Prefer Lombok for boilerplate and constructor injection where applicable.
## Testing Guidelines
- Framework: Spring Boot testing + JUnit (see `spring-boot-starter-test`).
- Test names end with `Test` or `Tests` and mirror package structure.
- Aim to cover service/util layers and critical controllers. No enforced coverage target.
- To enable tests locally, remove/override the `maven-compiler-plugin` `testExcludes` in `pom.xml` and run `mvn -DskipTests=false test`.
## Commit & Pull Request Guidelines
- Follow Conventional Commits: `feat(scope): summary`, `fix(scope): summary`, `refactor: ...`.
- Reference issues (e.g., `#123`) and include brief rationale and screenshots for UI-facing changes.
- Keep PRs focused; include run/build instructions and any config changes.
## Security & Configuration Tips
- Profiles: `application.yml` and `bootstrap.yml` with `-dev`/`-prod` variants. Select via `--spring.profiles.active`.
- Do not commit secrets. Provide Nacos, Redis, MySQL, OSS/S3, and 3rd‑party keys via environment or secure config.
- Review `logback-spring*.xml` before raising log levels in production.
## Agent-Specific Notes
- Keep changes minimal and within existing package boundaries.
- Do not reorganize MyBatis XML names or mapper interfaces without updating both sides.
- If altering APIs, update affected tests and documentation in the same PR.

567
CLAUDE.md
View File

@@ -1,527 +1,40 @@
# CLAUDE.md # Repository Guidelines
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Structure & Module Organization
- Application code: `src/main/java/com/ycwl/basic/**` (controllers, services, mapper/repository, dto/model, config, util).
## 构建和开发命令 - Resources: `src/main/resources/**` (Spring configs, `mapper/*.xml`, static assets, logging).
- Tests: `src/test/java/**` mirrors main packages.
### 构建应用程序 - Build output: `target/` (never commit).
```bash
# 清理构建(默认跳过测试) ## Build, Test, and Development Commands
mvn clean package - Build artifact: `mvn clean package` (tests are skipped by default via `pom.xml`).
- Run locally (dev): `mvn spring-boot:run -Dspring-boot.run.profiles=dev`.
# 清理构建并执行测试 - Run jar: `java -jar target/basic21-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev`.
mvn clean package -DskipTests=false - Execute tests: `mvn -DskipTests=false test` (note: `pom.xml` excludes `**/*Test.java` from test-compile; temporarily remove/override that config if you need to compile and run tests).
# 运行应用程序 ## Coding Style & Naming Conventions
mvn spring-boot:run - Java 21. Use 4-space indentation; UTF-8; no wildcard imports.
``` - Packages: `com.ycwl.basic.*`; classes PascalCase; methods/fields camelCase; constants UPPER_SNAKE_CASE.
- Controllers in `controller`, business logic in `service`, persistence in `mapper` + `resources/mapper/*.xml`.
### 测试命令 - Prefer Lombok for boilerplate and constructor injection where applicable.
```bash
# 运行特定测试类 ## Testing Guidelines
mvn test -Dtest=FaceCleanerTest - Framework: Spring Boot testing + JUnit (see `spring-boot-starter-test`).
- Test names end with `Test` or `Tests` and mirror package structure.
# 运行特定测试方法 - Aim to cover service/util layers and critical controllers. No enforced coverage target.
mvn test -Dtest=FaceCleanerTest#testSpecificMethod - To enable tests locally, remove/override the `maven-compiler-plugin` `testExcludes` in `pom.xml` and run `mvn -DskipTests=false test`.
# 运行特定包的测试 ## Commit & Pull Request Guidelines
mvn test -Dtest="com.ycwl.basic.storage.adapters.*Test" - Follow Conventional Commits: `feat(scope): summary`, `fix(scope): summary`, `refactor: ...`.
- Reference issues (e.g., `#123`) and include brief rationale and screenshots for UI-facing changes.
# 运行pricing模块测试 - Keep PRs focused; include run/build instructions and any config changes.
mvn test -Dtest="com.ycwl.basic.pricing.*Test"
## Security & Configuration Tips
# 运行所有测试 - Profiles: `application.yml` and `bootstrap.yml` with `-dev`/`-prod` variants. Select via `--spring.profiles.active`.
mvn test -DskipTests=false - Do not commit secrets. Provide Nacos, Redis, MySQL, OSS/S3, and 3rd‑party keys via environment or secure config.
- Review `logback-spring*.xml` before raising log levels in production.
# 运行测试并生成详细报告
mvn test -DskipTests=false -Dsurefire.printSummary=true ## Agent-Specific Notes
``` - Keep changes minimal and within existing package boundaries.
- Do not reorganize MyBatis XML names or mapper interfaces without updating both sides.
### 开发环境配置 - If altering APIs, update affected tests and documentation in the same PR.
应用程序使用 Spring 配置文件:
- 默认激活配置文件:`dev`
- 生产环境配置文件:`prod`(启用定时任务)
- 配置文件:`application-dev.yml``application-prod.yml`
## 架构概览
这是一个 Spring Boot 3.3.5 应用程序(Java 21),采用多租户架构,通过不同的 API 端点为不同的客户端类型提供服务。
### 控制器架构
- **移动端 APIs** (`/api/mobile/`):面向移动应用的客户端端点
- **PC 端 APIs** (`/api/`):Web 仪表板/管理面板端点
- **任务 APIs** (`/task/`):后台工作和渲染任务端点
- **外部 APIs**:专用集成(打印机、代理、viid、vpt、wvp)
### 核心业务模块
#### 工厂模式实现
三个主要工厂类管理第三方集成:
1. **StorageFactory** (`com.ycwl.basic.storage.StorageFactory`)
- 管理:本地存储、AWS S3、阿里云 OSS 存储适配器
- 配置节:`storage.configs[]`
2. **PayFactory** (`com.ycwl.basic.pay.PayFactory`)
- 管理:微信支付、聪明支付适配器
- 配置节:`pay.configs[]`
3. **FaceBodyFactory** (`com.ycwl.basic.facebody.FaceBodyFactory`)
- 管理:阿里云、百度人脸识别适配器
- 配置节:`facebody.configs[]`
#### 适配器模式
每个工厂使用标准化接口:
- `IStorageAdapter`:文件操作(上传/下载/删除/ACL)
- `IPayAdapter`:支付生命周期(创建/回调/退款)
- `IFaceBodyAdapter`:人脸识别操作
#### 定时任务系统
`com.ycwl.basic.task` 包中的后台任务(仅生产环境):
- `VideoTaskGenerator`:人脸识别和视频处理
- `FaceCleaner`:人脸和存储清理任务
- `DynamicTaskGenerator`:带延迟队列的动态任务创建
- `ScenicStatsTask`:统计数据聚合
### 数据库和持久化
- **MyBatis Plus**:具有自动 CRUD 操作的 ORM
- **MapperScan**:扫描 `com.ycwl.basic.mapper` 及子包
- **数据库**:MySQL 配合 HikariCP 连接池
- **Redis**:会话管理和缓存
### 主要库和依赖
- Spring Boot 3.3.5 启用 Java 21 虚拟线程
- MyBatis Plus 3.5.5 用于数据库操作
- JWT (jjwt 0.9.0) 用于身份验证
- 微信支付 SDK 用于支付处理
- 阿里云 OSS 和 AWS S3 用于文件存储
- 阿里云和百度 SDK 用于人脸识别
- OpenTelemetry 用于可观测性(开发环境中禁用)
### 业务逻辑组织
- **Service 层**:`service` 包中的业务逻辑实现
- **Biz 层**:`biz` 包中的高级业务编排
- **Repository 模式**:`repository` 包中的数据访问抽象
- **自定义异常**:特定领域的异常处理
### 配置管理
每个模块使用 Spring Boot 自动配置启动器:
- 支持多供应商的命名配置
- 通过配置进行默认供应商选择
- 针对不同环境的特定配置文件
## 常见开发模式
### 添加新的存储/支付/人脸识别供应商
1. 实现相应接口(`IStorageAdapter``IPayAdapter``IFaceBodyAdapter`
2. 在相应的类型枚举中添加枚举值
3. 更新工厂的 switch 表达式
4. 如需要,添加配置类
5. 在 application.yml 中更新新供应商配置
### 身份验证上下文
在整个应用程序中使用 `BaseContextHandler.getUserId()` 获取当前已认证用户 ID。
### API 响应模式
所有 API 都返回 `ApiResponse<T>` 包装器,通过 `CustomExceptionHandle` 进行一致的错误处理。
### 添加新的定时任务
1.`com.ycwl.basic.task` 包中创建类
2. 添加 `@Component``@Profile("prod")` 注解
3. 使用 `@Scheduled` 进行基于 cron 的执行
4. 遵循现有的错误处理和日志记录模式
### 多端API架构
应用程序通过路径前缀区分不同的客户端:
- **移动端**: `/api/mobile/*` - 针对移动应用优化的接口
- **PC管理端**: `/api/*` - Web管理面板接口
- **任务处理**: `/task/*` - 后台任务和渲染服务接口
- **外部集成**: 专用集成接口(打印机、代理、viid、vpt、wvp等)
每个端点都有对应的Controller包结构,确保API的职责分离和维护性。
## 价格查询系统 (Pricing Module)
### 核心架构
价格查询系统是一个独立的业务模块,位于 `com.ycwl.basic.pricing` 包中,提供商品定价、优惠券管理、券码管理和统一优惠检测功能。
#### 关键组件
- **PriceCalculationController** (`/api/pricing/calculate`):统一价格计算API,支持自动优惠组合
- **CouponManagementController** (`/api/pricing/admin/coupons/`):优惠券配置和统计管理
- **VoucherManagementController** (`/api/pricing/voucher/`):券码批次和券码管理
- **VoucherUsageController** (`/api/pricing/voucher/usage/`):券码使用记录和统计
- **PricingConfigController** (`/api/pricing/config/`):商品价格配置管理
- **OnePricePurchaseController** (`/api/pricing/admin/one-price/`):一口价配置管理
#### 商品类型支持
```java
ProductType枚举定义了支持的商品类型
- VLOG_VIDEO: Vlog视频
- RECORDING_SET: 录像集
- PHOTO_SET: 照相集
- PHOTO_PRINT: 照片打印
- MACHINE_PRINT: 一体机打印
```
#### 价格计算流程(统一优惠检测)
1. 接收PriceCalculationRequest(包含商品列表、用户ID、券码等)
2. 查找商品基础配置和分层定价
3. 处理套餐商品(BundleProductItem)
4. **统一优惠检测**:通过IDiscountDetectionService自动检测所有可用优惠
- 券码优惠(VoucherDiscountProvider,优先级100)
- 优惠券优惠(CouponDiscountProvider,优先级80)
- 一口价优惠(OnePricePurchaseDiscountProvider,优先级60)
5. **智能优惠组合**:按优先级和叠加规则应用最优优惠组合
6. 返回PriceCalculationResult(包含原价、最终价格、使用的优惠详情、可用优惠列表)
#### 优惠券系统
- **CouponType**: PERCENTAGE(百分比)、FIXED_AMOUNT(固定金额)
- **CouponStatus**: CLAIMED(已领取)、USED(已使用)、EXPIRED(已过期)
- 支持商品类型限制 (`applicableProducts` JSON字段)
- 最小消费金额和最大折扣限制
- 时间有效期控制
#### 分页查询功能
所有管理接口都支持分页查询:
- **优惠券系统**:使用PageHelper实现
- 优惠券配置分页:支持按状态、名称筛选
- 领取记录分页:支持按用户、优惠券、状态、时间范围筛选
- **券码系统**:使用MyBatis-Plus Page实现
- 券码批次分页:支持按景区、批次名称、状态筛选
- 券码列表分页:支持按批次、状态、用户筛选
- 使用记录分页:支持按券码、用户、时间范围筛选
#### 统计功能
- **优惠券统计**:基础统计(领取数、使用数、可用数)、详细统计(使用率、平均使用天数)
- **券码统计**:支持可重复使用的统计(使用率、重复使用率、平均使用次数)
- 时间范围统计:指定时间段的整体数据分析
## 券码管理系统 (Voucher System)
### 核心特性
券码系统支持**可重复使用**的优惠券管理,与传统优惠券系统并行工作。
#### 关键优势
- **可重复使用**:支持单个券码多次使用,通过`maxUseCount`配置最大使用次数
- **用户使用限制**:支持单个用户对券码的使用次数限制(`maxUsePerUser`)
- **使用间隔控制**:支持设置使用时间间隔(`useIntervalHours`)
- **时间范围控制**:支持设置券码的有效期开始和结束时间
#### 券码优惠类型
```java
public enum VoucherDiscountType {
FREE_ALL(0, "全场免费"), // 优先级最高,且不可叠加
REDUCE_PRICE(1, "商品降价"), // 每个商品减免固定金额
DISCOUNT(2, "商品打折"); // 每个商品按百分比打折
}
```
#### 数据库表结构
- **price_voucher_batch_config**:券码批次配置表,支持按景区、推客创建券码批次
- **price_voucher_code**:券码表,每个券码全局唯一,支持同一用户在同一景区只能领取一次
- **price_voucher_usage_record**:券码使用记录表,记录每次使用的完整信息
- **voucher_print_record**:券码打印记录表,用于移动端打印功能
## 统一优惠检测系统 (Unified Discount Detection)
### 设计模式
采用**策略模式**的可扩展优惠检测系统,统一管理并自动组合多种优惠类型。
#### 核心接口
```java
// 优惠提供者接口
public interface IDiscountProvider {
String getProviderType(); // 提供者类型
int getPriority(); // 优先级(数字越大越高)
List<DiscountInfo> detectAvailableDiscounts(); // 检测可用优惠
DiscountResult applyDiscount(); // 应用优惠
}
// 优惠检测服务接口
public interface IDiscountDetectionService {
DiscountCombinationResult calculateOptimalCombination(); // 计算最优组合
DiscountCombinationResult previewOptimalCombination(); // 预览优惠组合
}
```
#### 优惠提供者实现(按优先级排序)
1. **VoucherDiscountProvider** (优先级: 100)
- 处理券码优惠逻辑
- 支持用户主动输入券码或自动选择最优券码
- 全场免费券码不可与其他优惠叠加
2. **CouponDiscountProvider** (优先级: 80)
- 处理优惠券优惠逻辑
- 自动选择最优优惠券
- 可与券码叠加使用(除全场免费券码外)
3. **OnePricePurchaseDiscountProvider** (优先级: 60)
- 处理一口价优惠逻辑(景区级统一价格)
- 仅当一口价小于当前金额时产生优惠
- 叠加性由配置`canUseCoupon/canUseVoucher`控制
#### 优惠应用策略
```java
原价 券码 优惠券 一口价 最终价格
特殊情况
- 全场免费券码直接最终价=0停止后续优惠
- 一口价可叠加性由配置 canUseCoupon / canUseVoucher 控制
```
### 开发模式
#### 添加新商品类型
1. 在ProductType枚举中添加新类型
2. 在PriceProductConfig表中配置default配置
3. 根据需要添加分层定价(PriceTierConfig)
4. 更新前端产品类型映射
#### 添加新优惠券类型
1. 在CouponType枚举中添加类型
2. 在CouponServiceImpl中实现计算逻辑
3. 更新applicableProducts验证规则
#### 添加新优惠提供者(策略扩展)
```java
@Component
public class FlashSaleDiscountProvider implements IDiscountProvider {
@Override
public String getProviderType() { return "FLASH_SALE"; }
@Override
public int getPriority() { return 90; } // 介于券码和优惠券之间
@Override
public List<DiscountInfo> detectAvailableDiscounts(DiscountDetectionContext context) {
// 实现限时抢购优惠检测逻辑
return discountInfoList;
}
@Override
public DiscountResult applyDiscount(DiscountDetectionContext context, DiscountInfo discount) {
// 实现优惠应用逻辑
return discountResult;
}
}
```
#### 创建可重复使用券码批次
```java
VoucherBatchCreateReqV2 request = new VoucherBatchCreateReqV2();
request.setBatchName("限时活动券码");
request.setMaxUseCount(3); // 每个券码最多使用三次
request.setMaxUsePerUser(2); // 每个用户最多使用两次
request.setUseIntervalHours(12); // 使用间隔12小时
request.setValidStartTime(startTime); // 有效期开始时间
request.setValidEndTime(endTime); // 有效期结束时间
```
#### 自定义TypeHandler使用
项目使用自定义TypeHandler处理复杂JSON字段:
- `BundleProductListTypeHandler`:处理套餐商品列表JSON序列化
### 测试策略
针对pricing模块的全面测试策略:
#### 单元测试类型
- **服务层测试**:每个服务类都有对应测试类
- `PriceBundleServiceTest` - 套餐价格计算测试
- `ReusableVoucherServiceTest` - 可重复使用券码测试
- `VoucherTimeRangeTest` - 券码时间范围功能测试
- `VoucherPrintServiceCodeGenerationTest` - 券码生成测试
- **实体映射测试**:验证数据库映射和JSON序列化
- `PriceBundleConfigStructureTest` - 实体结构测试
- `PriceBundleConfigJsonTest` - JSON序列化测试
- `CouponSwitchFieldsMappingTest` - 字段映射测试
- **类型处理器测试**:验证自定义TypeHandler
- `BundleProductListTypeHandlerTest` - 套餐商品列表序列化测试
- **配置验证测试**:验证系统配置完整性
- `DefaultConfigValidationTest` - 验证所有ProductType的default配置
- `CodeGenerationStandaloneTest` - 独立代码生成测试
#### 测试执行命令
```bash
# 运行单个测试类
mvn test -Dtest=VoucherTimeRangeTest
mvn test -Dtest=ReusableVoucherServiceTest
mvn test -Dtest=BundleProductListTypeHandlerTest
# 运行整个pricing模块测试
mvn test -Dtest="com.ycwl.basic.pricing.*Test"
# 运行特定分类的测试
mvn test -Dtest="com.ycwl.basic.pricing.service.*Test" # 服务层测试
mvn test -Dtest="com.ycwl.basic.pricing.handler.*Test" # TypeHandler测试
mvn test -Dtest="com.ycwl.basic.pricing.entity.*Test" # 实体测试
mvn test -Dtest="com.ycwl.basic.pricing.mapper.*Test" # Mapper测试
# 运行带详细报告的测试
mvn test -Dtest="com.ycwl.basic.pricing.*Test" -Dsurefire.printSummary=true
```
#### 重点测试场景
- **价格计算核心流程**:验证统一优惠检测和组合逻辑
- **可重复使用券码**:验证多次使用、时间间隔、用户限制逻辑
- **时间范围控制**:验证券码有效期开始和结束时间
- **优惠叠加规则**:验证券码、优惠券、一口价的叠加逻辑
- **JSON序列化**:验证复杂对象在数据库中的存储和读取
- **分页功能**:验证PageHelper和MyBatis-Plus分页集成
- **异常处理**:验证业务异常和全局异常处理器
## 关键架构模式
### Repository 层模式
项目使用Repository层抽象数据访问逻辑:
- Repository接口定义数据访问契约
- Mapper接口处理MyBatis Plus的数据库映射
- Service层通过Repository访问数据,避免直接依赖Mapper
### 异常处理架构
- **全局异常处理**: `CustomExceptionHandle` 提供统一的异常处理和响应格式
- **业务异常**: 自定义异常类继承RuntimeException,携带业务错误码
- **集成异常**: `IntegrationException` 专门处理外部服务调用异常
### 配置驱动的扩展性
通过配置文件驱动的多供应商支持:
- 存储:本地、AWS S3、阿里云 OSS
- 支付:微信支付、聪明支付
- 人脸识别:阿里云、百度
每个供应商通过统一接口访问,配置切换无需代码修改。
### 业务层架构
- **Service层**: 核心业务逻辑实现
- **Biz层**: 高级业务流程编排,组合多个Service
- **Controller层**: HTTP请求处理和响应转换
- **Repository层**: 数据访问抽象
### 认证和会话管理
- **JWT**: 使用jjwt库进行身份验证
- **Redis**: 存储会话信息和缓存
- **BaseContextHandler**: 提供当前用户上下文访问
## 微服务集成架构 (Integration Package)
### 核心架构
位于 `com.ycwl.basic.integration` 包,使用 Spring Cloud OpenFeign 和 Nacos 实现外部微服务集成。
#### 通用基础设施
- **IntegrationProperties**: 所有集成的集中配置管理
- **FeignErrorDecoder**: 自定义错误解码器,统一错误处理
- **IntegrationException**: 标准化集成异常
- **CommonResponse/PageResponse**: 外部服务响应包装器
#### 已实现的服务集成
- **Scenic Integration** (`integration.scenic`): ZT-Scenic 微服务集成
- **Device Integration** (`integration.device`): ZT-Device 微服务集成
#### 集成模式
每个外部服务按以下结构组织:
```
service/
├── client/ # Feign 客户端
├── config/ # 服务特定配置
├── dto/ # 数据传输对象
├── service/ # 业务逻辑层
└── example/ # 使用示例
```
### 配置管理
```yaml
integration:
scenic:
enabled: true
serviceName: zt-scenic
connectTimeout: 5000
readTimeout: 10000
device:
enabled: true
serviceName: zt-device
connectTimeout: 5000
readTimeout: 10000
```
### 使用模式
所有集成服务使用统一的 `handleResponse` 模式进行错误处理,确保一致的异常包装和日志记录。
### 测试集成服务
```bash
# 运行特定集成测试
mvn test -Dtest=ScenicIntegrationServiceTest
mvn test -Dtest=DeviceIntegrationServiceTest
# 运行所有集成测试
mvn test -Dtest="com.ycwl.basic.integration.*Test"
```
### 调试集成问题
启用 Feign 客户端日志:
```yaml
logging:
level:
com.ycwl.basic.integration: DEBUG
```
## 开发环境和调试
### 端口配置
- **开发环境端口**: 8030
- **应用名称**: zt
### 日志配置
开发环境默认启用详细的集成服务日志,便于调试外部服务调用问题。
### CI/CD 配置
项目使用 Jenkins 进行持续集成:
- **JDK 版本**: OpenJDK 21
- **构建命令**: `mvn clean package -DskipTests=true`
- **构建产物**: 自动归档和发布 JAR 文件
## 重要开发约定
### 测试文件组织
测试按功能模块组织,包括:
- **适配器测试**: `*AdapterTest.java` 测试第三方集成
- **实体测试**: 验证数据库映射和JSON序列化
- **Mapper测试**: 验证数据访问层逻辑
- **Handler测试**: 测试自定义TypeHandler
### 模块化架构
每个业务模块(如 `pricing``integration``order`)都有完整的分层结构:
```
module/
├── controller/ # REST API控制器
├── service/ # 业务逻辑层
├── repository/ # 数据访问抽象
├── mapper/ # MyBatis数据映射
├── entity/ # JPA/MyBatis实体
├── dto/ # 数据传输对象
├── enums/ # 枚举定义
└── exception/ # 模块特定异常
```
### 外部服务集成
集成服务统一使用以下模式:
- **Feign客户端**: 声明式HTTP客户端调用
- **错误处理**: 统一的`handleResponse`模式
- **配置管理**: 通过`IntegrationProperties`集中配置
- **超时配置**: 连接超时5秒,读取超时10秒
## Windows 开发环境注意事项
### 路径处理
- 项目在Windows系统上运行,注意路径分隔符使用反斜杠 `\`
- 配置文件中的资源路径已适配Windows环境
- 日志文件和临时文件路径会自动适配系统环境
### 开发工具兼容性
- 确保使用Java 21兼容的IDE
- Maven命令在Windows Command Prompt和PowerShell中均可使用
- 建议使用UTF-8编码避免中文字符问题
### 端口占用检查
开发时如遇端口冲突,使用以下命令检查:
```cmd
netstat -ano | findstr :8030
taskkill /f /pid <PID>
```

View File

View File

@@ -32,8 +32,6 @@ public class TemplateBiz {
private FaceRepository faceRepository; private FaceRepository faceRepository;
@Autowired @Autowired
private SourceMapper sourceMapper; private SourceMapper sourceMapper;
@Autowired
private SourceRepository sourceRepository;
public boolean determineTemplateCanGenerate(Long templateId, Long faceId) { public boolean determineTemplateCanGenerate(Long templateId, Long faceId) {
return determineTemplateCanGenerate(templateId, faceId, true); return determineTemplateCanGenerate(templateId, faceId, true);
@@ -175,4 +173,17 @@ public class TemplateBiz {
return filteredParams; return filteredParams;
} }
} public Long findFirstAvailableTemplate(List<Long> templateIds, Long faceId, boolean scanSource) {
if (templateIds == null || templateIds.isEmpty() || faceId == null) {
return null;
}
for (Long templateId : templateIds) {
if (determineTemplateCanGenerate(templateId, faceId, scanSource)) {
return templateId;
}
}
return null;
}
}

View File

@@ -55,7 +55,7 @@ public class AppTaskController {
@PostMapping("/submit") @PostMapping("/submit")
public ApiResponse<String> submitVideoTask(@RequestBody VideoTaskReq videoTaskReq) { public ApiResponse<String> submitVideoTask(@RequestBody VideoTaskReq videoTaskReq) {
taskService.createTaskByFaceIdAndTempalteId(videoTaskReq.getFaceId(),videoTaskReq.getTemplateId(),0); taskService.createTaskByFaceIdAndTemplateId(videoTaskReq.getFaceId(),videoTaskReq.getTemplateId(),0);
return ApiResponse.success("成功"); return ApiResponse.success("成功");
} }
} }

View File

@@ -25,6 +25,7 @@ public class ContentPageVO {
private int lockType; private int lockType;
// 内容id contentType为0或1时才有值 // 内容id contentType为0或1时才有值
private Long contentId; private Long contentId;
private String videoUrl;
// 模版id // 模版id
private Long templateId; private Long templateId;
private String templateCoverUrl; private String templateCoverUrl;

View File

@@ -691,6 +691,7 @@ public class FaceServiceImpl implements FaceService {
contentPageVO.setContentId(memberVideoEntityList.getFirst().getVideoId()); contentPageVO.setContentId(memberVideoEntityList.getFirst().getVideoId());
VideoEntity video = videoRepository.getVideo(contentPageVO.getContentId()); VideoEntity video = videoRepository.getVideo(contentPageVO.getContentId());
if (video != null) { if (video != null) {
contentPageVO.setVideoUrl(video.getVideoUrl());
contentPageVO.setDuration(video.getDuration()); contentPageVO.setDuration(video.getDuration());
contentPageVO.setLockType(-1); contentPageVO.setLockType(-1);
TaskUpdateResult updResult = videoTaskRepository.checkTaskUpdate(video.getTaskId()); TaskUpdateResult updResult = videoTaskRepository.checkTaskUpdate(video.getTaskId());

View File

@@ -14,9 +14,9 @@ public interface TaskService {
TemplateRespVO workerGetTemplate(Long templateId, WorkerAuthReqVo req); TemplateRespVO workerGetTemplate(Long templateId, WorkerAuthReqVo req);
void createTaskByFaceIdAndTempalteId(Long faceId, Long templateId); void createTaskByFaceIdAndTemplateId(Long faceId, Long templateId);
void createTaskByFaceIdAndTempalteId(Long faceId, Long templateId, int automatic); void createTaskByFaceIdAndTemplateId(Long faceId, Long templateId, int automatic);
void taskSuccess(Long taskId, TaskSuccessReqVo req); void taskSuccess(Long taskId, TaskSuccessReqVo req);
@@ -28,7 +28,7 @@ public interface TaskService {
void forceCreateTaskByFaceIdAndTempalteId(Long faceId, Long templateId); void forceCreateTaskByFaceIdAndTempalteId(Long faceId, Long templateId);
void autoCreateTaskByFaceId(Long id); void autoCreateTaskByFaceId(Long faceId);
Date getTaskShotDate(Long taskId); Date getTaskShotDate(Long taskId);

View File

@@ -250,7 +250,7 @@ public class TaskTaskServiceImpl implements TaskService {
@Override @Override
public void forceCreateTaskByFaceIdAndTempalteId(Long faceId, Long templateId) { public void forceCreateTaskByFaceIdAndTempalteId(Long faceId, Long templateId) {
createTaskByFaceIdAndTempalteIdInternal(faceId, templateId, 0, true); createTaskByFaceIdAndTemplateIdInternal(faceId, templateId, 0, true);
} }
@Override @Override
@@ -269,7 +269,7 @@ public class TaskTaskServiceImpl implements TaskService {
log.info("faceId:{} faceSampleList is empty", faceId); log.info("faceId:{} faceSampleList is empty", faceId);
return; return;
} }
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(faceRespVO.getScenicId()); ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(faceRespVO.getScenicId());
List<TemplateRespVO> templateList = templateRepository.getTemplateListByScenicId(faceRespVO.getScenicId()); List<TemplateRespVO> templateList = templateRepository.getTemplateListByScenicId(faceRespVO.getScenicId());
if (templateList == null || templateList.isEmpty()) { if (templateList == null || templateList.isEmpty()) {
// 没有vlog视频的情况下 // 没有vlog视频的情况下
@@ -284,24 +284,36 @@ public class TaskTaskServiceImpl implements TaskService {
VideoPieceGetter.addTask(task); VideoPieceGetter.addTask(task);
return; return;
} }
if (Integer.valueOf(3).equals(scenicConfig.getBookRoutine()) || Integer.valueOf(4).equals(scenicConfig.getBookRoutine())) { if (Integer.valueOf(3).equals(scenicConfig.getInteger("book_routine")) || Integer.valueOf(4).equals(scenicConfig.getInteger("book_routine"))) {
// 生成全部视频的逻辑 // 生成全部视频的逻辑
templateList.forEach(template -> createTaskByFaceIdAndTempalteId(faceId, template.getId(), 1)); templateList.forEach(template -> createTaskByFaceIdAndTemplateId(faceId, template.getId(), 1));
} else { } else {
createTaskByFaceIdAndTempalteId(faceId, templateList.getFirst().getId(), 1); if (Boolean.TRUE.equals(scenicConfig.getBoolean("force_create_vlog"))) {
Long availableTemplateId = templateBiz.findFirstAvailableTemplate(templateList.stream().map(TemplateRespVO::getId).toList(), faceId, false);
if (availableTemplateId != null) {
createTaskByFaceIdAndTemplateId(faceId, availableTemplateId, 1);
} else {
log.info("faceId:{} available template is not exist", faceId);
}
} else {
// 非强制创建,只创建第一个可用模板
if (!templateList.isEmpty()) {
createTaskByFaceIdAndTemplateId(faceId, templateList.getFirst().getId(), 1);
}
}
} }
} }
@Override @Override
public void createTaskByFaceIdAndTempalteId(Long faceId, Long templateId) { public void createTaskByFaceIdAndTemplateId(Long faceId, Long templateId) {
createTaskByFaceIdAndTempalteId(faceId, templateId, 0); createTaskByFaceIdAndTemplateId(faceId, templateId, 0);
} }
@Override @Override
public void createTaskByFaceIdAndTempalteId(Long faceId, Long templateId, int automatic) { public void createTaskByFaceIdAndTemplateId(Long faceId, Long templateId, int automatic) {
createTaskByFaceIdAndTempalteIdInternal(faceId, templateId, automatic, false); createTaskByFaceIdAndTemplateIdInternal(faceId, templateId, automatic, false);
} }
private void createTaskByFaceIdAndTempalteIdInternal(Long faceId, Long templateId, int automatic, boolean forceCreate) { private void createTaskByFaceIdAndTemplateIdInternal(Long faceId, Long templateId, int automatic, boolean forceCreate) {
FaceEntity face = faceRepository.getFace(faceId); FaceEntity face = faceRepository.getFace(faceId);
if (face == null) { if (face == null) {
log.info("faceId:{} is not exist", faceId); log.info("faceId:{} is not exist", faceId);

View File

@@ -99,6 +99,10 @@
member_photo_data AS ( member_photo_data AS (
SELECT mp.member_id, 3 as type, mp.id, mp.crop_url as url, mp.quantity, mp.status, mp.create_time SELECT mp.member_id, 3 as type, mp.id, mp.crop_url as url, mp.quantity, mp.status, mp.create_time
FROM member_print mp FROM member_print mp
),
member_aio_photo_data AS (
SELECT 4 as type, s.id, s.url as url
FROM source s
) )
SELECT SELECT
oi.id AS oiId, oi.id AS oiId,
@@ -137,13 +141,14 @@
WHEN '1' THEN msd.url WHEN '1' THEN msd.url
WHEN '2' THEN msd.url WHEN '2' THEN msd.url
WHEN '3' THEN mpd.url WHEN '3' THEN mpd.url
WHEN '4' THEN msd.url WHEN '4' THEN mpa.url
END AS imgUrl END AS imgUrl
FROM order_item oi FROM order_item oi
LEFT JOIN `order` o ON oi.order_id = o.id LEFT JOIN `order` o ON oi.order_id = o.id
LEFT JOIN member_video_data mvd ON o.face_id = mvd.face_id AND oi.goods_id = mvd.video_id LEFT JOIN member_video_data mvd ON o.face_id = mvd.face_id AND oi.goods_id = mvd.video_id
LEFT JOIN member_source_data msd ON o.face_id = msd.face_id AND oi.goods_id = msd.face_id AND msd.type = oi.goods_type LEFT JOIN member_source_data msd ON o.face_id = msd.face_id AND oi.goods_id = msd.face_id AND msd.type = oi.goods_type
LEFT JOIN member_photo_data mpd ON oi.goods_id = mpd.id AND mpd.type = oi.goods_type LEFT JOIN member_photo_data mpd ON oi.goods_id = mpd.id AND mpd.type = oi.goods_type
LEFT JOIN member_aio_photo_data mpa ON oi.goods_id = mpa.id AND mpa.type = oi.goods_type
WHERE oi.order_id = #{id}; WHERE oi.order_id = #{id};
</select> </select>