You've already forked FrameTour-BE
Compare commits
75 Commits
0432b99524
...
master
Author | SHA1 | Date | |
---|---|---|---|
a7ede3303d | |||
aa7330000f | |||
29f4bbf2d8 | |||
ad42254ea0 | |||
0ceecf0488 | |||
311008cbf2 | |||
f54d40d026 | |||
3cb12c13c2 | |||
feac2e8d93 | |||
be375067ce | |||
7dec2e614c | |||
51d0716606 | |||
765998bd97 | |||
5f4f89112b | |||
d68b062951 | |||
99857db006 | |||
e8c645a3c0 | |||
fe8068b3d9 | |||
c689496130 | |||
7e16ad35e7 | |||
1727619b29 | |||
3099e68a97 | |||
db86c82bc8 | |||
f33ce8e7a7 | |||
de65fa1dd8 | |||
132a539bb6 | |||
9f66544a29 | |||
f4a16b5b09 | |||
9bc34fcfdb | |||
4b01e4cf82 | |||
f885f734ad | |||
ddbc2a0edb | |||
da89067c48 | |||
2836326518 | |||
6091d41df9 | |||
d4f9f1fe0d | |||
d860996f6d | |||
1b2793215f | |||
4f1443a3ca | |||
aba9fb0a15 | |||
ab3208c9df | |||
09e376e089 | |||
dad9ddc17c | |||
4a05773860 | |||
3c700a42f9 | |||
47c6b2ca67 | |||
59baf8811b | |||
019b9ffca6 | |||
30805f3e30 | |||
94d6b2f443 | |||
b34f994298 | |||
7728f4424f | |||
becbe5f6ab | |||
dc3a46362b | |||
a361b59d74 | |||
f779b0e040 | |||
78c4548d02 | |||
842310f73c | |||
cf235d38bb | |||
8903818cb0 | |||
ae0cf56216 | |||
90b6f53986 | |||
80b4508211 | |||
57b8d90d5e | |||
59b481989c | |||
61cf9383d0 | |||
878dec2c55 | |||
48bd9d2b0c | |||
b365d86796 | |||
18cb459320 | |||
b7d3e20c46 | |||
d55c7a7769 | |||
b14754ec0a | |||
a888ed3fe2 | |||
dc2154c020 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ target/
|
|||||||
.serena
|
.serena
|
||||||
.claude
|
.claude
|
||||||
.vscode
|
.vscode
|
||||||
|
*.jpg
|
||||||
|
40
AGENTS.md
Normal file
40
AGENTS.md
Normal 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
567
CLAUDE.md
@@ -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>
|
|
||||||
```
|
|
||||||
|
6
pom.xml
6
pom.xml
@@ -266,6 +266,12 @@
|
|||||||
<artifactId>mts20140618</artifactId>
|
<artifactId>mts20140618</artifactId>
|
||||||
<version>5.0.0</version>
|
<version>5.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Spring Kafka -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.kafka</groupId>
|
||||||
|
<artifactId>spring-kafka</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
@@ -360,4 +360,12 @@ public class OrderBiz {
|
|||||||
orderRepository.clearOrderCache(orderId); // 更新完了,清理下
|
orderRepository.clearOrderCache(orderId); // 更新完了,清理下
|
||||||
profitSharingBiz.revokeProfitSharing(order.getScenicId(), orderId, "订单已退款");
|
profitSharingBiz.revokeProfitSharing(order.getScenicId(), orderId, "订单已退款");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否购买了指定商品
|
||||||
|
* 提供给PriceBiz使用,避免循环调用
|
||||||
|
*/
|
||||||
|
public boolean checkUserBuyItem(Long userId, int goodsType, Long goodsId) {
|
||||||
|
return orderRepository.checkUserBuyItem(userId, goodsType, goodsId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
package com.ycwl.basic.biz;
|
package com.ycwl.basic.biz;
|
||||||
|
|
||||||
import com.ycwl.basic.model.mobile.order.IsBuyBatchRespVO;
|
import com.ycwl.basic.model.mobile.order.IsBuyBatchRespVO;
|
||||||
|
import com.ycwl.basic.model.mobile.order.IsBuyRespVO;
|
||||||
|
import com.ycwl.basic.model.mobile.scenic.content.ContentPageVO;
|
||||||
import com.ycwl.basic.model.pc.coupon.entity.CouponEntity;
|
import com.ycwl.basic.model.pc.coupon.entity.CouponEntity;
|
||||||
import com.ycwl.basic.model.pc.couponRecord.resp.CouponRecordQueryResp;
|
import com.ycwl.basic.model.pc.couponRecord.resp.CouponRecordQueryResp;
|
||||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||||
@@ -9,20 +11,25 @@ import com.ycwl.basic.model.pc.price.entity.PriceConfigEntity;
|
|||||||
import com.ycwl.basic.model.pc.price.resp.GoodsListRespVO;
|
import com.ycwl.basic.model.pc.price.resp.GoodsListRespVO;
|
||||||
import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity;
|
import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity;
|
||||||
import com.ycwl.basic.model.pc.template.resp.TemplateRespVO;
|
import com.ycwl.basic.model.pc.template.resp.TemplateRespVO;
|
||||||
|
import com.ycwl.basic.model.pc.video.entity.MemberVideoEntity;
|
||||||
import com.ycwl.basic.pricing.entity.PriceOnePriceConfig;
|
import com.ycwl.basic.pricing.entity.PriceOnePriceConfig;
|
||||||
import com.ycwl.basic.pricing.service.IOnePricePurchaseService;
|
import com.ycwl.basic.pricing.service.IOnePricePurchaseService;
|
||||||
import com.ycwl.basic.repository.FaceRepository;
|
import com.ycwl.basic.repository.FaceRepository;
|
||||||
|
import com.ycwl.basic.repository.MemberRelationRepository;
|
||||||
import com.ycwl.basic.repository.PriceRepository;
|
import com.ycwl.basic.repository.PriceRepository;
|
||||||
import com.ycwl.basic.repository.ScenicRepository;
|
import com.ycwl.basic.repository.ScenicRepository;
|
||||||
import com.ycwl.basic.repository.TemplateRepository;
|
import com.ycwl.basic.repository.TemplateRepository;
|
||||||
|
import com.ycwl.basic.service.pc.FaceService;
|
||||||
import org.apache.commons.lang3.Strings;
|
import org.apache.commons.lang3.Strings;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@@ -38,7 +45,12 @@ public class PriceBiz {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private FaceRepository faceRepository;
|
private FaceRepository faceRepository;
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@Lazy
|
||||||
|
private FaceService faceService;
|
||||||
|
@Autowired
|
||||||
private CouponBiz couponBiz;
|
private CouponBiz couponBiz;
|
||||||
|
@Autowired
|
||||||
|
private MemberRelationRepository memberRelationRepository;
|
||||||
|
|
||||||
public List<GoodsListRespVO> listGoodsByScenic(Long scenicId) {
|
public List<GoodsListRespVO> listGoodsByScenic(Long scenicId) {
|
||||||
List<GoodsListRespVO> goodsList = new ArrayList<>();
|
List<GoodsListRespVO> goodsList = new ArrayList<>();
|
||||||
@@ -48,15 +60,16 @@ public class PriceBiz {
|
|||||||
GoodsListRespVO goods = new GoodsListRespVO();
|
GoodsListRespVO goods = new GoodsListRespVO();
|
||||||
goods.setGoodsId(template.getId());
|
goods.setGoodsId(template.getId());
|
||||||
goods.setGoodsName(template.getName());
|
goods.setGoodsName(template.getName());
|
||||||
|
goods.setGoodsType(0);
|
||||||
return goods;
|
return goods;
|
||||||
}).forEach(goodsList::add);
|
}).forEach(goodsList::add);
|
||||||
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId);
|
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId);
|
||||||
if (scenicConfig != null) {
|
if (scenicConfig != null) {
|
||||||
if (!Boolean.TRUE.equals(scenicConfig.getDisableSourceVideo())) {
|
if (!Boolean.TRUE.equals(scenicConfig.getDisableSourceVideo())) {
|
||||||
goodsList.add(new GoodsListRespVO(1L, "录像集"));
|
goodsList.add(new GoodsListRespVO(1L, "录像集", 1));
|
||||||
}
|
}
|
||||||
if (!Boolean.TRUE.equals(scenicConfig.getDisableSourceImage())) {
|
if (!Boolean.TRUE.equals(scenicConfig.getDisableSourceImage())) {
|
||||||
goodsList.add(new GoodsListRespVO(2L, "照片集"));
|
goodsList.add(new GoodsListRespVO(2L, "照片集", 2));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return goodsList;
|
return goodsList;
|
||||||
@@ -140,6 +153,52 @@ public class PriceBiz {
|
|||||||
respVO.setBuy(Integer.valueOf(1).equals(orderEntity.getStatus()));
|
respVO.setBuy(Integer.valueOf(1).equals(orderEntity.getStatus()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (type == -1 && !respVO.isBuy()) {
|
||||||
|
// 直接查询用户购买状态,避免调用faceContentList造成循环调用
|
||||||
|
boolean allContentsPurchased = true;
|
||||||
|
|
||||||
|
// 检查视频模板购买状态
|
||||||
|
List<TemplateRespVO> templateList = templateRepository.getTemplateListByScenicId(scenicId);
|
||||||
|
for (TemplateRespVO template : templateList) {
|
||||||
|
// 使用OrderRepository直接检查是否购买了该模板下的内容
|
||||||
|
List<MemberVideoEntity> videoEntities = memberRelationRepository.listRelationByFaceAndTemplate(faceId, template.getId());
|
||||||
|
if (videoEntities == null || videoEntities.isEmpty()) {
|
||||||
|
allContentsPurchased = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
boolean hasPurchasedTemplate = orderBiz.checkUserBuyItem(userId, -1, videoEntities.getFirst().getVideoId());
|
||||||
|
if (!hasPurchasedTemplate) {
|
||||||
|
allContentsPurchased = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查源文件购买状态(录像集和照片集)
|
||||||
|
if (allContentsPurchased) {
|
||||||
|
if (scenicConfig != null) {
|
||||||
|
// 检查录像集
|
||||||
|
if (!Boolean.TRUE.equals(scenicConfig.getDisableSourceVideo())) {
|
||||||
|
boolean hasPurchasedRecording = orderBiz.checkUserBuyItem(userId, 1, faceId);
|
||||||
|
if (!hasPurchasedRecording) {
|
||||||
|
allContentsPurchased = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查照片集
|
||||||
|
if (allContentsPurchased && !Boolean.TRUE.equals(scenicConfig.getDisableSourceImage())) {
|
||||||
|
boolean hasPurchasedPhoto = orderBiz.checkUserBuyItem(userId, 2, faceId);
|
||||||
|
if (!hasPurchasedPhoto) {
|
||||||
|
allContentsPurchased = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果所有内容都已购买,则认为已购买套餐
|
||||||
|
if (allContentsPurchased) {
|
||||||
|
respVO.setBuy(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
respVO.setShare(false);
|
respVO.setShare(false);
|
||||||
if (face == null || !face.getMemberId().equals(userId)) {
|
if (face == null || !face.getMemberId().equals(userId)) {
|
||||||
respVO.setShare(true);
|
respVO.setShare(true);
|
||||||
|
@@ -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);
|
||||||
@@ -134,6 +132,7 @@ public class TemplateBiz {
|
|||||||
log.info("filterTaskParams: templateId:{} has no placeholders", templateId);
|
log.info("filterTaskParams: templateId:{} has no placeholders", templateId);
|
||||||
return Map.of();
|
return Map.of();
|
||||||
}
|
}
|
||||||
|
TemplateConfigEntity templateConfig = templateRepository.getTemplateConfig(templateId);
|
||||||
|
|
||||||
// 统计每个 placeholder 在模板中出现的次数
|
// 统计每个 placeholder 在模板中出现的次数
|
||||||
Map<String, Long> placeholderCounts = templatePlaceholders.stream()
|
Map<String, Long> placeholderCounts = templatePlaceholders.stream()
|
||||||
@@ -144,6 +143,9 @@ public class TemplateBiz {
|
|||||||
|
|
||||||
Map<String, List<SourceEntity>> filteredParams = new HashMap<>();
|
Map<String, List<SourceEntity>> filteredParams = new HashMap<>();
|
||||||
|
|
||||||
|
// 判断是否允许片段重复
|
||||||
|
boolean allowDuplicate = templateConfig != null && Integer.valueOf(1).equals(templateConfig.getDuplicateEnable());
|
||||||
|
|
||||||
for (Map.Entry<String, Long> entry : placeholderCounts.entrySet()) {
|
for (Map.Entry<String, Long> entry : placeholderCounts.entrySet()) {
|
||||||
String placeholder = entry.getKey();
|
String placeholder = entry.getKey();
|
||||||
Long requiredCount = entry.getValue();
|
Long requiredCount = entry.getValue();
|
||||||
@@ -153,26 +155,64 @@ public class TemplateBiz {
|
|||||||
String imageKey = placeholder;
|
String imageKey = placeholder;
|
||||||
if (allTaskParams.containsKey(imageKey)) {
|
if (allTaskParams.containsKey(imageKey)) {
|
||||||
List<SourceEntity> allSources = allTaskParams.get(imageKey);
|
List<SourceEntity> allSources = allTaskParams.get(imageKey);
|
||||||
int actualCount = Math.min(requiredCount.intValue(), allSources.size());
|
List<SourceEntity> selectedSources = selectSources(allSources, requiredCount.intValue(), allowDuplicate);
|
||||||
List<SourceEntity> selectedSources = allSources.subList(0, actualCount);
|
if (!selectedSources.isEmpty()) {
|
||||||
filteredParams.put(imageKey, new ArrayList<>(selectedSources));
|
filteredParams.put(imageKey, selectedSources);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 视频源:占位符直接对应设备ID
|
// 视频源:占位符直接对应设备ID
|
||||||
String videoKey = placeholder;
|
String videoKey = placeholder;
|
||||||
if (allTaskParams.containsKey(videoKey)) {
|
if (allTaskParams.containsKey(videoKey)) {
|
||||||
List<SourceEntity> allSources = allTaskParams.get(videoKey);
|
List<SourceEntity> allSources = allTaskParams.get(videoKey);
|
||||||
int actualCount = Math.min(requiredCount.intValue(), allSources.size());
|
List<SourceEntity> selectedSources = selectSources(allSources, requiredCount.intValue(), allowDuplicate);
|
||||||
List<SourceEntity> selectedSources = allSources.subList(0, actualCount);
|
if (!selectedSources.isEmpty()) {
|
||||||
filteredParams.put(videoKey, new ArrayList<>(selectedSources));
|
filteredParams.put(videoKey, selectedSources);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug("filterTaskParams: templateId:{}, original keys:{}, filtered keys:{}, placeholder counts:{}",
|
log.debug("filterTaskParams: templateId:{}, original keys:{}, filtered keys:{}, placeholder counts:{}, allowDuplicate:{}",
|
||||||
templateId, allTaskParams.keySet().size(), filteredParams.keySet().size(), placeholderCounts);
|
templateId, allTaskParams.keySet().size(), filteredParams.keySet().size(), placeholderCounts, allowDuplicate);
|
||||||
|
|
||||||
return filteredParams;
|
return filteredParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<SourceEntity> selectSources(List<SourceEntity> allSources, int requiredCount, boolean allowDuplicate) {
|
||||||
|
if (allSources == null || allSources.isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allowDuplicate) {
|
||||||
|
// 不允许重复,使用原有逻辑
|
||||||
|
int actualCount = Math.min(requiredCount, allSources.size());
|
||||||
|
return new ArrayList<>(allSources.subList(0, actualCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 允许重复,循环填充到所需数量
|
||||||
|
List<SourceEntity> selectedSources = new ArrayList<>();
|
||||||
|
int sourceIndex = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < requiredCount; i++) {
|
||||||
|
selectedSources.add(allSources.get(sourceIndex));
|
||||||
|
sourceIndex = (sourceIndex + 1) % allSources.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedSources;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
109
src/main/java/com/ycwl/basic/config/KafkaConfig.java
Normal file
109
src/main/java/com/ycwl/basic/config/KafkaConfig.java
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package com.ycwl.basic.config;
|
||||||
|
|
||||||
|
import org.apache.kafka.clients.consumer.ConsumerConfig;
|
||||||
|
import org.apache.kafka.clients.producer.ProducerConfig;
|
||||||
|
import org.apache.kafka.common.serialization.StringDeserializer;
|
||||||
|
import org.apache.kafka.common.serialization.StringSerializer;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
|
||||||
|
import org.springframework.kafka.core.*;
|
||||||
|
import org.springframework.kafka.listener.ContainerProperties;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@ConditionalOnProperty(name = "kafka.enabled", havingValue = "true", matchIfMissing = false)
|
||||||
|
public class KafkaConfig {
|
||||||
|
|
||||||
|
@Value("${kafka.bootstrap-servers:100.64.0.12:39092}")
|
||||||
|
private String bootstrapServers;
|
||||||
|
|
||||||
|
@Value("${kafka.consumer.group-id:liuying-microservice}")
|
||||||
|
private String consumerGroupId;
|
||||||
|
|
||||||
|
@Value("${kafka.consumer.auto-offset-reset:earliest}")
|
||||||
|
private String autoOffsetReset;
|
||||||
|
|
||||||
|
@Value("${kafka.producer.acks:all}")
|
||||||
|
private String acks;
|
||||||
|
|
||||||
|
@Value("${kafka.producer.retries:3}")
|
||||||
|
private Integer retries;
|
||||||
|
|
||||||
|
@Value("${kafka.producer.batch-size:16384}")
|
||||||
|
private Integer batchSize;
|
||||||
|
|
||||||
|
@Value("${kafka.producer.linger-ms:1}")
|
||||||
|
private Integer lingerMs;
|
||||||
|
|
||||||
|
@Value("${kafka.producer.buffer-memory:33554432}")
|
||||||
|
private Integer bufferMemory;
|
||||||
|
|
||||||
|
@Value("${kafka.producer.enable-idempotence:true}")
|
||||||
|
private boolean enableIdempotence;
|
||||||
|
|
||||||
|
@Value("${kafka.producer.compression-type:snappy}")
|
||||||
|
private String compressionType;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ProducerFactory<String, String> producerFactory() {
|
||||||
|
Map<String, Object> configProps = new HashMap<>();
|
||||||
|
configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
|
||||||
|
configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
|
||||||
|
configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
|
||||||
|
configProps.put(ProducerConfig.ACKS_CONFIG, acks);
|
||||||
|
configProps.put(ProducerConfig.RETRIES_CONFIG, retries);
|
||||||
|
configProps.put(ProducerConfig.BATCH_SIZE_CONFIG, batchSize);
|
||||||
|
configProps.put(ProducerConfig.LINGER_MS_CONFIG, lingerMs);
|
||||||
|
configProps.put(ProducerConfig.BUFFER_MEMORY_CONFIG, bufferMemory);
|
||||||
|
configProps.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, enableIdempotence);
|
||||||
|
configProps.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, compressionType);
|
||||||
|
|
||||||
|
return new DefaultKafkaProducerFactory<>(configProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public KafkaTemplate<String, String> kafkaTemplate() {
|
||||||
|
return new KafkaTemplate<>(producerFactory());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ConsumerFactory<String, String> consumerFactory() {
|
||||||
|
Map<String, Object> props = new HashMap<>();
|
||||||
|
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
|
||||||
|
props.put(ConsumerConfig.GROUP_ID_CONFIG, consumerGroupId);
|
||||||
|
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
|
||||||
|
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
|
||||||
|
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, autoOffsetReset);
|
||||||
|
|
||||||
|
return new DefaultKafkaConsumerFactory<>(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() {
|
||||||
|
ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
|
||||||
|
factory.setConsumerFactory(consumerFactory());
|
||||||
|
return factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ConcurrentKafkaListenerContainerFactory<String, String> manualCommitKafkaListenerContainerFactory() {
|
||||||
|
ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
|
||||||
|
|
||||||
|
Map<String, Object> props = new HashMap<>();
|
||||||
|
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
|
||||||
|
props.put(ConsumerConfig.GROUP_ID_CONFIG, consumerGroupId);
|
||||||
|
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
|
||||||
|
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
|
||||||
|
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, autoOffsetReset);
|
||||||
|
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
|
||||||
|
|
||||||
|
factory.setConsumerFactory(new DefaultKafkaConsumerFactory<>(props));
|
||||||
|
factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);
|
||||||
|
return factory;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,18 +0,0 @@
|
|||||||
package com.ycwl.basic.constant;
|
|
||||||
|
|
||||||
public class ShareParkingSpaceRedisKeyConstant {
|
|
||||||
// 更改数量时候的锁
|
|
||||||
public final static String UPDATE_NUMBER_LOCK_KEY="ShareParking:updateNumberLockKey";
|
|
||||||
// 地上车位
|
|
||||||
public final static String GROUND_PARKING_SPACE_NUMBER="ShareParking:groundParkingSpaceNumber";
|
|
||||||
// 地下车位数
|
|
||||||
public final static String UNDERGROUND_PARKING_SPACE_NUMBER="ShareParking:undergroundParkingSpaceNumber";
|
|
||||||
// 每日开放预约时间
|
|
||||||
public final static String OPEN_TIME="ShareParking:openTime";
|
|
||||||
// 预约后当日车辆最晚停留时间
|
|
||||||
public final static String RESIDENCE_TIME="ShareParking:residenceTime";
|
|
||||||
//取消时间
|
|
||||||
public final static String CANCEL_TIME="ShareParking:cancelTime";
|
|
||||||
//支付时间
|
|
||||||
public final static String PAY_TIME="ShareParking:payTime";
|
|
||||||
}
|
|
@@ -196,7 +196,7 @@ public class AppOrderV2Controller {
|
|||||||
if (cachedResult.getFinalAmount().compareTo(request.getExpectedFinalAmount()) != 0) {
|
if (cachedResult.getFinalAmount().compareTo(request.getExpectedFinalAmount()) != 0) {
|
||||||
log.warn("移动端下单:价格不匹配, cached={}, expected={}, userId={}, scenicId={}",
|
log.warn("移动端下单:价格不匹配, cached={}, expected={}, userId={}, scenicId={}",
|
||||||
cachedResult.getFinalAmount(), request.getExpectedFinalAmount(), currentUserId, scenicId);
|
cachedResult.getFinalAmount(), request.getExpectedFinalAmount(), currentUserId, scenicId);
|
||||||
return ApiResponse.fail("请重新下单!");
|
return ApiResponse.fail("价格信息变化,请退出后重新查询价格!");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证原价是否匹配(可选)
|
// 验证原价是否匹配(可选)
|
||||||
@@ -215,6 +215,7 @@ public class AppOrderV2Controller {
|
|||||||
Long orderId = oldOrderService.createOrderCompact(currentUserId, request, cachedResult);
|
Long orderId = oldOrderService.createOrderCompact(currentUserId, request, cachedResult);
|
||||||
return ApiResponse.success(String.valueOf(orderId));
|
return ApiResponse.success(String.valueOf(orderId));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
log.warn("移动端下单:订单创建失败, userId={}, scenicId={}, error={}", currentUserId, scenicId, e.getMessage(), e);
|
||||||
return ApiResponse.fail("订单创建失败,请稍后重试");
|
return ApiResponse.fail("订单创建失败,请稍后重试");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -75,8 +75,8 @@ public class AppPrinterController {
|
|||||||
}
|
}
|
||||||
@PostMapping("/uploadTo/{scenicId}/formSource")
|
@PostMapping("/uploadTo/{scenicId}/formSource")
|
||||||
public ApiResponse<?> uploadFromSource(@PathVariable("scenicId") Long scenicId, @RequestBody FromSourceReq req) throws IOException {
|
public ApiResponse<?> uploadFromSource(@PathVariable("scenicId") Long scenicId, @RequestBody FromSourceReq req) throws IOException {
|
||||||
printerService.addUserPhotoFromSource(JwtTokenUtil.getWorker().getUserId(), scenicId, req);
|
List<Integer> list = printerService.addUserPhotoFromSource(JwtTokenUtil.getWorker().getUserId(), scenicId, req);
|
||||||
return ApiResponse.success(null);
|
return ApiResponse.success(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/setQuantity/{scenicId}/{id}")
|
@PostMapping("/setQuantity/{scenicId}/{id}")
|
||||||
|
@@ -72,26 +72,14 @@ public class AppScenicController {
|
|||||||
public ApiResponse<ScenicConfigResp> getConfig(@PathVariable Long id){
|
public ApiResponse<ScenicConfigResp> getConfig(@PathVariable Long id){
|
||||||
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(id);
|
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(id);
|
||||||
ScenicConfigResp resp = new ScenicConfigResp();
|
ScenicConfigResp resp = new ScenicConfigResp();
|
||||||
resp.setBookRoutine(scenicConfig.getInteger("book_routine"));
|
resp.setWatermarkUrl(scenicConfig.getString("watermark_url"));
|
||||||
resp.setForceFinishTime(scenicConfig.getInteger("force_finish_time"));
|
|
||||||
resp.setTourTime(scenicConfig.getInteger("tour_time"));
|
|
||||||
resp.setSampleStoreDay(scenicConfig.getInteger("sample_store_day"));
|
|
||||||
resp.setFaceStoreDay(scenicConfig.getInteger("face_store_day"));
|
|
||||||
resp.setVideoStoreDay(scenicConfig.getInteger("video_store_day"));
|
resp.setVideoStoreDay(scenicConfig.getInteger("video_store_day"));
|
||||||
resp.setAllFree(scenicConfig.getBoolean("all_free"));
|
|
||||||
resp.setDisableSourceVideo(scenicConfig.getBoolean("disable_source_video"));
|
|
||||||
resp.setDisableSourceImage(scenicConfig.getBoolean("disable_source_image"));
|
|
||||||
resp.setAntiScreenRecordType(scenicConfig.getInteger("anti_screen_record_type"));
|
resp.setAntiScreenRecordType(scenicConfig.getInteger("anti_screen_record_type"));
|
||||||
resp.setVideoSourceStoreDay(scenicConfig.getInteger("video_source_store_day"));
|
resp.setGroupingEnable(scenicConfig.getBoolean("grouping_enable", false));
|
||||||
resp.setImageSourceStoreDay(scenicConfig.getInteger("image_source_store_day"));
|
resp.setVoucherEnable(scenicConfig.getBoolean("voucher_enable", false));
|
||||||
resp.setUserSourceExpireDay(scenicConfig.getInteger("user_source_expire_day"));
|
resp.setShowPhotoWhenWaiting(scenicConfig.getBoolean("show_photo_when_waiting", false));
|
||||||
resp.setBrokerDirectRate(scenicConfig.getBigDecimal("broker_direct_rate"));
|
|
||||||
resp.setVideoSourcePackHint(scenicConfig.getString("video_source_pack_hint"));
|
|
||||||
resp.setImageSourcePackHint(scenicConfig.getString("image_source_pack_hint"));
|
resp.setImageSourcePackHint(scenicConfig.getString("image_source_pack_hint"));
|
||||||
resp.setVoucherEnable(scenicConfig.getBoolean("voucher_enable"));
|
resp.setVideoSourcePackHint(scenicConfig.getString("video_source_pack_hint"));
|
||||||
resp.setEnableVoucher(scenicConfig.getBoolean("voucher_enable")); // compactible
|
|
||||||
resp.setGroupingEnable(scenicConfig.getBoolean("grouping_enable"));
|
|
||||||
resp.setShowPhotoWhenWaiting(scenicConfig.getBoolean("show_photo_when_waiting"));
|
|
||||||
return ApiResponse.success(resp);
|
return ApiResponse.success(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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("成功");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -25,7 +25,7 @@ public class CouponController {
|
|||||||
@GetMapping("/{scenicId}/goodsList")
|
@GetMapping("/{scenicId}/goodsList")
|
||||||
public ApiResponse<List<GoodsListRespVO>> scenicGoodsList(@PathVariable Long scenicId) {
|
public ApiResponse<List<GoodsListRespVO>> scenicGoodsList(@PathVariable Long scenicId) {
|
||||||
List<GoodsListRespVO> data = priceBiz.listGoodsByScenic(scenicId);
|
List<GoodsListRespVO> data = priceBiz.listGoodsByScenic(scenicId);
|
||||||
data.add(new GoodsListRespVO(-1L, "一口价"));
|
data.add(new GoodsListRespVO(-1L, "一口价", -1));
|
||||||
return ApiResponse.success(data);
|
return ApiResponse.success(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -3,8 +3,10 @@ package com.ycwl.basic.controller.pc;
|
|||||||
import com.ycwl.basic.integration.device.dto.config.*;
|
import com.ycwl.basic.integration.device.dto.config.*;
|
||||||
import com.ycwl.basic.integration.common.response.PageResponse;
|
import com.ycwl.basic.integration.common.response.PageResponse;
|
||||||
import com.ycwl.basic.integration.device.dto.device.*;
|
import com.ycwl.basic.integration.device.dto.device.*;
|
||||||
|
import com.ycwl.basic.integration.device.dto.status.DeviceStatusDTO;
|
||||||
import com.ycwl.basic.integration.device.service.DeviceConfigIntegrationService;
|
import com.ycwl.basic.integration.device.service.DeviceConfigIntegrationService;
|
||||||
import com.ycwl.basic.integration.device.service.DeviceIntegrationService;
|
import com.ycwl.basic.integration.device.service.DeviceIntegrationService;
|
||||||
|
import com.ycwl.basic.integration.device.service.DeviceStatusIntegrationService;
|
||||||
import com.ycwl.basic.utils.ApiResponse;
|
import com.ycwl.basic.utils.ApiResponse;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -29,6 +31,7 @@ public class DeviceV2Controller {
|
|||||||
|
|
||||||
private final DeviceIntegrationService deviceIntegrationService;
|
private final DeviceIntegrationService deviceIntegrationService;
|
||||||
private final DeviceConfigIntegrationService deviceConfigIntegrationService;
|
private final DeviceConfigIntegrationService deviceConfigIntegrationService;
|
||||||
|
private final DeviceStatusIntegrationService deviceStatusIntegrationService;
|
||||||
|
|
||||||
// ========== 设备基础 CRUD 操作 ==========
|
// ========== 设备基础 CRUD 操作 ==========
|
||||||
|
|
||||||
@@ -60,34 +63,6 @@ public class DeviceV2Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 设备V2带配置信息分页列表
|
|
||||||
*/
|
|
||||||
@GetMapping("/with-config")
|
|
||||||
public ApiResponse<PageResponse<DeviceV2WithConfigDTO>> listDevicesWithConfig(@RequestParam(defaultValue = "1") Integer page,
|
|
||||||
@RequestParam(defaultValue = "10") Integer pageSize,
|
|
||||||
@RequestParam(required = false) String name,
|
|
||||||
@RequestParam(required = false) String no,
|
|
||||||
@RequestParam(required = false) String type,
|
|
||||||
@RequestParam(required = false) Integer isActive,
|
|
||||||
@RequestParam(required = false) Long scenicId) {
|
|
||||||
log.info("分页查询设备带配置信息列表, page: {}, pageSize: {}, name: {}, no: {}, type: {}, isActive: {}, scenicId: {}",
|
|
||||||
page, pageSize, name, no, type, isActive, scenicId);
|
|
||||||
|
|
||||||
// 参数验证:限制pageSize最大值为100
|
|
||||||
if (pageSize > 100) {
|
|
||||||
pageSize = 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
PageResponse<DeviceV2WithConfigDTO> response = deviceIntegrationService.listDevicesWithConfig(page, pageSize, name, no, type, isActive, scenicId);
|
|
||||||
return ApiResponse.success(response);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("分页查询设备带配置信息列表失败", e);
|
|
||||||
return ApiResponse.fail("分页查询设备带配置信息列表失败: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据ID获取设备信息
|
* 根据ID获取设备信息
|
||||||
*/
|
*/
|
||||||
@@ -102,20 +77,6 @@ public class DeviceV2Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据ID获取设备带配置信息
|
|
||||||
*/
|
|
||||||
@GetMapping("/{id}/with-config")
|
|
||||||
public ApiResponse<DeviceV2WithConfigDTO> getDeviceWithConfig(@PathVariable Long id) {
|
|
||||||
try {
|
|
||||||
DeviceV2WithConfigDTO device = deviceIntegrationService.getDeviceWithConfig(id);
|
|
||||||
return ApiResponse.success(device);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("获取设备配置信息失败, id: {}", id, e);
|
|
||||||
return ApiResponse.fail("获取设备配置信息失败: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据设备编号获取设备信息
|
* 根据设备编号获取设备信息
|
||||||
*/
|
*/
|
||||||
@@ -131,16 +92,24 @@ public class DeviceV2Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据设备编号获取设备带配置信息
|
* 根据设备ID获取设备在线状态
|
||||||
*/
|
*/
|
||||||
@GetMapping("/no/{no}/with-config")
|
@GetMapping("/{id}/status")
|
||||||
public ApiResponse<DeviceV2WithConfigDTO> getDeviceWithConfigByNo(@PathVariable String no) {
|
public ApiResponse<DeviceStatusDTO> getDeviceOnlineStatus(@PathVariable Long id) {
|
||||||
|
log.info("获取设备在线状态, deviceId: {}", id);
|
||||||
try {
|
try {
|
||||||
DeviceV2WithConfigDTO device = deviceIntegrationService.getDeviceWithConfigByNo(no);
|
// 首先获取设备信息以获得设备编号
|
||||||
return ApiResponse.success(device);
|
DeviceV2DTO device = deviceIntegrationService.getDevice(id);
|
||||||
|
if (device == null) {
|
||||||
|
return ApiResponse.fail("设备不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用设备编号查询在线状态
|
||||||
|
DeviceStatusDTO onlineStatus = deviceStatusIntegrationService.getDeviceStatus(device.getNo());
|
||||||
|
return ApiResponse.success(onlineStatus);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("根据设备编号获取设备配置信息失败, no: {}", no, e);
|
log.error("获取设备在线状态失败, deviceId: {}", id, e);
|
||||||
return ApiResponse.fail("根据设备编号获取设备配置信息失败: " + e.getMessage());
|
return ApiResponse.fail("获取设备在线状态失败: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,20 +271,6 @@ public class DeviceV2Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取设备扁平化配置
|
|
||||||
*/
|
|
||||||
@GetMapping("/{id}/flat-config")
|
|
||||||
public ApiResponse<Map<String, Object>> getDeviceFlatConfig(@PathVariable Long id) {
|
|
||||||
try {
|
|
||||||
Map<String, Object> config = deviceConfigIntegrationService.getDeviceFlatConfig(id);
|
|
||||||
return ApiResponse.success(config);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("获取设备扁平化配置失败, deviceId: {}", id, e);
|
|
||||||
return ApiResponse.fail("获取设备扁平化配置失败: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据配置键获取配置
|
* 根据配置键获取配置
|
||||||
*/
|
*/
|
||||||
@@ -346,21 +301,6 @@ public class DeviceV2Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据设备编号获取扁平化配置
|
|
||||||
*/
|
|
||||||
@GetMapping("/no/{no}/flat-config")
|
|
||||||
public ApiResponse<Map<String, Object>> getDeviceFlatConfigByNo(@PathVariable String no) {
|
|
||||||
log.info("根据设备编号获取扁平化配置, deviceNo: {}", no);
|
|
||||||
try {
|
|
||||||
Map<String, Object> config = deviceConfigIntegrationService.getDeviceFlatConfigByNo(no);
|
|
||||||
return ApiResponse.success(config);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("根据设备编号获取扁平化配置失败, deviceNo: {}", no, e);
|
|
||||||
return ApiResponse.fail("根据设备编号获取扁平化配置失败: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建设备配置
|
* 创建设备配置
|
||||||
*/
|
*/
|
||||||
|
@@ -0,0 +1,60 @@
|
|||||||
|
package com.ycwl.basic.controller.pc;
|
||||||
|
|
||||||
|
import com.ycwl.basic.integration.message.dto.ChannelsResponse;
|
||||||
|
import com.ycwl.basic.integration.message.dto.MessageListData;
|
||||||
|
import com.ycwl.basic.integration.message.service.MessageIntegrationService;
|
||||||
|
import com.ycwl.basic.utils.ApiResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/message/v1")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MessageController {
|
||||||
|
|
||||||
|
private final MessageIntegrationService messageService;
|
||||||
|
|
||||||
|
@GetMapping("/messages")
|
||||||
|
public ApiResponse<MessageListData> listMessages(
|
||||||
|
@RequestParam(defaultValue = "1") Integer page,
|
||||||
|
@RequestParam(defaultValue = "20") Integer pageSize,
|
||||||
|
@RequestParam(required = false) String channelId,
|
||||||
|
@RequestParam(required = false) String title,
|
||||||
|
@RequestParam(required = false) String content,
|
||||||
|
@RequestParam(required = false) String sendBiz,
|
||||||
|
@RequestParam(required = false) String sentAtStart,
|
||||||
|
@RequestParam(required = false) String sentAtEnd,
|
||||||
|
@RequestParam(required = false) String createdAtStart,
|
||||||
|
@RequestParam(required = false) String createdAtEnd
|
||||||
|
) {
|
||||||
|
log.debug("PC|消息列表查询 page={}, pageSize={}, channelId={}, title={}, sendBiz={}", page, pageSize, channelId, title, sendBiz);
|
||||||
|
if (pageSize > 100) {
|
||||||
|
pageSize = 100;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
MessageListData data = messageService.listMessages(page, pageSize, channelId, title, content, sendBiz,
|
||||||
|
sentAtStart, sentAtEnd, createdAtStart, createdAtEnd);
|
||||||
|
return ApiResponse.success(data);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("PC|消息列表查询失败", e);
|
||||||
|
return ApiResponse.fail("消息列表查询失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/channels")
|
||||||
|
public ApiResponse<ChannelsResponse> listChannels() {
|
||||||
|
log.debug("PC|获取消息通道列表");
|
||||||
|
try {
|
||||||
|
ChannelsResponse data = messageService.listChannels();
|
||||||
|
return ApiResponse.success(data);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("PC|获取消息通道列表失败", e);
|
||||||
|
return ApiResponse.fail("获取消息通道列表失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -10,7 +10,6 @@ import com.ycwl.basic.integration.scenic.dto.filter.ScenicFilterRequest;
|
|||||||
import com.ycwl.basic.integration.scenic.dto.scenic.CreateScenicRequest;
|
import com.ycwl.basic.integration.scenic.dto.scenic.CreateScenicRequest;
|
||||||
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
|
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
|
||||||
import com.ycwl.basic.integration.common.response.PageResponse;
|
import com.ycwl.basic.integration.common.response.PageResponse;
|
||||||
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2WithConfigDTO;
|
|
||||||
import com.ycwl.basic.integration.scenic.dto.scenic.UpdateScenicRequest;
|
import com.ycwl.basic.integration.scenic.dto.scenic.UpdateScenicRequest;
|
||||||
import com.ycwl.basic.integration.scenic.service.ScenicConfigIntegrationService;
|
import com.ycwl.basic.integration.scenic.service.ScenicConfigIntegrationService;
|
||||||
import com.ycwl.basic.integration.scenic.service.ScenicIntegrationService;
|
import com.ycwl.basic.integration.scenic.service.ScenicIntegrationService;
|
||||||
@@ -71,30 +70,6 @@ public class ScenicV2Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 景区V2带配置信息分页列表
|
|
||||||
*/
|
|
||||||
@GetMapping("/with-config")
|
|
||||||
public ApiResponse<PageResponse<ScenicV2WithConfigDTO>> listScenicsWithConfig(@RequestParam(defaultValue = "1") Integer page,
|
|
||||||
@RequestParam(defaultValue = "10") Integer pageSize,
|
|
||||||
@RequestParam(required = false) Integer status,
|
|
||||||
@RequestParam(required = false) String name) {
|
|
||||||
log.info("分页查询景区带配置信息列表, page: {}, pageSize: {}, status: {}, name: {}", page, pageSize, status, name);
|
|
||||||
|
|
||||||
// 参数验证:限制pageSize最大值为100
|
|
||||||
if (pageSize > 100) {
|
|
||||||
pageSize = 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
PageResponse<ScenicV2WithConfigDTO> response = scenicIntegrationService.listScenicsWithConfig(page, pageSize, status, name);
|
|
||||||
return ApiResponse.success(response);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("分页查询景区带配置信息列表失败", e);
|
|
||||||
return ApiResponse.fail("分页查询景区带配置信息列表失败: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询单个景区详情
|
* 查询单个景区详情
|
||||||
*/
|
*/
|
||||||
@@ -192,36 +167,6 @@ public class ScenicV2Controller {
|
|||||||
|
|
||||||
// ========== 景区配置管理 ==========
|
// ========== 景区配置管理 ==========
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取景区及其配置信息
|
|
||||||
*/
|
|
||||||
@GetMapping("/{scenicId}/with-config")
|
|
||||||
public ApiResponse<ScenicV2WithConfigDTO> getScenicWithConfig(@PathVariable Long scenicId) {
|
|
||||||
log.info("获取景区配置信息, scenicId: {}", scenicId);
|
|
||||||
try {
|
|
||||||
ScenicV2WithConfigDTO scenic = scenicIntegrationService.getScenicWithConfig(scenicId);
|
|
||||||
return ApiResponse.success(scenic);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("获取景区配置信息失败, scenicId: {}", scenicId, e);
|
|
||||||
return ApiResponse.fail("获取景区配置信息失败: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取景区扁平化配置
|
|
||||||
*/
|
|
||||||
@GetMapping("/{scenicId}/flat-config")
|
|
||||||
public ApiResponse<Map<String, Object>> getScenicFlatConfig(@PathVariable Long scenicId) {
|
|
||||||
log.info("获取景区扁平化配置, scenicId: {}", scenicId);
|
|
||||||
try {
|
|
||||||
Map<String, Object> config = scenicIntegrationService.getScenicFlatConfig(scenicId);
|
|
||||||
return ApiResponse.success(config);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("获取景区扁平化配置失败, scenicId: {}", scenicId, e);
|
|
||||||
return ApiResponse.fail("获取景区扁平化配置失败: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取景区配置列表
|
* 获取景区配置列表
|
||||||
*/
|
*/
|
||||||
@@ -316,20 +261,4 @@ public class ScenicV2Controller {
|
|||||||
return ApiResponse.fail("批量更新配置失败: " + e.getMessage());
|
return ApiResponse.fail("批量更新配置失败: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 扁平化批量更新景区配置
|
|
||||||
*/
|
|
||||||
@PutMapping("/{scenicId}/flat-config")
|
|
||||||
public ApiResponse<BatchUpdateResponse> batchFlatUpdateConfigs(@PathVariable Long scenicId,
|
|
||||||
@RequestBody Map<String, Object> configs) {
|
|
||||||
log.info("扁平化批量更新景区配置, scenicId: {}, configs count: {}", scenicId, configs.size());
|
|
||||||
try {
|
|
||||||
BatchUpdateResponse response = scenicConfigIntegrationService.batchFlatUpdateConfigs(scenicId, configs);
|
|
||||||
return ApiResponse.success(response);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("扁平化批量更新景区配置失败, scenicId: {}", scenicId, e);
|
|
||||||
return ApiResponse.fail("扁平化批量更新配置失败: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@@ -101,9 +101,11 @@ public class ViidController {
|
|||||||
.setNamePrefix("VIID-" + scenicId + "-t")
|
.setNamePrefix("VIID-" + scenicId + "-t")
|
||||||
.build();
|
.build();
|
||||||
return new ThreadPoolExecutor(
|
return new ThreadPoolExecutor(
|
||||||
4, 1024, 0L, TimeUnit.MILLISECONDS,
|
8, 32, 10L, TimeUnit.SECONDS, // 核心2个线程,最大20个线程,空闲60秒回收
|
||||||
new ArrayBlockingQueue<>(1024),
|
new ArrayBlockingQueue<>(1024), // 队列大小从1024降至100
|
||||||
threadFactory);
|
threadFactory,
|
||||||
|
new ThreadPoolExecutor.CallerRunsPolicy() // 队列满时由调用线程执行,提供背压控制
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
65
src/main/java/com/ycwl/basic/dto/ZTSourceMessage.java
Normal file
65
src/main/java/com/ycwl/basic/dto/ZTSourceMessage.java
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package com.ycwl.basic.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ZT-Source Kafka消息实体
|
||||||
|
* 用于接收素材数据(照片和视频片段)
|
||||||
|
*
|
||||||
|
* @author system
|
||||||
|
* @date 2024/12/27
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ZTSourceMessage {
|
||||||
|
@JsonProperty("sourceId")
|
||||||
|
private Long sourceId;
|
||||||
|
|
||||||
|
@JsonProperty("sourceType")
|
||||||
|
private Integer sourceType;
|
||||||
|
|
||||||
|
@JsonProperty("scenicId")
|
||||||
|
private Long scenicId;
|
||||||
|
|
||||||
|
@JsonProperty("deviceId")
|
||||||
|
private Long deviceId;
|
||||||
|
|
||||||
|
@JsonProperty("shootTime")
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||||
|
private Date shootTime;
|
||||||
|
|
||||||
|
@JsonProperty("thumbnailUrl")
|
||||||
|
private String thumbnailUrl;
|
||||||
|
|
||||||
|
@JsonProperty("sourceUrl")
|
||||||
|
private String sourceUrl;
|
||||||
|
|
||||||
|
@JsonProperty("resolution")
|
||||||
|
private String resolution;
|
||||||
|
|
||||||
|
@JsonProperty("faceSampleId")
|
||||||
|
private Long faceSampleId;
|
||||||
|
|
||||||
|
@JsonProperty("posJson")
|
||||||
|
private String posJson;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否为视频片段
|
||||||
|
*/
|
||||||
|
public boolean isVideo() {
|
||||||
|
return sourceType != null && sourceType == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否为照片
|
||||||
|
*/
|
||||||
|
public boolean isPhoto() {
|
||||||
|
return sourceType != null && sourceType == 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -25,6 +25,7 @@ Currently implemented:
|
|||||||
- **Device Integration** (`com.ycwl.basic.integration.device`): ZT-Device microservice integration
|
- **Device Integration** (`com.ycwl.basic.integration.device`): ZT-Device microservice integration
|
||||||
- **Render Worker Integration** (`com.ycwl.basic.integration.render`): ZT-Render-Worker microservice integration
|
- **Render Worker Integration** (`com.ycwl.basic.integration.render`): ZT-Render-Worker microservice integration
|
||||||
- **Questionnaire Integration** (`com.ycwl.basic.integration.questionnaire`): ZT-Questionnaire microservice integration
|
- **Questionnaire Integration** (`com.ycwl.basic.integration.questionnaire`): ZT-Questionnaire microservice integration
|
||||||
|
- **Message Integration** (`com.ycwl.basic.integration.message`): ZT-Message Kafka producer integration
|
||||||
|
|
||||||
### Integration Pattern
|
### Integration Pattern
|
||||||
|
|
||||||
@@ -34,8 +35,7 @@ service/
|
|||||||
├── client/ # Feign clients for HTTP calls
|
├── client/ # Feign clients for HTTP calls
|
||||||
├── config/ # Service-specific configuration
|
├── config/ # Service-specific configuration
|
||||||
├── dto/ # Data transfer objects
|
├── dto/ # Data transfer objects
|
||||||
├── service/ # Service layer with business logic
|
└── service/ # Service layer with business logic
|
||||||
└── example/ # Usage examples
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Integration Fallback Mechanism
|
## Integration Fallback Mechanism
|
||||||
@@ -792,13 +792,6 @@ mvn test -Dtest=DefaultConfigIntegrationServiceTest
|
|||||||
|
|
||||||
# Run all device integration tests (including default configs)
|
# Run all device integration tests (including default configs)
|
||||||
mvn test -Dtest="com.ycwl.basic.integration.device.*Test"
|
mvn test -Dtest="com.ycwl.basic.integration.device.*Test"
|
||||||
|
|
||||||
# Enable example runner in application-dev.yml
|
|
||||||
integration:
|
|
||||||
device:
|
|
||||||
example:
|
|
||||||
default-config:
|
|
||||||
enabled: true
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Common Configuration Keys
|
### Common Configuration Keys
|
||||||
@@ -820,8 +813,7 @@ com.ycwl.basic.integration.{service-name}/
|
|||||||
├── client/
|
├── client/
|
||||||
├── config/
|
├── config/
|
||||||
├── dto/
|
├── dto/
|
||||||
├── service/
|
└── service/
|
||||||
└── example/
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Add Configuration Properties
|
### 2. Add Configuration Properties
|
||||||
@@ -1168,6 +1160,57 @@ fallbackService.clearAllFallbackCache("zt-render-worker");
|
|||||||
- **Active (isActive=1)**: Worker is available for tasks
|
- **Active (isActive=1)**: Worker is available for tasks
|
||||||
- **Inactive (isActive=0)**: Worker is disabled
|
- **Inactive (isActive=0)**: Worker is disabled
|
||||||
|
|
||||||
|
## ZT-Message Integration (Kafka Producer)
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
The zt-message microservice accepts messages via Kafka on topic `zt-message`. This integration provides a simple producer service to publish notification messages.
|
||||||
|
|
||||||
|
- Topic: `zt-message`
|
||||||
|
- Key: Use `channelId` for partitioning stability
|
||||||
|
- Value: UTF-8 JSON with fields: `channelId` (required), `title` (required), `content` (required), `target` (required), `extra` (object, optional), `sendReason` (optional), `sendBiz` (optional)
|
||||||
|
|
||||||
|
### Components
|
||||||
|
- `com.ycwl.basic.integration.message.dto.ZtMessage`: DTO for message body
|
||||||
|
- `com.ycwl.basic.integration.message.service.ZtMessageProducerService`: Producer service using Spring Kafka
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
```yaml
|
||||||
|
kafka:
|
||||||
|
enabled: true # enable Kafka integration
|
||||||
|
bootstrap-servers: 127.0.0.1:9092 # adjust per environment
|
||||||
|
zt-message-topic: zt-message # topic name (default already zt-message)
|
||||||
|
producer:
|
||||||
|
acks: all
|
||||||
|
enable-idempotence: true
|
||||||
|
retries: 5
|
||||||
|
linger-ms: 10
|
||||||
|
batch-size: 32768
|
||||||
|
compression-type: snappy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
```java
|
||||||
|
@Autowired
|
||||||
|
private ZtMessageProducerService producer;
|
||||||
|
|
||||||
|
public void sendWelcome() {
|
||||||
|
ZtMessage msg = ZtMessage.of("dummy", "欢迎", "注册成功", "user-001");
|
||||||
|
Map<String, Object> extra = new HashMap<>();
|
||||||
|
extra.put("k", "v");
|
||||||
|
msg.setExtra(extra);
|
||||||
|
msg.setSendReason("REGISTER");
|
||||||
|
msg.setSendBiz("USER");
|
||||||
|
|
||||||
|
producer.send(msg); // key uses channelId, value is JSON
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
- Required fields must be non-empty: `channelId`, `title`, `content`, `target`
|
||||||
|
- Keep message body small (< 100 KB)
|
||||||
|
- Use string for 64-bit integers in `extra` to avoid JS precision loss
|
||||||
|
- Service logs the partition/offset upon success, errors on failure
|
||||||
|
|
||||||
## Common Development Tasks
|
## Common Development Tasks
|
||||||
|
|
||||||
### Running Integration Tests
|
### Running Integration Tests
|
||||||
|
@@ -16,8 +16,6 @@ import java.util.stream.Collectors;
|
|||||||
*/
|
*/
|
||||||
public class ScenicConfigManager extends ConfigManager<ScenicConfigV2DTO> {
|
public class ScenicConfigManager extends ConfigManager<ScenicConfigV2DTO> {
|
||||||
|
|
||||||
private final Map<String, Object> configMap;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从配置列表构造管理器
|
* 从配置列表构造管理器
|
||||||
*
|
*
|
||||||
@@ -25,26 +23,7 @@ public class ScenicConfigManager extends ConfigManager<ScenicConfigV2DTO> {
|
|||||||
*/
|
*/
|
||||||
public ScenicConfigManager(List<ScenicConfigV2DTO> configList) {
|
public ScenicConfigManager(List<ScenicConfigV2DTO> configList) {
|
||||||
super(configList);
|
super(configList);
|
||||||
this.configMap = new HashMap<>();
|
|
||||||
if (configList != null) {
|
|
||||||
for (ScenicConfigV2DTO config : configList) {
|
|
||||||
if (config.getConfigKey() != null && config.getConfigValue() != null) {
|
|
||||||
this.configMap.put(config.getConfigKey(), config.getConfigValue());
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从配置Map构造管理器
|
|
||||||
*
|
|
||||||
* @param configMap 配置Map
|
|
||||||
*/
|
|
||||||
public ScenicConfigManager(Map<String, Object> configMap) {
|
|
||||||
super(null); // 使用Map构造时,父类configs为null
|
|
||||||
this.configMap = configMap != null ? new HashMap<>(configMap) : new HashMap<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String getConfigKey(ScenicConfigV2DTO config) {
|
protected String getConfigKey(ScenicConfigV2DTO config) {
|
||||||
return config != null ? config.getConfigKey() : null;
|
return config != null ? config.getConfigKey() : null;
|
||||||
@@ -55,276 +34,4 @@ public class ScenicConfigManager extends ConfigManager<ScenicConfigV2DTO> {
|
|||||||
return config != null ? config.getConfigValue() : null;
|
return config != null ? config.getConfigValue() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取长整数值
|
|
||||||
*
|
|
||||||
* @param key 配置键
|
|
||||||
* @return Long值,如果键不存在或转换失败返回null
|
|
||||||
*/
|
|
||||||
public Long getLong(String key) {
|
|
||||||
return ConfigValueUtil.getLongValue(configMap, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取长整数值,如果为null则返回默认值
|
|
||||||
*
|
|
||||||
* @param key 配置键
|
|
||||||
* @param defaultValue 默认值
|
|
||||||
* @return Long值或默认值
|
|
||||||
*/
|
|
||||||
public Long getLong(String key, Long defaultValue) {
|
|
||||||
Long value = ConfigValueUtil.getLongValue(configMap, key);
|
|
||||||
return value != null ? value : defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取浮点数值
|
|
||||||
*
|
|
||||||
* @param key 配置键
|
|
||||||
* @return Float值,如果键不存在或转换失败返回null
|
|
||||||
*/
|
|
||||||
public Float getFloat(String key) {
|
|
||||||
return ConfigValueUtil.getFloatValue(configMap, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取浮点数值,如果为null则返回默认值
|
|
||||||
*
|
|
||||||
* @param key 配置键
|
|
||||||
* @param defaultValue 默认值
|
|
||||||
* @return Float值或默认值
|
|
||||||
*/
|
|
||||||
public Float getFloat(String key, Float defaultValue) {
|
|
||||||
Float value = ConfigValueUtil.getFloatValue(configMap, key);
|
|
||||||
return value != null ? value : defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取双精度浮点数值
|
|
||||||
*
|
|
||||||
* @param key 配置键
|
|
||||||
* @return Double值,如果键不存在或转换失败返回null
|
|
||||||
*/
|
|
||||||
public Double getDouble(String key) {
|
|
||||||
return ConfigValueUtil.getDoubleValue(configMap, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取双精度浮点数值,如果为null则返回默认值
|
|
||||||
*
|
|
||||||
* @param key 配置键
|
|
||||||
* @param defaultValue 默认值
|
|
||||||
* @return Double值或默认值
|
|
||||||
*/
|
|
||||||
public Double getDouble(String key, Double defaultValue) {
|
|
||||||
Double value = ConfigValueUtil.getDoubleValue(configMap, key);
|
|
||||||
return value != null ? value : defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取高精度小数值
|
|
||||||
*
|
|
||||||
* @param key 配置键
|
|
||||||
* @return BigDecimal值,如果键不存在或转换失败返回null
|
|
||||||
*/
|
|
||||||
public BigDecimal getBigDecimal(String key) {
|
|
||||||
return ConfigValueUtil.getBigDecimalValue(configMap, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取高精度小数值,如果为null则返回默认值
|
|
||||||
*
|
|
||||||
* @param key 配置键
|
|
||||||
* @param defaultValue 默认值
|
|
||||||
* @return BigDecimal值或默认值
|
|
||||||
*/
|
|
||||||
public BigDecimal getBigDecimal(String key, BigDecimal defaultValue) {
|
|
||||||
BigDecimal value = ConfigValueUtil.getBigDecimalValue(configMap, key);
|
|
||||||
return value != null ? value : defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取布尔值
|
|
||||||
*
|
|
||||||
* @param key 配置键
|
|
||||||
* @return Boolean值,如果键不存在或转换失败返回null
|
|
||||||
*/
|
|
||||||
public Boolean getBoolean(String key) {
|
|
||||||
return ConfigValueUtil.getBooleanValue(configMap, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取布尔值,如果为null则返回默认值
|
|
||||||
*
|
|
||||||
* @param key 配置键
|
|
||||||
* @param defaultValue 默认值
|
|
||||||
* @return Boolean值或默认值
|
|
||||||
*/
|
|
||||||
public Boolean getBoolean(String key, Boolean defaultValue) {
|
|
||||||
return ConfigValueUtil.getBooleanValue(configMap, key, defaultValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查配置键是否存在
|
|
||||||
*
|
|
||||||
* @param key 配置键
|
|
||||||
* @return true如果键存在,false如果不存在
|
|
||||||
*/
|
|
||||||
public boolean hasKey(String key) {
|
|
||||||
return ConfigValueUtil.hasKey(configMap, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查配置键是否存在且值不为null
|
|
||||||
*
|
|
||||||
* @param key 配置键
|
|
||||||
* @return true如果键存在且值不为null
|
|
||||||
*/
|
|
||||||
public boolean hasNonNullValue(String key) {
|
|
||||||
return ConfigValueUtil.hasNonNullValue(configMap, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有配置键
|
|
||||||
*
|
|
||||||
* @return 配置键集合
|
|
||||||
*/
|
|
||||||
public Set<String> getAllKeys() {
|
|
||||||
return new HashSet<>(configMap.keySet());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取配置项数量
|
|
||||||
*
|
|
||||||
* @return 配置项数量
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public int size() {
|
|
||||||
return configMap.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查配置是否为空
|
|
||||||
*
|
|
||||||
* @return true如果没有配置项
|
|
||||||
*/
|
|
||||||
public boolean isEmpty() {
|
|
||||||
return configMap.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有配置的拷贝
|
|
||||||
*
|
|
||||||
* @return 配置Map的拷贝
|
|
||||||
*/
|
|
||||||
public Map<String, Object> getAllConfigsAsMap() {
|
|
||||||
return new HashMap<>(configMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据键前缀过滤配置
|
|
||||||
*
|
|
||||||
* @param prefix 键前缀
|
|
||||||
* @return 匹配前缀的配置Map
|
|
||||||
*/
|
|
||||||
public Map<String, Object> getConfigsByPrefix(String prefix) {
|
|
||||||
if (prefix == null) {
|
|
||||||
return new HashMap<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
return configMap.entrySet().stream()
|
|
||||||
.filter(entry -> entry.getKey() != null && entry.getKey().startsWith(prefix))
|
|
||||||
.collect(Collectors.toMap(
|
|
||||||
Map.Entry::getKey,
|
|
||||||
Map.Entry::getValue
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建新的ScenicConfigManager,包含当前配置的子集
|
|
||||||
*
|
|
||||||
* @param keys 要包含的配置键
|
|
||||||
* @return 包含指定键配置的新管理器
|
|
||||||
*/
|
|
||||||
public ScenicConfigManager subset(Set<String> keys) {
|
|
||||||
Map<String, Object> subsetMap = new HashMap<>();
|
|
||||||
if (keys != null) {
|
|
||||||
for (String key : keys) {
|
|
||||||
if (configMap.containsKey(key)) {
|
|
||||||
subsetMap.put(key, configMap.get(key));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new ScenicConfigManager(subsetMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将配置转换为扁平化的Map,键名转换为驼峰形式
|
|
||||||
*
|
|
||||||
* @return 扁平化的配置Map,键为驼峰形式
|
|
||||||
*/
|
|
||||||
public Map<String, Object> toFlatConfig() {
|
|
||||||
Map<String, Object> flatConfig = new HashMap<>();
|
|
||||||
|
|
||||||
for (Map.Entry<String, Object> entry : configMap.entrySet()) {
|
|
||||||
String key = entry.getKey();
|
|
||||||
Object value = entry.getValue();
|
|
||||||
|
|
||||||
if (key != null) {
|
|
||||||
String camelCaseKey = toCamelCase(key);
|
|
||||||
flatConfig.put(camelCaseKey, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return flatConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将字符串转换为驼峰形式
|
|
||||||
* 支持下划线、短横线、点号分隔的字符串转换
|
|
||||||
*
|
|
||||||
* @param str 原始字符串
|
|
||||||
* @return 驼峰形式的字符串
|
|
||||||
*/
|
|
||||||
private String toCamelCase(String str) {
|
|
||||||
if (str == null || str.isEmpty()) {
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 支持下划线、短横线、点号作为分隔符
|
|
||||||
String[] parts = str.split("[_\\-.]");
|
|
||||||
|
|
||||||
if (parts.length <= 1) {
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
StringBuilder camelCase = new StringBuilder();
|
|
||||||
|
|
||||||
// 第一部分保持原样(全小写)
|
|
||||||
camelCase.append(parts[0].toLowerCase());
|
|
||||||
|
|
||||||
// 后续部分首字母大写
|
|
||||||
for (int i = 1; i < parts.length; i++) {
|
|
||||||
String part = parts[i];
|
|
||||||
if (!part.isEmpty()) {
|
|
||||||
camelCase.append(Character.toUpperCase(part.charAt(0)));
|
|
||||||
if (part.length() > 1) {
|
|
||||||
camelCase.append(part.substring(1).toLowerCase());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return camelCase.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "ScenicConfigManager{" +
|
|
||||||
"configCount=" + configMap.size() +
|
|
||||||
", keys=" + configMap.keySet() +
|
|
||||||
'}';
|
|
||||||
}
|
|
||||||
}
|
}
|
@@ -30,18 +30,6 @@ public interface DeviceConfigV2Client {
|
|||||||
CommonResponse<DeviceConfigV2DTO> getDeviceConfigByKey(@PathVariable("deviceId") Long deviceId,
|
CommonResponse<DeviceConfigV2DTO> getDeviceConfigByKey(@PathVariable("deviceId") Long deviceId,
|
||||||
@PathVariable("configKey") String configKey);
|
@PathVariable("configKey") String configKey);
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取设备扁平化配置
|
|
||||||
*/
|
|
||||||
@GetMapping("/{deviceId}/flat")
|
|
||||||
CommonResponse<Map<String, Object>> getDeviceFlatConfig(@PathVariable("deviceId") Long deviceId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据设备编号获取设备扁平化配置
|
|
||||||
*/
|
|
||||||
@GetMapping("/no/{no}/flat")
|
|
||||||
CommonResponse<Map<String, Object>> getDeviceFlatConfigByNo(@PathVariable("no") String no);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建设备配置
|
* 创建设备配置
|
||||||
*/
|
*/
|
||||||
|
@@ -0,0 +1,51 @@
|
|||||||
|
package com.ycwl.basic.integration.device.client;
|
||||||
|
|
||||||
|
import com.ycwl.basic.integration.common.response.CommonResponse;
|
||||||
|
import com.ycwl.basic.integration.device.dto.status.DeviceStatusDTO;
|
||||||
|
import com.ycwl.basic.integration.device.dto.status.OnlineStatusResponseDTO;
|
||||||
|
import org.springframework.cloud.openfeign.FeignClient;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@FeignClient(name = "zt-device", contextId = "deviceStatusClient", path = "/api/device/status")
|
||||||
|
public interface DeviceStatusClient {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备状态
|
||||||
|
*/
|
||||||
|
@GetMapping("/{deviceNo}")
|
||||||
|
CommonResponse<DeviceStatusDTO> getDeviceStatus(@PathVariable("deviceNo") String deviceNo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查设备是否在线
|
||||||
|
*/
|
||||||
|
@GetMapping("/{deviceNo}/online")
|
||||||
|
CommonResponse<OnlineStatusResponseDTO> isDeviceOnline(@PathVariable("deviceNo") String deviceNo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有在线设备
|
||||||
|
*/
|
||||||
|
@GetMapping("/online")
|
||||||
|
CommonResponse<List<DeviceStatusDTO>> getAllOnlineDevices();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置设备离线
|
||||||
|
*/
|
||||||
|
@PostMapping("/{deviceNo}/offline")
|
||||||
|
CommonResponse<String> setDeviceOffline(@PathVariable("deviceNo") String deviceNo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置设备在线
|
||||||
|
*/
|
||||||
|
@PostMapping("/{deviceNo}/online")
|
||||||
|
CommonResponse<String> setDeviceOnline(@PathVariable("deviceNo") String deviceNo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理过期设备状态
|
||||||
|
*/
|
||||||
|
@PostMapping("/clean")
|
||||||
|
CommonResponse<String> cleanExpiredDevices();
|
||||||
|
}
|
@@ -21,18 +21,6 @@ public interface DeviceV2Client {
|
|||||||
@GetMapping("/no/{no}")
|
@GetMapping("/no/{no}")
|
||||||
CommonResponse<DeviceV2DTO> getDeviceByNo(@PathVariable("no") String no);
|
CommonResponse<DeviceV2DTO> getDeviceByNo(@PathVariable("no") String no);
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取设备详细信息(含配置)
|
|
||||||
*/
|
|
||||||
@GetMapping("/{id}/with-config")
|
|
||||||
CommonResponse<DeviceV2WithConfigDTO> getDeviceWithConfig(@PathVariable("id") Long id);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据设备编号获取设备详细信息(含配置)
|
|
||||||
*/
|
|
||||||
@GetMapping("/no/{no}/with-config")
|
|
||||||
CommonResponse<DeviceV2WithConfigDTO> getDeviceByNoWithConfig(@PathVariable("no") String no);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建设备
|
* 创建设备
|
||||||
*/
|
*/
|
||||||
@@ -65,19 +53,6 @@ public interface DeviceV2Client {
|
|||||||
@RequestParam(value = "isActive", required = false) Integer isActive,
|
@RequestParam(value = "isActive", required = false) Integer isActive,
|
||||||
@RequestParam(value = "scenicId", required = false) Long scenicId);
|
@RequestParam(value = "scenicId", required = false) Long scenicId);
|
||||||
|
|
||||||
/**
|
|
||||||
* 分页获取设备列表(含配置)
|
|
||||||
*/
|
|
||||||
@GetMapping("/with-config")
|
|
||||||
CommonResponse<PageResponse<DeviceV2WithConfigDTO>> listDevicesWithConfig(
|
|
||||||
@RequestParam(value = "page", defaultValue = "1") Integer page,
|
|
||||||
@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize,
|
|
||||||
@RequestParam(value = "name", required = false) String name,
|
|
||||||
@RequestParam(value = "no", required = false) String no,
|
|
||||||
@RequestParam(value = "type", required = false) String type,
|
|
||||||
@RequestParam(value = "isActive", required = false) Integer isActive,
|
|
||||||
@RequestParam(value = "scenicId", required = false) Long scenicId);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据配置条件筛选设备
|
* 根据配置条件筛选设备
|
||||||
*/
|
*/
|
||||||
|
@@ -0,0 +1,41 @@
|
|||||||
|
package com.ycwl.basic.integration.device.dto.status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备状态动作枚举
|
||||||
|
*/
|
||||||
|
public enum DeviceStatusActionEnum {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备注册
|
||||||
|
*/
|
||||||
|
REGISTER("register"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备保活
|
||||||
|
*/
|
||||||
|
KEEPALIVE("keepalive"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备注销
|
||||||
|
*/
|
||||||
|
UNREGISTER("unregister");
|
||||||
|
|
||||||
|
private final String action;
|
||||||
|
|
||||||
|
DeviceStatusActionEnum(String action) {
|
||||||
|
this.action = action;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAction() {
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DeviceStatusActionEnum fromString(String action) {
|
||||||
|
for (DeviceStatusActionEnum statusAction : values()) {
|
||||||
|
if (statusAction.action.equalsIgnoreCase(action)) {
|
||||||
|
return statusAction;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Unknown device status action: " + action);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,50 @@
|
|||||||
|
package com.ycwl.basic.integration.device.dto.status;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备状态信息
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class DeviceStatusDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备编号
|
||||||
|
*/
|
||||||
|
private String deviceNo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否在线
|
||||||
|
*/
|
||||||
|
private Boolean isOnline;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最后活动时间
|
||||||
|
*/
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date lastActiveTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最后动作(register/keepalive/unregister)
|
||||||
|
*/
|
||||||
|
private String lastAction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客户端IP
|
||||||
|
*/
|
||||||
|
private String clientIP;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态更新时间
|
||||||
|
*/
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date updateTime;
|
||||||
|
}
|
@@ -0,0 +1,24 @@
|
|||||||
|
package com.ycwl.basic.integration.device.dto.status;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线状态响应
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class OnlineStatusResponseDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备编号
|
||||||
|
*/
|
||||||
|
private String deviceNo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否在线
|
||||||
|
*/
|
||||||
|
private Boolean isOnline;
|
||||||
|
}
|
@@ -1,275 +0,0 @@
|
|||||||
package com.ycwl.basic.integration.device.example;
|
|
||||||
|
|
||||||
import com.ycwl.basic.integration.common.response.PageResponse;
|
|
||||||
import com.ycwl.basic.integration.device.dto.defaults.*;
|
|
||||||
import com.ycwl.basic.integration.device.service.DeviceDefaultConfigIntegrationService;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.boot.CommandLineRunner;
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 默认配置集成服务使用示例
|
|
||||||
*
|
|
||||||
* 通过在 application.yml 中设置 integration.device.example.default-config.enabled=true 来启用
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@ConditionalOnProperty(name = "integration.device.example.default-config.enabled", havingValue = "true")
|
|
||||||
public class DefaultConfigIntegrationExample implements CommandLineRunner {
|
|
||||||
|
|
||||||
private final DeviceDefaultConfigIntegrationService defaultConfigService;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run(String... args) throws Exception {
|
|
||||||
log.info("=== 默认配置集成服务使用示例 ===");
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. 基础查询操作示例(支持自动 Fallback)
|
|
||||||
basicQueryExamples();
|
|
||||||
|
|
||||||
// 2. 配置管理操作示例(直接操作)
|
|
||||||
configManagementExamples();
|
|
||||||
|
|
||||||
// 3. 批量操作示例
|
|
||||||
batchOperationExamples();
|
|
||||||
|
|
||||||
// 4. 高级使用模式示例
|
|
||||||
advancedUsageExamples();
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("默认配置集成示例执行失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 基础查询操作示例(支持自动 Fallback)
|
|
||||||
*/
|
|
||||||
private void basicQueryExamples() {
|
|
||||||
log.info("--- 基础查询操作示例(支持自动 Fallback)---");
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 获取默认配置列表(自动缓存,服务不可用时返回缓存数据)
|
|
||||||
PageResponse<DefaultConfigResponse> configList = defaultConfigService.listDefaultConfigs(1, 10);
|
|
||||||
log.info("默认配置列表: 总数={}, 当前页配置数={}",
|
|
||||||
configList.getTotal(), configList.getList().size());
|
|
||||||
|
|
||||||
// 显示配置详情
|
|
||||||
for (DefaultConfigResponse config : configList.getList()) {
|
|
||||||
log.info("配置详情: key={}, value={}, type={}, description={}",
|
|
||||||
config.getConfigKey(), config.getConfigValue(),
|
|
||||||
config.getConfigType(), config.getDescription());
|
|
||||||
|
|
||||||
// 获取单个配置详情(自动缓存,服务不可用时返回缓存数据)
|
|
||||||
DefaultConfigResponse detailConfig = defaultConfigService.getDefaultConfig(config.getConfigKey());
|
|
||||||
if (detailConfig != null) {
|
|
||||||
log.info("配置详情获取成功: usageCount={}, isActive={}",
|
|
||||||
detailConfig.getUsageCount(), detailConfig.getIsActive());
|
|
||||||
}
|
|
||||||
break; // 只展示第一个
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("基础查询操作失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 配置管理操作示例(直接操作)
|
|
||||||
*/
|
|
||||||
private void configManagementExamples() {
|
|
||||||
log.info("--- 配置管理操作示例(直接操作)---");
|
|
||||||
|
|
||||||
String testConfigKey = "example_test_config";
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. 创建默认配置(直接操作,失败时立即报错)
|
|
||||||
DefaultConfigRequest createRequest = new DefaultConfigRequest();
|
|
||||||
createRequest.setConfigKey(testConfigKey);
|
|
||||||
createRequest.setConfigValue("1920x1080");
|
|
||||||
createRequest.setConfigType("string");
|
|
||||||
createRequest.setDescription("示例测试配置 - 默认分辨率");
|
|
||||||
|
|
||||||
boolean createResult = defaultConfigService.createDefaultConfig(createRequest);
|
|
||||||
log.info("创建默认配置结果: {}", createResult ? "成功" : "失败");
|
|
||||||
|
|
||||||
// 2. 更新默认配置(直接操作,可能返回冲突信息)
|
|
||||||
Map<String, Object> updates = new HashMap<>();
|
|
||||||
updates.put("configValue", "3840x2160");
|
|
||||||
updates.put("description", "更新后的默认分辨率 - 4K");
|
|
||||||
|
|
||||||
DefaultConfigConflict conflict = defaultConfigService.updateDefaultConfig(testConfigKey, updates);
|
|
||||||
if (conflict != null) {
|
|
||||||
log.warn("更新配置存在冲突: configKey={}, conflictType={}, deviceCount={}",
|
|
||||||
conflict.getConfigKey(), conflict.getConflictType(), conflict.getDeviceCount());
|
|
||||||
} else {
|
|
||||||
log.info("配置更新成功,无冲突");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 验证更新结果
|
|
||||||
DefaultConfigResponse updatedConfig = defaultConfigService.getDefaultConfig(testConfigKey);
|
|
||||||
if (updatedConfig != null) {
|
|
||||||
log.info("更新后配置值: {}", updatedConfig.getConfigValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 删除测试配置(直接操作)
|
|
||||||
boolean deleteResult = defaultConfigService.deleteDefaultConfig(testConfigKey);
|
|
||||||
log.info("删除默认配置结果: {}", deleteResult ? "成功" : "失败");
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("配置管理操作失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量操作示例
|
|
||||||
*/
|
|
||||||
private void batchOperationExamples() {
|
|
||||||
log.info("--- 批量操作示例 ---");
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. 使用构建器模式创建批量配置
|
|
||||||
BatchDefaultConfigRequest batchRequest = defaultConfigService.createBatchConfigBuilder()
|
|
||||||
.addVideoConfig("1920x1080", 30, "H264") // 添加视频配置组
|
|
||||||
.addNetworkConfig("192.168.1.100", 554, "RTSP") // 添加网络配置组
|
|
||||||
.addConfig("recording_enabled", "true", "bool", "是否启用录制")
|
|
||||||
.addConfig("storage_path", "/data/recordings", "string", "录制存储路径")
|
|
||||||
.addConfig("max_file_size", "1024", "int", "最大文件大小(MB)")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
log.info("准备批量创建 {} 个默认配置", batchRequest.getConfigs().size());
|
|
||||||
|
|
||||||
// 2. 执行批量更新(直接操作)
|
|
||||||
BatchDefaultConfigResponse batchResult = defaultConfigService.batchUpdateDefaultConfigs(batchRequest);
|
|
||||||
|
|
||||||
// 3. 处理批量结果
|
|
||||||
log.info("批量操作结果: 成功={}, 失败={}", batchResult.getSuccess(), batchResult.getFailed());
|
|
||||||
|
|
||||||
if (batchResult.getConflicts() != null && !batchResult.getConflicts().isEmpty()) {
|
|
||||||
log.warn("发现 {} 个配置冲突:", batchResult.getConflicts().size());
|
|
||||||
for (DefaultConfigConflict conflict : batchResult.getConflicts()) {
|
|
||||||
log.warn("冲突配置: key={}, type={}, deviceCount={}",
|
|
||||||
conflict.getConfigKey(), conflict.getConflictType(), conflict.getDeviceCount());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (batchResult.getProcessedItems() != null) {
|
|
||||||
log.info("处理详情:");
|
|
||||||
batchResult.getProcessedItems().forEach(item ->
|
|
||||||
log.info(" 配置 {}: status={}, action={}, finalType={}",
|
|
||||||
item.getConfigKey(), item.getStatus(), item.getAction(), item.getFinalType())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (batchResult.getErrors() != null && !batchResult.getErrors().isEmpty()) {
|
|
||||||
log.error("批量操作错误:");
|
|
||||||
batchResult.getErrors().forEach(error -> log.error(" {}", error));
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("批量操作失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 高级使用模式示例
|
|
||||||
*/
|
|
||||||
private void advancedUsageExamples() {
|
|
||||||
log.info("--- 高级使用模式示例 ---");
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. 设备类型特定的默认配置模式
|
|
||||||
createDeviceTypeSpecificConfigs();
|
|
||||||
|
|
||||||
// 2. 配置验证和完整性检查模式
|
|
||||||
validateConfigCompleteness();
|
|
||||||
|
|
||||||
// 3. 配置迁移和批量更新模式
|
|
||||||
configMigrationPattern();
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("高级使用模式示例失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建设备类型特定的默认配置
|
|
||||||
*/
|
|
||||||
private void createDeviceTypeSpecificConfigs() {
|
|
||||||
log.info("创建设备类型特定的默认配置...");
|
|
||||||
|
|
||||||
// IPC摄像头默认配置
|
|
||||||
BatchDefaultConfigRequest ipcDefaults = defaultConfigService.createBatchConfigBuilder()
|
|
||||||
.addVideoConfig("1920x1080", 25, "H264")
|
|
||||||
.addConfig("night_vision", "true", "bool", "夜视功能")
|
|
||||||
.addConfig("motion_detection", "true", "bool", "移动检测")
|
|
||||||
.addConfig("stream_profile", "main", "string", "码流类型")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// NVR设备默认配置
|
|
||||||
BatchDefaultConfigRequest nvrDefaults = defaultConfigService.createBatchConfigBuilder()
|
|
||||||
.addConfig("max_channels", "16", "int", "最大通道数")
|
|
||||||
.addConfig("storage_mode", "continuous", "string", "存储模式")
|
|
||||||
.addConfig("backup_enabled", "true", "bool", "备份启用")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
log.info("IPC默认配置项数: {}, NVR默认配置项数: {}",
|
|
||||||
ipcDefaults.getConfigs().size(), nvrDefaults.getConfigs().size());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 配置验证和完整性检查
|
|
||||||
*/
|
|
||||||
private void validateConfigCompleteness() {
|
|
||||||
log.info("验证配置完整性...");
|
|
||||||
|
|
||||||
// 获取所有默认配置
|
|
||||||
PageResponse<DefaultConfigResponse> allConfigs = defaultConfigService.listDefaultConfigs(1, 100);
|
|
||||||
|
|
||||||
// 检查必需的基础配置是否存在
|
|
||||||
String[] requiredConfigs = {"resolution", "frameRate", "codec", "protocol"};
|
|
||||||
for (String requiredConfig : requiredConfigs) {
|
|
||||||
boolean exists = allConfigs.getList().stream()
|
|
||||||
.anyMatch(config -> requiredConfig.equals(config.getConfigKey()));
|
|
||||||
log.info("必需配置 {} 存在: {}", requiredConfig, exists ? "✓" : "✗");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 统计配置类型分布
|
|
||||||
Map<String, Long> typeDistribution = new HashMap<>();
|
|
||||||
allConfigs.getList().forEach(config ->
|
|
||||||
typeDistribution.merge(config.getConfigType(), 1L, Long::sum)
|
|
||||||
);
|
|
||||||
|
|
||||||
log.info("配置类型分布: {}", typeDistribution);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 配置迁移和批量更新模式
|
|
||||||
*/
|
|
||||||
private void configMigrationPattern() {
|
|
||||||
log.info("配置迁移模式示例...");
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. 获取需要升级的配置
|
|
||||||
PageResponse<DefaultConfigResponse> oldConfigs = defaultConfigService.listDefaultConfigs(1, 50);
|
|
||||||
|
|
||||||
// 2. 创建升级配置批次
|
|
||||||
BatchDefaultConfigRequest upgradeRequest = defaultConfigService.createBatchConfigBuilder()
|
|
||||||
.addConfig("api_version", "v2", "string", "API版本")
|
|
||||||
.addConfig("security_mode", "enhanced", "string", "安全模式")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// 3. 执行批量升级
|
|
||||||
BatchDefaultConfigResponse upgradeResult = defaultConfigService.batchUpdateDefaultConfigs(upgradeRequest);
|
|
||||||
log.info("配置升级结果: 成功={}, 失败={}", upgradeResult.getSuccess(), upgradeResult.getFailed());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("配置迁移示例执行失败(这是正常的,因为是示例代码)", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,190 +0,0 @@
|
|||||||
package com.ycwl.basic.integration.device.example;
|
|
||||||
|
|
||||||
import com.ycwl.basic.integration.device.dto.device.*;
|
|
||||||
import com.ycwl.basic.integration.device.service.DeviceIntegrationService;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设备配置筛选功能使用示例
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class DeviceFilterExample {
|
|
||||||
|
|
||||||
private final DeviceIntegrationService deviceIntegrationService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 示例1: 查找高质量户外IPC设备
|
|
||||||
* 条件: 分辨率1920x1080,帧率>=30,位置包含"outdoor"
|
|
||||||
*/
|
|
||||||
public void findHighQualityOutdoorDevices() {
|
|
||||||
log.info("=== 查找高质量户外IPC设备 ===");
|
|
||||||
|
|
||||||
List<ConfigFilter> configFilters = Arrays.asList(
|
|
||||||
new ConfigFilter("resolution", "1920x1080", "eq"),
|
|
||||||
new ConfigFilter("framerate", 30, "gte"),
|
|
||||||
new ConfigFilter("location", "outdoor", "like")
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, Object> deviceFilters = new HashMap<>();
|
|
||||||
deviceFilters.put("type", "IPC");
|
|
||||||
deviceFilters.put("isActive", 1);
|
|
||||||
|
|
||||||
FilterDevicesByConfigsRequest request = new FilterDevicesByConfigsRequest();
|
|
||||||
request.setPage(1);
|
|
||||||
request.setPageSize(20);
|
|
||||||
request.setConfigFilters(configFilters);
|
|
||||||
request.setFilterLogic("AND");
|
|
||||||
request.setDeviceFilters(deviceFilters);
|
|
||||||
|
|
||||||
try {
|
|
||||||
FilterDevicesByConfigsResponse response = deviceIntegrationService.filterDevicesByConfigs(request);
|
|
||||||
log.info("找到 {} 个高质量户外IPC设备", response.getTotal());
|
|
||||||
|
|
||||||
response.getList().forEach(device -> {
|
|
||||||
log.info("设备: {} ({}), 配置: {}", device.getName(), device.getNo(), device.getConfig());
|
|
||||||
});
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("查找高质量户外IPC设备失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 示例2: 查找缺少关键配置的设备
|
|
||||||
* 条件: 缺少备份服务器或夜视功能配置
|
|
||||||
*/
|
|
||||||
public void findDevicesWithMissingConfigs() {
|
|
||||||
log.info("=== 查找缺少关键配置的设备 ===");
|
|
||||||
|
|
||||||
List<ConfigFilter> configFilters = Arrays.asList(
|
|
||||||
new ConfigFilter("backup_server", null, "is_null"),
|
|
||||||
new ConfigFilter("night_vision", null, "is_null")
|
|
||||||
);
|
|
||||||
|
|
||||||
FilterDevicesByConfigsRequest request = new FilterDevicesByConfigsRequest();
|
|
||||||
request.setConfigFilters(configFilters);
|
|
||||||
request.setFilterLogic("OR"); // 缺少任一配置即返回
|
|
||||||
request.setPageSize(50);
|
|
||||||
|
|
||||||
try {
|
|
||||||
FilterDevicesByConfigsResponse response = deviceIntegrationService.filterDevicesByConfigs(request);
|
|
||||||
log.info("找到 {} 个缺少关键配置的设备", response.getTotal());
|
|
||||||
|
|
||||||
response.getList().forEach(device -> {
|
|
||||||
Map<String, Object> config = device.getConfig();
|
|
||||||
boolean missingBackup = !config.containsKey("backup_server") || config.get("backup_server") == null;
|
|
||||||
boolean missingNightVision = !config.containsKey("night_vision") || config.get("night_vision") == null;
|
|
||||||
|
|
||||||
log.info("设备: {} ({}), 缺少配置: {}{}",
|
|
||||||
device.getName(), device.getNo(),
|
|
||||||
missingBackup ? "备份服务器 " : "",
|
|
||||||
missingNightVision ? "夜视功能 " : "");
|
|
||||||
});
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("查找缺少关键配置的设备失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 示例3: 根据多个位置查找设备
|
|
||||||
* 条件: 位置在指定列表中
|
|
||||||
*/
|
|
||||||
public void findDevicesByMultipleLocations() {
|
|
||||||
log.info("=== 根据多个位置查找设备 ===");
|
|
||||||
|
|
||||||
List<String> locations = Arrays.asList("outdoor", "indoor", "parking");
|
|
||||||
List<ConfigFilter> configFilters = Arrays.asList(
|
|
||||||
new ConfigFilter("location", locations, "in")
|
|
||||||
);
|
|
||||||
|
|
||||||
FilterDevicesByConfigsRequest request = new FilterDevicesByConfigsRequest();
|
|
||||||
request.setConfigFilters(configFilters);
|
|
||||||
request.setPageSize(100);
|
|
||||||
|
|
||||||
try {
|
|
||||||
FilterDevicesByConfigsResponse response = deviceIntegrationService.filterDevicesByConfigs(request);
|
|
||||||
log.info("找到 {} 个设备在指定位置", response.getTotal());
|
|
||||||
|
|
||||||
response.getList().forEach(device -> {
|
|
||||||
Object location = device.getConfig().get("location");
|
|
||||||
log.info("设备: {} ({}), 位置: {}", device.getName(), device.getNo(), location);
|
|
||||||
});
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("根据多个位置查找设备失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 示例4: 使用便捷方法查找设备
|
|
||||||
*/
|
|
||||||
public void useConvenienceMethods() {
|
|
||||||
log.info("=== 使用便捷方法查找设备 ===");
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 查找缺少配置的设备
|
|
||||||
FilterDevicesByConfigsResponse response4 = deviceIntegrationService
|
|
||||||
.findDevicesWithMissingConfig("firmware_version", 1, 10);
|
|
||||||
log.info("缺少固件版本配置的设备数: {}", response4.getTotal());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("使用便捷方法查找设备失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 示例5: 性能监控查询
|
|
||||||
* 条件: CPU使用率>80% 或 内存使用率>85%
|
|
||||||
*/
|
|
||||||
public void findHighLoadDevices() {
|
|
||||||
log.info("=== 查找高负载设备 ===");
|
|
||||||
|
|
||||||
List<ConfigFilter> configFilters = Arrays.asList(
|
|
||||||
new ConfigFilter("cpu_usage", 80, "gt"),
|
|
||||||
new ConfigFilter("memory_usage", 85, "gt")
|
|
||||||
);
|
|
||||||
|
|
||||||
FilterDevicesByConfigsRequest request = new FilterDevicesByConfigsRequest();
|
|
||||||
request.setConfigFilters(configFilters);
|
|
||||||
request.setFilterLogic("OR");
|
|
||||||
request.setPageSize(50);
|
|
||||||
|
|
||||||
try {
|
|
||||||
FilterDevicesByConfigsResponse response = deviceIntegrationService.filterDevicesByConfigs(request);
|
|
||||||
log.info("找到 {} 个高负载设备", response.getTotal());
|
|
||||||
|
|
||||||
response.getList().forEach(device -> {
|
|
||||||
Map<String, Object> config = device.getConfig();
|
|
||||||
Object cpuUsage = config.get("cpu_usage");
|
|
||||||
Object memoryUsage = config.get("memory_usage");
|
|
||||||
|
|
||||||
log.info("高负载设备: {} ({}), CPU: {}%, 内存: {}%",
|
|
||||||
device.getName(), device.getNo(), cpuUsage, memoryUsage);
|
|
||||||
});
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("查找高负载设备失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 运行所有示例
|
|
||||||
*/
|
|
||||||
public void runAllExamples() {
|
|
||||||
log.info("开始运行设备配置筛选示例...");
|
|
||||||
|
|
||||||
findHighQualityOutdoorDevices();
|
|
||||||
findDevicesWithMissingConfigs();
|
|
||||||
findDevicesByMultipleLocations();
|
|
||||||
useConvenienceMethods();
|
|
||||||
findHighLoadDevices();
|
|
||||||
|
|
||||||
log.info("所有示例运行完成");
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,185 +0,0 @@
|
|||||||
package com.ycwl.basic.integration.device.example;
|
|
||||||
|
|
||||||
import com.ycwl.basic.integration.common.response.PageResponse;
|
|
||||||
import com.ycwl.basic.integration.device.dto.device.*;
|
|
||||||
import com.ycwl.basic.integration.device.dto.config.*;
|
|
||||||
import com.ycwl.basic.integration.device.service.DeviceConfigIntegrationService;
|
|
||||||
import com.ycwl.basic.integration.device.service.DeviceIntegrationService;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Device Integration 使用示例
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class DeviceIntegrationExample {
|
|
||||||
|
|
||||||
private final DeviceIntegrationService deviceService;
|
|
||||||
private final DeviceConfigIntegrationService deviceConfigService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 基本设备操作
|
|
||||||
*/
|
|
||||||
public void basicDeviceOperations() {
|
|
||||||
log.info("=== 基本设备操作 ===");
|
|
||||||
|
|
||||||
// 创建IPC摄像头设备(默认排序)
|
|
||||||
DeviceV2DTO ipcDevice = deviceService.createIpcDevice(
|
|
||||||
"前门摄像头", "CAM001", 1001L);
|
|
||||||
log.info("创建IPC设备: {}, 排序值: {}", ipcDevice.getName(), ipcDevice.getSort());
|
|
||||||
|
|
||||||
// 根据ID获取设备信息
|
|
||||||
DeviceV2DTO device = deviceService.getDevice(ipcDevice.getId());
|
|
||||||
log.info("获取设备信息: {}, 排序值: {}", device.getName(), device.getSort());
|
|
||||||
|
|
||||||
// 根据设备编号获取设备信息
|
|
||||||
DeviceV2DTO deviceByNo = deviceService.getDeviceByNo("CAM001");
|
|
||||||
log.info("根据编号获取设备: {}", deviceByNo.getName());
|
|
||||||
|
|
||||||
// 获取设备详细信息(含配置)
|
|
||||||
DeviceV2WithConfigDTO deviceWithConfig = deviceService.getDeviceWithConfig(ipcDevice.getId());
|
|
||||||
log.info("获取设备配置: {}", deviceWithConfig.getName());
|
|
||||||
|
|
||||||
// 分页查询景区设备列表
|
|
||||||
PageResponse<DeviceV2DTO> deviceList = deviceService.getScenicIpcDevices(1001L, 1, 10);
|
|
||||||
log.info("景区设备列表: 总数={}", deviceList.getTotal());
|
|
||||||
|
|
||||||
// 启用设备
|
|
||||||
deviceService.enableDevice(ipcDevice.getId());
|
|
||||||
log.info("设备已启用");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设备排序功能演示
|
|
||||||
*/
|
|
||||||
public void deviceSortingOperations() {
|
|
||||||
log.info("=== 设备排序功能演示 ===");
|
|
||||||
|
|
||||||
Long scenicId = 1001L;
|
|
||||||
|
|
||||||
// 创建带排序的设备
|
|
||||||
DeviceV2DTO camera1 = deviceService.createIpcDeviceWithSort(
|
|
||||||
"大门摄像头", "CAM_GATE", scenicId, 10);
|
|
||||||
log.info("创建摄像头1: {}, 排序: {}", camera1.getName(), camera1.getSort());
|
|
||||||
|
|
||||||
DeviceV2DTO camera2 = deviceService.createIpcDeviceWithSort(
|
|
||||||
"后门摄像头", "CAM_BACK", scenicId, 20);
|
|
||||||
log.info("创建摄像头2: {}, 排序: {}", camera2.getName(), camera2.getSort());
|
|
||||||
|
|
||||||
DeviceV2DTO sensor1 = deviceService.createCustomDeviceWithSort(
|
|
||||||
"温度传感器", "TEMP_01", scenicId, 5);
|
|
||||||
log.info("创建传感器: {}, 排序: {}", sensor1.getName(), sensor1.getSort());
|
|
||||||
|
|
||||||
// 更新设备排序
|
|
||||||
deviceService.updateDeviceSort(camera1.getId(), 1);
|
|
||||||
log.info("更新摄像头1排序为1(置顶)");
|
|
||||||
|
|
||||||
// 获取排序后的设备列表
|
|
||||||
PageResponse<DeviceV2DTO> sortedList = deviceService.listDevices(1, 10, null, null, null, 1, scenicId);
|
|
||||||
log.info("排序后的设备列表:");
|
|
||||||
for (DeviceV2DTO device : sortedList.getList()) {
|
|
||||||
log.info(" - {}: 排序={}, 类型={}", device.getName(), device.getSort(), device.getType());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量调整排序演示
|
|
||||||
log.info("--- 批量调整排序演示 ---");
|
|
||||||
deviceService.updateDeviceSort(sensor1.getId(), 15); // 传感器排到中间
|
|
||||||
deviceService.updateDeviceSort(camera2.getId(), 30); // 后门摄像头排到最后
|
|
||||||
log.info("批量排序调整完成");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设备配置管理
|
|
||||||
*/
|
|
||||||
public void deviceConfigurationOperations() {
|
|
||||||
log.info("=== 设备配置管理 ===");
|
|
||||||
|
|
||||||
Long deviceId = 1L;
|
|
||||||
|
|
||||||
// 获取设备所有配置
|
|
||||||
List<DeviceConfigV2DTO> configs = deviceConfigService.getDeviceConfigs(deviceId);
|
|
||||||
log.info("设备配置数量: {}", configs.size());
|
|
||||||
|
|
||||||
// 获取扁平化配置
|
|
||||||
Map<String, Object> flatConfig = deviceConfigService.getDeviceFlatConfig(deviceId);
|
|
||||||
log.info("扁平化配置项数: {}", flatConfig.size());
|
|
||||||
|
|
||||||
// 使用批量配置API
|
|
||||||
BatchDeviceConfigRequest builderRequest = deviceConfigService.createBatchConfigBuilder()
|
|
||||||
.build();
|
|
||||||
|
|
||||||
BatchUpdateResponse result = deviceConfigService.batchUpdateDeviceConfig(deviceId, builderRequest);
|
|
||||||
log.info("批量配置更新结果: 成功={}, 失败={}", result.getSuccess(), result.getFailed());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 排序最佳实践演示
|
|
||||||
*/
|
|
||||||
public void sortingBestPractices() {
|
|
||||||
log.info("=== 排序最佳实践演示 ===");
|
|
||||||
|
|
||||||
Long scenicId = 1001L;
|
|
||||||
|
|
||||||
// 推荐使用10的倍数作为排序值
|
|
||||||
DeviceV2DTO device1 = deviceService.createIpcDeviceWithSort(
|
|
||||||
"重要摄像头", "CAM_IMPORTANT", scenicId, 10);
|
|
||||||
|
|
||||||
DeviceV2DTO device2 = deviceService.createIpcDeviceWithSort(
|
|
||||||
"普通摄像头", "CAM_NORMAL", scenicId, 20);
|
|
||||||
|
|
||||||
DeviceV2DTO device3 = deviceService.createIpcDeviceWithSort(
|
|
||||||
"备用摄像头", "CAM_BACKUP", scenicId, 30);
|
|
||||||
|
|
||||||
log.info("使用10的倍数创建设备排序: 10, 20, 30");
|
|
||||||
|
|
||||||
// 在中间插入新设备
|
|
||||||
DeviceV2DTO insertDevice = deviceService.createIpcDeviceWithSort(
|
|
||||||
"中间摄像头", "CAM_MIDDLE", scenicId, 25);
|
|
||||||
log.info("在20和30之间插入设备,排序值: 25");
|
|
||||||
|
|
||||||
// 置顶操作
|
|
||||||
deviceService.updateDeviceSort(device2.getId(), 1);
|
|
||||||
log.info("将普通摄像头置顶(排序值: 1)");
|
|
||||||
|
|
||||||
// 查看最终排序结果
|
|
||||||
PageResponse<DeviceV2DTO> finalList = deviceService.listDevices(1, 10, null, null, null, 1, scenicId);
|
|
||||||
log.info("最终排序结果:");
|
|
||||||
for (DeviceV2DTO device : finalList.getList()) {
|
|
||||||
log.info(" - {}: 排序={}", device.getName(), device.getSort());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 运行所有示例
|
|
||||||
*/
|
|
||||||
public void runAllExamples() {
|
|
||||||
try {
|
|
||||||
basicDeviceOperations();
|
|
||||||
deviceSortingOperations();
|
|
||||||
sortingBestPractices();
|
|
||||||
deviceConfigurationOperations();
|
|
||||||
log.info("=== 所有示例执行完成 ===");
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("示例执行过程中发生错误", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 运行基础示例(简化版)
|
|
||||||
*/
|
|
||||||
public void runBasicExamples() {
|
|
||||||
try {
|
|
||||||
basicDeviceOperations();
|
|
||||||
deviceConfigurationOperations();
|
|
||||||
log.info("=== 基础示例执行完成 ===");
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("示例执行过程中发生错误", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,124 +0,0 @@
|
|||||||
package com.ycwl.basic.integration.device.example;
|
|
||||||
|
|
||||||
import com.ycwl.basic.integration.common.service.IntegrationFallbackService;
|
|
||||||
import com.ycwl.basic.integration.device.service.DeviceIntegrationService;
|
|
||||||
import com.ycwl.basic.integration.device.service.DeviceConfigIntegrationService;
|
|
||||||
import com.ycwl.basic.integration.device.dto.device.*;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设备集成示例(包含降级机制)
|
|
||||||
* 演示设备集成和失败降级策略的使用
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class DeviceIntegrationFallbackExample {
|
|
||||||
|
|
||||||
private final DeviceIntegrationService deviceService;
|
|
||||||
private final DeviceConfigIntegrationService configService;
|
|
||||||
private final IntegrationFallbackService fallbackService;
|
|
||||||
|
|
||||||
private static final String SERVICE_NAME = "zt-device";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 演示设备信息获取的降级机制
|
|
||||||
*/
|
|
||||||
public void deviceInfoFallbackExample() {
|
|
||||||
log.info("=== 设备信息获取降级示例 ===");
|
|
||||||
|
|
||||||
Long deviceId = 1001L;
|
|
||||||
String deviceNo = "CAM001";
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 获取设备信息 - 自动降级
|
|
||||||
DeviceV2DTO device = deviceService.getDevice(deviceId);
|
|
||||||
log.info("获取设备成功: {}", device.getName());
|
|
||||||
|
|
||||||
// 根据设备号获取设备 - 自动降级
|
|
||||||
DeviceV2DTO deviceByNo = deviceService.getDeviceByNo(deviceNo);
|
|
||||||
log.info("根据设备号获取设备成功: {}", deviceByNo.getName());
|
|
||||||
|
|
||||||
// 获取设备配置 - 自动降级
|
|
||||||
DeviceV2WithConfigDTO deviceWithConfig = deviceService.getDeviceWithConfig(deviceId);
|
|
||||||
log.info("获取设备配置成功,配置数量: {}", deviceWithConfig.getConfig().size());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("所有降级策略失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 演示设备操作(无降级机制)
|
|
||||||
*/
|
|
||||||
public void deviceOperationExample() {
|
|
||||||
log.info("=== 设备操作示例 ===");
|
|
||||||
|
|
||||||
Long deviceId = 1001L;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 设备更新操作 - 直接操作,失败时抛出异常
|
|
||||||
UpdateDeviceRequest updateRequest = new UpdateDeviceRequest();
|
|
||||||
updateRequest.setName("更新后的摄像头");
|
|
||||||
deviceService.updateDevice(deviceId, updateRequest);
|
|
||||||
log.info("设备更新操作完成");
|
|
||||||
|
|
||||||
// 设备排序更新 - 直接操作,失败时抛出异常
|
|
||||||
deviceService.updateDeviceSort(deviceId, 5);
|
|
||||||
log.info("设备排序更新完成");
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("设备操作失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 演示降级缓存管理
|
|
||||||
*/
|
|
||||||
public void fallbackCacheManagementExample() {
|
|
||||||
log.info("=== 降级缓存管理示例 ===");
|
|
||||||
|
|
||||||
String deviceCacheKey = "device:1001";
|
|
||||||
String configCacheKey = "device:flat:config:1001";
|
|
||||||
|
|
||||||
// 检查降级缓存状态
|
|
||||||
boolean hasDeviceCache = fallbackService.hasFallbackCache(SERVICE_NAME, deviceCacheKey);
|
|
||||||
boolean hasConfigCache = fallbackService.hasFallbackCache(SERVICE_NAME, configCacheKey);
|
|
||||||
|
|
||||||
log.info("设备降级缓存存在: {}", hasDeviceCache);
|
|
||||||
log.info("配置降级缓存存在: {}", hasConfigCache);
|
|
||||||
|
|
||||||
// 清理特定的降级缓存
|
|
||||||
if (hasDeviceCache) {
|
|
||||||
fallbackService.clearFallbackCache(SERVICE_NAME, deviceCacheKey);
|
|
||||||
log.info("已清理设备降级缓存");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取降级缓存统计信息
|
|
||||||
IntegrationFallbackService.FallbackCacheStats stats = fallbackService.getFallbackCacheStats(SERVICE_NAME);
|
|
||||||
log.info("设备服务降级缓存统计: {}", stats);
|
|
||||||
|
|
||||||
// 批量清理所有设备降级缓存
|
|
||||||
if (stats.getTotalCacheCount() > 10) {
|
|
||||||
fallbackService.clearAllFallbackCache(SERVICE_NAME);
|
|
||||||
log.info("已批量清理所有设备降级缓存");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 运行所有示例
|
|
||||||
*/
|
|
||||||
public void runAllExamples() {
|
|
||||||
log.info("开始运行设备集成示例...");
|
|
||||||
|
|
||||||
deviceInfoFallbackExample();
|
|
||||||
deviceOperationExample();
|
|
||||||
fallbackCacheManagementExample();
|
|
||||||
|
|
||||||
log.info("设备集成示例运行完成");
|
|
||||||
}
|
|
||||||
}
|
|
@@ -39,31 +39,6 @@ public class DeviceConfigIntegrationService {
|
|||||||
return handleResponse(response, "根据键获取设备配置失败");
|
return handleResponse(response, "根据键获取设备配置失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, Object> getDeviceFlatConfig(Long deviceId) {
|
|
||||||
return fallbackService.executeWithFallback(
|
|
||||||
SERVICE_NAME,
|
|
||||||
"device:flat:config:" + deviceId,
|
|
||||||
() -> {
|
|
||||||
CommonResponse<Map<String, Object>> response = deviceConfigV2Client.getDeviceFlatConfig(deviceId);
|
|
||||||
return handleResponse(response, "获取设备扁平化配置失败");
|
|
||||||
},
|
|
||||||
Map.class
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<String, Object> getDeviceFlatConfigByNo(String deviceNo) {
|
|
||||||
log.debug("根据设备编号获取扁平化配置, deviceNo: {}", deviceNo);
|
|
||||||
return fallbackService.executeWithFallback(
|
|
||||||
SERVICE_NAME,
|
|
||||||
"device:flat:config:no:" + deviceNo,
|
|
||||||
() -> {
|
|
||||||
CommonResponse<Map<String, Object>> response = deviceConfigV2Client.getDeviceFlatConfigByNo(deviceNo);
|
|
||||||
return handleResponse(response, "根据设备编号获取扁平化配置失败");
|
|
||||||
},
|
|
||||||
Map.class
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public DeviceConfigV2DTO createDeviceConfig(Long deviceId, CreateDeviceConfigRequest request) {
|
public DeviceConfigV2DTO createDeviceConfig(Long deviceId, CreateDeviceConfigRequest request) {
|
||||||
log.debug("创建设备配置, deviceId: {}, configKey: {}", deviceId, request.getConfigKey());
|
log.debug("创建设备配置, deviceId: {}, configKey: {}", deviceId, request.getConfigKey());
|
||||||
CommonResponse<DeviceConfigV2DTO> response = deviceConfigV2Client.createDeviceConfig(deviceId, request);
|
CommonResponse<DeviceConfigV2DTO> response = deviceConfigV2Client.createDeviceConfig(deviceId, request);
|
||||||
|
@@ -48,32 +48,6 @@ public class DeviceIntegrationService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DeviceV2WithConfigDTO getDeviceWithConfig(Long deviceId) {
|
|
||||||
log.debug("获取设备配置信息, deviceId: {}", deviceId);
|
|
||||||
return fallbackService.executeWithFallback(
|
|
||||||
SERVICE_NAME,
|
|
||||||
"device:config:" + deviceId,
|
|
||||||
() -> {
|
|
||||||
CommonResponse<DeviceV2WithConfigDTO> response = deviceV2Client.getDeviceWithConfig(deviceId);
|
|
||||||
return handleResponse(response, "获取设备配置信息失败");
|
|
||||||
},
|
|
||||||
DeviceV2WithConfigDTO.class
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public DeviceV2WithConfigDTO getDeviceWithConfigByNo(String deviceNo) {
|
|
||||||
log.debug("根据设备编号获取设备配置信息, deviceNo: {}", deviceNo);
|
|
||||||
return fallbackService.executeWithFallback(
|
|
||||||
SERVICE_NAME,
|
|
||||||
"device:config:no:" + deviceNo,
|
|
||||||
() -> {
|
|
||||||
CommonResponse<DeviceV2WithConfigDTO> response = deviceV2Client.getDeviceByNoWithConfig(deviceNo);
|
|
||||||
return handleResponse(response, "根据设备编号获取设备配置信息失败");
|
|
||||||
},
|
|
||||||
DeviceV2WithConfigDTO.class
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public DeviceV2DTO createDevice(CreateDeviceRequest request) {
|
public DeviceV2DTO createDevice(CreateDeviceRequest request) {
|
||||||
log.debug("创建设备, name: {}, no: {}, type: {}", request.getName(), request.getNo(), request.getType());
|
log.debug("创建设备, name: {}, no: {}, type: {}", request.getName(), request.getNo(), request.getType());
|
||||||
CommonResponse<DeviceV2DTO> response = deviceV2Client.createDevice(request);
|
CommonResponse<DeviceV2DTO> response = deviceV2Client.createDevice(request);
|
||||||
@@ -101,15 +75,6 @@ public class DeviceIntegrationService {
|
|||||||
return handleResponse(response, "分页查询设备列表失败");
|
return handleResponse(response, "分页查询设备列表失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
public PageResponse<DeviceV2WithConfigDTO> listDevicesWithConfig(Integer page, Integer pageSize, String name, String no,
|
|
||||||
String type, Integer isActive, Long scenicId) {
|
|
||||||
log.debug("分页查询设备带配置列表, page: {}, pageSize: {}, name: {}, no: {}, type: {}, isActive: {}, scenicId: {}",
|
|
||||||
page, pageSize, name, no, type, isActive, scenicId);
|
|
||||||
CommonResponse<PageResponse<DeviceV2WithConfigDTO>> response = deviceV2Client.listDevicesWithConfig(
|
|
||||||
page, pageSize, name, no, type, isActive, scenicId);
|
|
||||||
return handleResponse(response, "分页查询设备带配置列表失败");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建IPC摄像头设备
|
* 创建IPC摄像头设备
|
||||||
*/
|
*/
|
||||||
|
@@ -0,0 +1,211 @@
|
|||||||
|
package com.ycwl.basic.integration.device.service;
|
||||||
|
|
||||||
|
import com.ycwl.basic.integration.common.exception.IntegrationException;
|
||||||
|
import com.ycwl.basic.integration.common.response.CommonResponse;
|
||||||
|
import com.ycwl.basic.integration.common.service.IntegrationFallbackService;
|
||||||
|
import com.ycwl.basic.integration.device.client.DeviceStatusClient;
|
||||||
|
import com.ycwl.basic.integration.device.dto.status.DeviceStatusDTO;
|
||||||
|
import com.ycwl.basic.integration.device.dto.status.OnlineStatusResponseDTO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DeviceStatusIntegrationService {
|
||||||
|
|
||||||
|
private final DeviceStatusClient deviceStatusClient;
|
||||||
|
private final IntegrationFallbackService fallbackService;
|
||||||
|
|
||||||
|
private static final String SERVICE_NAME = "zt-device";
|
||||||
|
|
||||||
|
public DeviceStatusDTO getDeviceStatus(String deviceNo) {
|
||||||
|
log.debug("获取设备状态, deviceNo: {}", deviceNo);
|
||||||
|
return fallbackService.executeWithFallback(
|
||||||
|
SERVICE_NAME,
|
||||||
|
"device:status:" + deviceNo,
|
||||||
|
() -> {
|
||||||
|
CommonResponse<DeviceStatusDTO> response = deviceStatusClient.getDeviceStatus(deviceNo);
|
||||||
|
return handleResponse(response, "获取设备状态失败");
|
||||||
|
},
|
||||||
|
DeviceStatusDTO.class
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OnlineStatusResponseDTO isDeviceOnline(String deviceNo) {
|
||||||
|
log.debug("检查设备是否在线, deviceNo: {}", deviceNo);
|
||||||
|
return fallbackService.executeWithFallback(
|
||||||
|
SERVICE_NAME,
|
||||||
|
"device:online:" + deviceNo,
|
||||||
|
() -> {
|
||||||
|
CommonResponse<OnlineStatusResponseDTO> response = deviceStatusClient.isDeviceOnline(deviceNo);
|
||||||
|
return handleResponse(response, "检查设备在线状态失败");
|
||||||
|
},
|
||||||
|
OnlineStatusResponseDTO.class
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DeviceStatusDTO> getAllOnlineDevices() {
|
||||||
|
log.debug("获取所有在线设备");
|
||||||
|
return fallbackService.executeWithFallback(
|
||||||
|
SERVICE_NAME,
|
||||||
|
"devices:online:all",
|
||||||
|
() -> {
|
||||||
|
CommonResponse<List<DeviceStatusDTO>> response = deviceStatusClient.getAllOnlineDevices();
|
||||||
|
return handleResponse(response, "获取所有在线设备失败");
|
||||||
|
},
|
||||||
|
List.class
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDeviceOffline(String deviceNo) {
|
||||||
|
log.debug("设置设备离线, deviceNo: {}", deviceNo);
|
||||||
|
CommonResponse<String> response = deviceStatusClient.setDeviceOffline(deviceNo);
|
||||||
|
handleResponse(response, "设置设备离线失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDeviceOnline(String deviceNo) {
|
||||||
|
log.debug("设置设备在线, deviceNo: {}", deviceNo);
|
||||||
|
CommonResponse<String> response = deviceStatusClient.setDeviceOnline(deviceNo);
|
||||||
|
handleResponse(response, "设置设备在线失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cleanExpiredDevices() {
|
||||||
|
log.debug("清理过期设备状态");
|
||||||
|
CommonResponse<String> response = deviceStatusClient.cleanExpiredDevices();
|
||||||
|
handleResponse(response, "清理过期设备状态失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全地获取设备状态
|
||||||
|
*/
|
||||||
|
public Optional<DeviceStatusDTO> getDeviceStatusSafely(String deviceNo) {
|
||||||
|
try {
|
||||||
|
DeviceStatusDTO status = getDeviceStatus(deviceNo);
|
||||||
|
return Optional.ofNullable(status);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("获取设备状态异常: deviceNo={}, error={}", deviceNo, e.getMessage());
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全地检查设备是否在线
|
||||||
|
*/
|
||||||
|
public boolean isDeviceOnlineSafely(String deviceNo) {
|
||||||
|
try {
|
||||||
|
OnlineStatusResponseDTO response = isDeviceOnline(deviceNo);
|
||||||
|
return response != null && Boolean.TRUE.equals(response.getIsOnline());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("检查设备在线状态异常: deviceNo={}, error={}", deviceNo, e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量检查设备是否在线
|
||||||
|
*/
|
||||||
|
public boolean areAllDevicesOnline(List<String> deviceNos) {
|
||||||
|
if (deviceNos == null || deviceNos.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("批量检查设备在线状态, deviceNos: {}", deviceNos);
|
||||||
|
for (String deviceNo : deviceNos) {
|
||||||
|
if (!isDeviceOnlineSafely(deviceNo)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量设置设备离线
|
||||||
|
*/
|
||||||
|
public void setDevicesOffline(List<String> deviceNos) {
|
||||||
|
if (deviceNos == null || deviceNos.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("批量设置设备离线, deviceNos: {}", deviceNos);
|
||||||
|
for (String deviceNo : deviceNos) {
|
||||||
|
try {
|
||||||
|
setDeviceOffline(deviceNo);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("设置设备离线失败: deviceNo={}, error={}", deviceNo, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量设置设备在线
|
||||||
|
*/
|
||||||
|
public void setDevicesOnline(List<String> deviceNos) {
|
||||||
|
if (deviceNos == null || deviceNos.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("批量设置设备在线, deviceNos: {}", deviceNos);
|
||||||
|
for (String deviceNo : deviceNos) {
|
||||||
|
try {
|
||||||
|
setDeviceOnline(deviceNo);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("设置设备在线失败: deviceNo={}, error={}", deviceNo, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取在线设备数量
|
||||||
|
*/
|
||||||
|
public int getOnlineDeviceCount() {
|
||||||
|
try {
|
||||||
|
List<DeviceStatusDTO> onlineDevices = getAllOnlineDevices();
|
||||||
|
return onlineDevices != null ? onlineDevices.size() : 0;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("获取在线设备数量失败: {}", e.getMessage());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定设备编号列表中的在线设备
|
||||||
|
*/
|
||||||
|
public List<String> getOnlineDeviceNos(List<String> deviceNos) {
|
||||||
|
if (deviceNos == null || deviceNos.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
return deviceNos.stream()
|
||||||
|
.filter(this::isDeviceOnlineSafely)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定设备编号列表中的离线设备
|
||||||
|
*/
|
||||||
|
public List<String> getOfflineDeviceNos(List<String> deviceNos) {
|
||||||
|
if (deviceNos == null || deviceNos.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
return deviceNos.stream()
|
||||||
|
.filter(deviceNo -> !isDeviceOnlineSafely(deviceNo))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> T handleResponse(CommonResponse<T> response, String errorMessage) {
|
||||||
|
if (response == null || !response.isSuccess()) {
|
||||||
|
String msg = response != null && response.getMessage() != null
|
||||||
|
? response.getMessage()
|
||||||
|
: errorMessage;
|
||||||
|
Integer code = response != null ? response.getCode() : 5000;
|
||||||
|
throw new IntegrationException(code, msg, SERVICE_NAME);
|
||||||
|
}
|
||||||
|
return response.getData();
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,49 @@
|
|||||||
|
package com.ycwl.basic.integration.kafka.config;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸识别异步处理线程池配置
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Configuration
|
||||||
|
@ConditionalOnProperty(name = "kafka.enabled", havingValue = "true")
|
||||||
|
public class FaceRecognitionThreadPoolConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建人脸识别专用线程池
|
||||||
|
* - 核心线程数:32
|
||||||
|
* - 最大线程数:128
|
||||||
|
* - 队列容量:1000(避免无限制增长)
|
||||||
|
* - 拒绝策略:CallerRunsPolicy(调用者线程执行)
|
||||||
|
*/
|
||||||
|
@Bean(name = "faceRecognitionExecutor", destroyMethod = "shutdown")
|
||||||
|
public ThreadPoolExecutor faceRecognitionExecutor() {
|
||||||
|
ThreadPoolExecutor executor = new ThreadPoolExecutor(
|
||||||
|
32, // 核心线程数
|
||||||
|
128, // 最大线程数
|
||||||
|
60L, // 空闲线程存活时间
|
||||||
|
TimeUnit.SECONDS, // 时间单位
|
||||||
|
new LinkedBlockingQueue<>(1000), // 任务队列
|
||||||
|
r -> {
|
||||||
|
Thread thread = new Thread(r);
|
||||||
|
thread.setName("face-recognition-" + thread.getId());
|
||||||
|
thread.setDaemon(false);
|
||||||
|
return thread;
|
||||||
|
},
|
||||||
|
new ThreadPoolExecutor.CallerRunsPolicy() // 超过容量时由调用者线程执行
|
||||||
|
);
|
||||||
|
|
||||||
|
log.info("人脸识别线程池初始化完成 - 核心线程数: {}, 最大线程数: {}, 队列容量: 1000",
|
||||||
|
executor.getCorePoolSize(), executor.getMaximumPoolSize());
|
||||||
|
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,36 @@
|
|||||||
|
package com.ycwl.basic.integration.kafka.config;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = "kafka")
|
||||||
|
public class KafkaIntegrationProperties {
|
||||||
|
|
||||||
|
private boolean enabled = false;
|
||||||
|
private String bootstrapServers = "100.64.0.12:39092";
|
||||||
|
private String ztMessageTopic = "zt-message"; // topic for zt-message microservice
|
||||||
|
private Consumer consumer = new Consumer();
|
||||||
|
private Producer producer = new Producer();
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Consumer {
|
||||||
|
private String groupId = "liuying-microservice";
|
||||||
|
private String autoOffsetReset = "earliest";
|
||||||
|
private String keyDeserializer = "org.apache.kafka.common.serialization.StringDeserializer";
|
||||||
|
private String valueDeserializer = "org.apache.kafka.common.serialization.StringDeserializer";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Producer {
|
||||||
|
private String acks = "all";
|
||||||
|
private Integer retries = 3;
|
||||||
|
private Integer batchSize = 16384;
|
||||||
|
private Integer lingerMs = 1;
|
||||||
|
private Integer bufferMemory = 33554432;
|
||||||
|
private String keySerializer = "org.apache.kafka.common.serialization.StringSerializer";
|
||||||
|
private String valueSerializer = "org.apache.kafka.common.serialization.StringSerializer";
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,83 @@
|
|||||||
|
package com.ycwl.basic.integration.kafka.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* zt-face topic消息结构
|
||||||
|
* 用于人脸处理任务的异步消息传递
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class FaceProcessingMessage {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸样本ID(外部系统传入)
|
||||||
|
*/
|
||||||
|
private Long faceSampleId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 景区ID
|
||||||
|
*/
|
||||||
|
private Long scenicId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备ID
|
||||||
|
*/
|
||||||
|
private Long deviceId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸图片URL
|
||||||
|
*/
|
||||||
|
private String faceUrl;
|
||||||
|
|
||||||
|
// 不再需要faceUniqueId,直接使用faceSampleId作为唯一标识
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拍摄时间
|
||||||
|
*/
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date shotTime;
|
||||||
|
|
||||||
|
// status字段已移除,由系统内部管理状态
|
||||||
|
|
||||||
|
// deviceConfig字段已移除,由系统内部通过deviceRepository查询
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息创建时间
|
||||||
|
*/
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date createTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息来源
|
||||||
|
*/
|
||||||
|
private String source;
|
||||||
|
|
||||||
|
// retryCount字段已移除,重试机制由系统内部管理
|
||||||
|
|
||||||
|
// DeviceConfig内部类已移除,不再需要在消息中传递设备配置
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建人脸处理消息的工厂方法(使用外部传入的faceId)
|
||||||
|
*/
|
||||||
|
public static FaceProcessingMessage create(Long externalFaceId, Long scenicId, Long deviceId,
|
||||||
|
String faceUrl, Date shotTime) {
|
||||||
|
return FaceProcessingMessage.builder()
|
||||||
|
.faceSampleId(externalFaceId) // 使用外部传入的ID作为唯一标识
|
||||||
|
.scenicId(scenicId)
|
||||||
|
.deviceId(deviceId)
|
||||||
|
.faceUrl(faceUrl)
|
||||||
|
.shotTime(shotTime)
|
||||||
|
.createTime(new Date())
|
||||||
|
.source("external-system")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,38 @@
|
|||||||
|
package com.ycwl.basic.integration.kafka.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class KafkaMessage<T> {
|
||||||
|
|
||||||
|
private String messageId;
|
||||||
|
private String topic;
|
||||||
|
private String eventType;
|
||||||
|
private T payload;
|
||||||
|
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private LocalDateTime timestamp;
|
||||||
|
|
||||||
|
private String source;
|
||||||
|
private String version;
|
||||||
|
|
||||||
|
public static <T> KafkaMessage<T> of(String topic, String eventType, T payload) {
|
||||||
|
return KafkaMessage.<T>builder()
|
||||||
|
.topic(topic)
|
||||||
|
.eventType(eventType)
|
||||||
|
.payload(payload)
|
||||||
|
.timestamp(LocalDateTime.now())
|
||||||
|
.source("liuying-microservice")
|
||||||
|
.version("1.0")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,194 @@
|
|||||||
|
package com.ycwl.basic.integration.kafka.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
|
||||||
|
import com.ycwl.basic.facebody.entity.AddFaceResp;
|
||||||
|
import com.ycwl.basic.integration.kafka.dto.FaceProcessingMessage;
|
||||||
|
import com.ycwl.basic.mapper.FaceSampleMapper;
|
||||||
|
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
||||||
|
import com.ycwl.basic.repository.DeviceRepository;
|
||||||
|
import com.ycwl.basic.service.pc.ScenicService;
|
||||||
|
import com.ycwl.basic.service.task.TaskFaceService;
|
||||||
|
import com.ycwl.basic.task.DynamicTaskGenerator;
|
||||||
|
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
|
||||||
|
// 不再需要SnowFlakeUtil,使用外部传入的ID
|
||||||
|
import com.ycwl.basic.utils.JacksonUtil;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.kafka.annotation.KafkaListener;
|
||||||
|
import org.springframework.kafka.support.Acknowledgment;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸处理Kafka消费服务
|
||||||
|
* 消费外部系统发送到zt-face topic的消息
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@ConditionalOnProperty(name = "kafka.enabled", havingValue = "true")
|
||||||
|
public class FaceProcessingKafkaService {
|
||||||
|
|
||||||
|
private static final String ZT_FACE_TOPIC = "zt-face";
|
||||||
|
|
||||||
|
private final FaceSampleMapper faceSampleMapper;
|
||||||
|
private final TaskFaceService taskFaceService;
|
||||||
|
private final ScenicService scenicService;
|
||||||
|
private final DeviceRepository deviceRepository;
|
||||||
|
private final ThreadPoolExecutor faceRecognitionExecutor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消费外部系统发送的人脸处理消息
|
||||||
|
* 先保存人脸样本数据,再进行异步人脸识别处理
|
||||||
|
*/
|
||||||
|
@KafkaListener(topics = ZT_FACE_TOPIC, containerFactory = "manualCommitKafkaListenerContainerFactory")
|
||||||
|
public void processFaceMessage(String message, Acknowledgment ack) {
|
||||||
|
try {
|
||||||
|
FaceProcessingMessage faceMessage = JacksonUtil.parseObject(message, FaceProcessingMessage.class);
|
||||||
|
log.debug("接收到外部人脸处理消息, scenicId: {}, deviceId: {}, faceUrl: {}",
|
||||||
|
faceMessage.getScenicId(), faceMessage.getDeviceId(), faceMessage.getFaceUrl());
|
||||||
|
|
||||||
|
// 使用外部传入的faceSampleId
|
||||||
|
Long externalFaceId = faceMessage.getFaceSampleId();
|
||||||
|
if (externalFaceId == null) {
|
||||||
|
log.error("外部消息中未包含faceSampleId");
|
||||||
|
// 即使消息格式错误,也消费消息避免重复处理
|
||||||
|
ack.acknowledge();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先保存人脸样本数据
|
||||||
|
boolean saved = saveFaceSample(faceMessage, externalFaceId);
|
||||||
|
|
||||||
|
// 然后异步进行人脸识别处理(使用专用线程池)
|
||||||
|
if (saved) {
|
||||||
|
faceRecognitionExecutor.execute(() -> processFaceRecognitionAsync(faceMessage));
|
||||||
|
log.debug("人脸识别任务已提交至线程池, faceSampleId: {}, 活跃线程: {}, 队列大小: {}",
|
||||||
|
externalFaceId, faceRecognitionExecutor.getActiveCount(),
|
||||||
|
faceRecognitionExecutor.getQueue().size());
|
||||||
|
} else {
|
||||||
|
log.warn("人脸样本保存失败,但消息仍将被消费, faceSampleId: {}", externalFaceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 无论处理是否成功,都消费消息
|
||||||
|
ack.acknowledge();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("处理外部人脸消息失败: {}", e.getMessage(), e);
|
||||||
|
// 即使发生异常也消费消息,避免消息堆积
|
||||||
|
ack.acknowledge();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存人脸样本数据到数据库
|
||||||
|
* @param faceMessage 人脸处理消息
|
||||||
|
* @param externalFaceId 外部传入的人脸ID
|
||||||
|
* @return 是否保存成功
|
||||||
|
*/
|
||||||
|
private boolean saveFaceSample(FaceProcessingMessage faceMessage, Long externalFaceId) {
|
||||||
|
try {
|
||||||
|
FaceSampleEntity faceSample = new FaceSampleEntity();
|
||||||
|
faceSample.setId(externalFaceId); // 使用外部传入的ID
|
||||||
|
faceSample.setScenicId(faceMessage.getScenicId());
|
||||||
|
faceSample.setDeviceId(faceMessage.getDeviceId());
|
||||||
|
faceSample.setStatus(0); // 初始状态
|
||||||
|
faceSample.setFaceUrl(faceMessage.getFaceUrl());
|
||||||
|
|
||||||
|
// 转换时间格式
|
||||||
|
if (faceMessage.getShotTime() != null) {
|
||||||
|
Date shotTime = faceMessage.getShotTime();
|
||||||
|
faceSample.setCreateAt(shotTime);
|
||||||
|
} else {
|
||||||
|
faceSample.setCreateAt(new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到数据库
|
||||||
|
faceSampleMapper.add(faceSample);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("保存人脸样本数据失败, 外部faceId: {}, scenicId: {}, deviceId: {}",
|
||||||
|
externalFaceId, faceMessage.getScenicId(), faceMessage.getDeviceId(), e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步执行人脸识别处理逻辑
|
||||||
|
* 对已保存的人脸样本进行识别处理
|
||||||
|
*/
|
||||||
|
private void processFaceRecognitionAsync(FaceProcessingMessage message) {
|
||||||
|
Long faceSampleId = message.getFaceSampleId();
|
||||||
|
Long scenicId = message.getScenicId();
|
||||||
|
String faceUrl = message.getFaceUrl();
|
||||||
|
|
||||||
|
// 直接使用faceSampleId作为唯一标识
|
||||||
|
String faceUniqueId = faceSampleId.toString();
|
||||||
|
|
||||||
|
// 获取人脸识别适配器
|
||||||
|
IFaceBodyAdapter faceBodyAdapter = scenicService.getScenicFaceBodyAdapter(scenicId);
|
||||||
|
if (faceBodyAdapter == null) {
|
||||||
|
log.error("人脸识别适配器不存在, scenicId: {}", scenicId);
|
||||||
|
updateFaceSampleStatus(faceSampleId, -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 更新状态为处理中
|
||||||
|
updateFaceSampleStatus(faceSampleId, 1);
|
||||||
|
|
||||||
|
// 确保人脸数据库存在
|
||||||
|
taskFaceService.assureFaceDb(faceBodyAdapter, scenicId.toString());
|
||||||
|
|
||||||
|
// 添加人脸到识别服务(使用faceSampleId作为唯一标识)
|
||||||
|
AddFaceResp addFaceResp = faceBodyAdapter.addFace(
|
||||||
|
scenicId.toString(),
|
||||||
|
faceSampleId.toString(),
|
||||||
|
faceUrl,
|
||||||
|
faceUniqueId // 即faceSampleId.toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (addFaceResp != null) {
|
||||||
|
// 更新人脸样本得分和状态
|
||||||
|
faceSampleMapper.updateScore(faceSampleId, addFaceResp.getScore());
|
||||||
|
updateFaceSampleStatus(faceSampleId, 2);
|
||||||
|
log.info("人脸识别处理成功, faceSampleId: {}", faceSampleId);
|
||||||
|
|
||||||
|
// 查询设备配置,判断是否启用预订功能
|
||||||
|
Long deviceId = message.getDeviceId();
|
||||||
|
DeviceConfigManager deviceConfig = deviceRepository.getDeviceConfigManager(deviceId);
|
||||||
|
if (deviceConfig != null &&
|
||||||
|
Integer.valueOf(1).equals(deviceConfig.getInteger("enable_pre_book"))) {
|
||||||
|
DynamicTaskGenerator.addTask(faceSampleId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("人脸添加返回空结果, faceSampleId: {}", faceSampleId);
|
||||||
|
updateFaceSampleStatus(faceSampleId, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("人脸识别处理失败, faceSampleId: {}, error: {}",
|
||||||
|
faceSampleId, e.getMessage(), e);
|
||||||
|
|
||||||
|
// 标记人脸样本为处理失败状态
|
||||||
|
updateFaceSampleStatus(faceSampleId, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新人脸样本状态
|
||||||
|
*/
|
||||||
|
private void updateFaceSampleStatus(Long faceSampleId, Integer status) {
|
||||||
|
try {
|
||||||
|
faceSampleMapper.updateStatus(faceSampleId, status);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("更新人脸样本状态失败, faceSampleId: {}", faceSampleId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,51 @@
|
|||||||
|
package com.ycwl.basic.integration.kafka.service;
|
||||||
|
|
||||||
|
import com.ycwl.basic.integration.kafka.config.KafkaIntegrationProperties;
|
||||||
|
import com.ycwl.basic.integration.kafka.dto.KafkaMessage;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.kafka.core.KafkaTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@ConditionalOnProperty(name = "kafka.enabled", havingValue = "true")
|
||||||
|
public class KafkaIntegrationService {
|
||||||
|
|
||||||
|
private final KafkaTemplate<String, String> kafkaTemplate;
|
||||||
|
private final KafkaIntegrationProperties kafkaProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试Kafka连接
|
||||||
|
*/
|
||||||
|
public boolean testConnection() {
|
||||||
|
try {
|
||||||
|
log.info("Testing Kafka connection to: {}", kafkaProperties.getBootstrapServers());
|
||||||
|
|
||||||
|
// 尝试获取元数据以测试连接
|
||||||
|
var metadata = kafkaTemplate.getProducerFactory().createProducer().partitionsFor("test-topic");
|
||||||
|
log.info("Kafka connection test successful");
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Kafka connection test failed", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送消息(预留接口,暂不实现具体逻辑)
|
||||||
|
*/
|
||||||
|
public void sendMessage(String topic, String key, KafkaMessage<?> message) {
|
||||||
|
log.info("Kafka message sending is not implemented yet. Topic: {}, Key: {}", topic, key);
|
||||||
|
// TODO: 后续实现具体的消息发送逻辑
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取Kafka配置信息
|
||||||
|
*/
|
||||||
|
public KafkaIntegrationProperties getKafkaProperties() {
|
||||||
|
return kafkaProperties;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,29 @@
|
|||||||
|
package com.ycwl.basic.integration.message.client;
|
||||||
|
|
||||||
|
import com.ycwl.basic.integration.common.response.CommonResponse;
|
||||||
|
import com.ycwl.basic.integration.message.dto.ChannelsResponse;
|
||||||
|
import com.ycwl.basic.integration.message.dto.MessageListData;
|
||||||
|
import org.springframework.cloud.openfeign.FeignClient;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
|
@FeignClient(name = "zt-message", contextId = "zt-message", path = "")
|
||||||
|
public interface MessageClient {
|
||||||
|
|
||||||
|
@GetMapping("/messages")
|
||||||
|
CommonResponse<MessageListData> listMessages(
|
||||||
|
@RequestParam(name = "page", defaultValue = "1") Integer page,
|
||||||
|
@RequestParam(name = "pageSize", defaultValue = "20") Integer pageSize,
|
||||||
|
@RequestParam(name = "channelId", required = false) String channelId,
|
||||||
|
@RequestParam(name = "title", required = false) String title,
|
||||||
|
@RequestParam(name = "content", required = false) String content,
|
||||||
|
@RequestParam(name = "sendBiz", required = false) String sendBiz,
|
||||||
|
@RequestParam(name = "sentAtStart", required = false) String sentAtStart,
|
||||||
|
@RequestParam(name = "sentAtEnd", required = false) String sentAtEnd,
|
||||||
|
@RequestParam(name = "createdAtStart", required = false) String createdAtStart,
|
||||||
|
@RequestParam(name = "createdAtEnd", required = false) String createdAtEnd
|
||||||
|
);
|
||||||
|
|
||||||
|
@GetMapping("/channels")
|
||||||
|
CommonResponse<ChannelsResponse> listChannels();
|
||||||
|
}
|
@@ -0,0 +1,10 @@
|
|||||||
|
package com.ycwl.basic.integration.message.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class ChannelsResponse {
|
||||||
|
private List<String> channels;
|
||||||
|
}
|
@@ -0,0 +1,13 @@
|
|||||||
|
package com.ycwl.basic.integration.message.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class MessageListData {
|
||||||
|
private List<MessageRecordDTO> list;
|
||||||
|
private String total; // string to avoid JS precision
|
||||||
|
private Integer page;
|
||||||
|
private Integer pageSize;
|
||||||
|
}
|
@@ -0,0 +1,25 @@
|
|||||||
|
package com.ycwl.basic.integration.message.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class MessageRecordDTO {
|
||||||
|
private String id; // string to avoid JS precision
|
||||||
|
private String channelId;
|
||||||
|
private String title;
|
||||||
|
private String content;
|
||||||
|
private String target;
|
||||||
|
private Map<String, Object> extraJson;
|
||||||
|
private String sendReason;
|
||||||
|
private String sendBiz;
|
||||||
|
private String status;
|
||||||
|
private String errorMsg;
|
||||||
|
private Integer attempts;
|
||||||
|
private String sentAt; // RFC3339 or yyyy-MM-dd HH:mm:ss (pass-through)
|
||||||
|
private String createdAt;
|
||||||
|
private String updatedAt;
|
||||||
|
}
|
@@ -0,0 +1,36 @@
|
|||||||
|
package com.ycwl.basic.integration.message.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class ZtMessage {
|
||||||
|
private String messageId; // unique message identifier
|
||||||
|
private String channelId; // required
|
||||||
|
private String title; // required
|
||||||
|
private String content; // required
|
||||||
|
private String target; // required
|
||||||
|
private Map<String, Object> extra; // optional
|
||||||
|
private String sendReason; // optional
|
||||||
|
private String sendBiz; // optional
|
||||||
|
|
||||||
|
public static ZtMessage of(String channelId, String title, String content, String target) {
|
||||||
|
return ZtMessage.builder()
|
||||||
|
.channelId(channelId)
|
||||||
|
.title(title)
|
||||||
|
.content(content)
|
||||||
|
.target(target)
|
||||||
|
.extra(new HashMap<>())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,57 @@
|
|||||||
|
package com.ycwl.basic.integration.message.service;
|
||||||
|
|
||||||
|
import com.ycwl.basic.integration.common.exception.IntegrationException;
|
||||||
|
import com.ycwl.basic.integration.common.response.CommonResponse;
|
||||||
|
import com.ycwl.basic.integration.common.service.IntegrationFallbackService;
|
||||||
|
import com.ycwl.basic.integration.message.client.MessageClient;
|
||||||
|
import com.ycwl.basic.integration.message.dto.ChannelsResponse;
|
||||||
|
import com.ycwl.basic.integration.message.dto.MessageListData;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MessageIntegrationService {
|
||||||
|
|
||||||
|
private final MessageClient client;
|
||||||
|
private final IntegrationFallbackService fallbackService;
|
||||||
|
|
||||||
|
private static final String SERVICE_NAME = "zt-message";
|
||||||
|
|
||||||
|
public MessageListData listMessages(Integer page, Integer pageSize,
|
||||||
|
String channelId, String title, String content, String sendBiz,
|
||||||
|
String sentAtStart, String sentAtEnd,
|
||||||
|
String createdAtStart, String createdAtEnd) {
|
||||||
|
log.debug("查询消息列表 page={}, pageSize={}, channelId={}, title={}, sendBiz={}", page, pageSize, channelId, title, sendBiz);
|
||||||
|
CommonResponse<MessageListData> resp = client.listMessages(page, pageSize, channelId, title, content, sendBiz,
|
||||||
|
sentAtStart, sentAtEnd, createdAtStart, createdAtEnd);
|
||||||
|
return handleResponse(resp, "查询消息列表失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChannelsResponse listChannels() {
|
||||||
|
log.debug("查询消息通道列表");
|
||||||
|
// 相对稳定的数据,使用fallback缓存
|
||||||
|
return fallbackService.executeWithFallback(
|
||||||
|
SERVICE_NAME,
|
||||||
|
"channels",
|
||||||
|
() -> {
|
||||||
|
CommonResponse<ChannelsResponse> resp = client.listChannels();
|
||||||
|
return handleResponse(resp, "查询通道列表失败");
|
||||||
|
},
|
||||||
|
ChannelsResponse.class
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> T handleResponse(CommonResponse<T> response, String errorMessage) {
|
||||||
|
if (response == null || !response.isSuccess()) {
|
||||||
|
String msg = response != null && response.getMessage() != null
|
||||||
|
? response.getMessage()
|
||||||
|
: errorMessage;
|
||||||
|
Integer code = response != null ? response.getCode() : 5000;
|
||||||
|
throw new IntegrationException(code, msg, SERVICE_NAME);
|
||||||
|
}
|
||||||
|
return response.getData();
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,52 @@
|
|||||||
|
package com.ycwl.basic.integration.message.service;
|
||||||
|
|
||||||
|
import com.ycwl.basic.integration.message.dto.ZtMessage;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ZT消息生产者空实现服务
|
||||||
|
* <p>
|
||||||
|
* 当 kafka.enabled=false 时,该服务会被激活,作为 ZtMessageProducerService 的替代。
|
||||||
|
* 所有消息发送操作都会被忽略,只记录日志。
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @see ZtMessageProducerService
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@ConditionalOnProperty(name = "kafka.enabled", havingValue = "false", matchIfMissing = true)
|
||||||
|
public class ZtMessageProducerNoOpService extends ZtMessageProducerService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 空构造函数
|
||||||
|
* 由于父类需要依赖项,但在此实现中不会使用,因此传入 null
|
||||||
|
*/
|
||||||
|
public ZtMessageProducerNoOpService() {
|
||||||
|
super(null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息发送的空操作实现
|
||||||
|
* <p>
|
||||||
|
* 当 Kafka 未启用时,此方法会被调用。
|
||||||
|
* 它不会实际发送消息,只会记录一条 debug 日志。
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param msg 待发送的消息(会被验证基本字段)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void send(ZtMessage msg) {
|
||||||
|
if (msg == null) {
|
||||||
|
log.debug("[ZT-MESSAGE] Kafka未启用,跳过消息发送(消息为null)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("[ZT-MESSAGE] Kafka未启用,跳过消息发送: channelId={}, title={}, target={}, messageId={}",
|
||||||
|
msg.getChannelId(),
|
||||||
|
msg.getTitle(),
|
||||||
|
msg.getTarget(),
|
||||||
|
msg.getMessageId());
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,70 @@
|
|||||||
|
package com.ycwl.basic.integration.message.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.ycwl.basic.integration.kafka.config.KafkaIntegrationProperties;
|
||||||
|
import com.ycwl.basic.integration.message.dto.ZtMessage;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.kafka.core.KafkaTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@ConditionalOnProperty(name = "kafka.enabled", havingValue = "true")
|
||||||
|
public class ZtMessageProducerService {
|
||||||
|
|
||||||
|
public static final String DEFAULT_TOPIC = "zt-message";
|
||||||
|
|
||||||
|
private final KafkaTemplate<String, String> kafkaTemplate;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
private final KafkaIntegrationProperties kafkaProps;
|
||||||
|
|
||||||
|
public void send(ZtMessage msg) {
|
||||||
|
validate(msg);
|
||||||
|
|
||||||
|
// Generate messageId if not present
|
||||||
|
if (StringUtils.isBlank(msg.getMessageId())) {
|
||||||
|
msg.setMessageId(java.util.UUID.randomUUID().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
String topic = kafkaProps != null && StringUtils.isNotBlank(kafkaProps.getZtMessageTopic())
|
||||||
|
? kafkaProps.getZtMessageTopic()
|
||||||
|
: DEFAULT_TOPIC;
|
||||||
|
String key = msg.getChannelId();
|
||||||
|
String payload = toJson(msg);
|
||||||
|
|
||||||
|
log.info("[ZT-MESSAGE] producing to topic={}, key={}, messageId={}, title={}", topic, key, msg.getMessageId(), msg.getTitle());
|
||||||
|
kafkaTemplate.send(topic, key, payload).whenComplete((metadata, ex) -> {
|
||||||
|
if (ex != null) {
|
||||||
|
log.error("[ZT-MESSAGE] produce failed: messageId={}, error={}", msg.getMessageId(), ex.getMessage(), ex);
|
||||||
|
} else if (metadata != null) {
|
||||||
|
log.info("[ZT-MESSAGE] produced: messageId={}, partition={}, offset={}", msg.getMessageId(), metadata.getRecordMetadata().partition(), metadata.getRecordMetadata().offset());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validate(ZtMessage msg) {
|
||||||
|
if (msg == null) throw new IllegalArgumentException("message is null");
|
||||||
|
if (StringUtils.isBlank(msg.getChannelId())) throw new IllegalArgumentException("channelId is required");
|
||||||
|
if (StringUtils.isBlank(msg.getTitle())) throw new IllegalArgumentException("title is required");
|
||||||
|
if (StringUtils.isBlank(msg.getContent())) throw new IllegalArgumentException("content is required");
|
||||||
|
if (StringUtils.isBlank(msg.getTarget())) throw new IllegalArgumentException("target is required");
|
||||||
|
if (msg.getExtra() != null && !(msg.getExtra() instanceof Map)) {
|
||||||
|
throw new IllegalArgumentException("extra must be a Map");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String toJson(ZtMessage msg) {
|
||||||
|
try {
|
||||||
|
return objectMapper.writeValueAsString(msg);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new IllegalArgumentException("failed to serialize message", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,306 +0,0 @@
|
|||||||
package com.ycwl.basic.integration.questionnaire.example;
|
|
||||||
|
|
||||||
import com.ycwl.basic.integration.common.response.PageResponse;
|
|
||||||
import com.ycwl.basic.integration.common.service.IntegrationFallbackService;
|
|
||||||
import com.ycwl.basic.integration.questionnaire.dto.answer.AnswerRequest;
|
|
||||||
import com.ycwl.basic.integration.questionnaire.dto.answer.ResponseDetailResponse;
|
|
||||||
import com.ycwl.basic.integration.questionnaire.dto.answer.SubmitAnswerRequest;
|
|
||||||
import com.ycwl.basic.integration.questionnaire.dto.question.CreateQuestionOptionRequest;
|
|
||||||
import com.ycwl.basic.integration.questionnaire.dto.question.CreateQuestionRequest;
|
|
||||||
import com.ycwl.basic.integration.questionnaire.dto.questionnaire.CreateQuestionnaireRequest;
|
|
||||||
import com.ycwl.basic.integration.questionnaire.dto.questionnaire.QuestionnaireResponse;
|
|
||||||
import com.ycwl.basic.integration.questionnaire.dto.statistics.QuestionnaireStatistics;
|
|
||||||
import com.ycwl.basic.integration.questionnaire.service.QuestionnaireIntegrationService;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
||||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
|
||||||
import org.springframework.context.event.EventListener;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@ConditionalOnProperty(prefix = "integration.questionnaire.example", name = "enabled", havingValue = "true")
|
|
||||||
public class QuestionnaireIntegrationExample {
|
|
||||||
|
|
||||||
private final QuestionnaireIntegrationService questionnaireService;
|
|
||||||
private final IntegrationFallbackService fallbackService;
|
|
||||||
|
|
||||||
@EventListener(ApplicationReadyEvent.class)
|
|
||||||
public void runExamples() {
|
|
||||||
try {
|
|
||||||
log.info("=== 开始问卷集成服务示例 ===");
|
|
||||||
|
|
||||||
// 示例1:创建问卷
|
|
||||||
createQuestionnaireExample();
|
|
||||||
|
|
||||||
// 示例2:查询问卷
|
|
||||||
queryQuestionnaireExample();
|
|
||||||
|
|
||||||
// 示例3:提交答案
|
|
||||||
submitAnswerExample();
|
|
||||||
|
|
||||||
// 示例4:统计查询
|
|
||||||
statisticsExample();
|
|
||||||
|
|
||||||
// 示例5:Fallback 缓存管理
|
|
||||||
fallbackCacheExample();
|
|
||||||
|
|
||||||
log.info("=== 问卷集成服务示例完成 ===");
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("问卷集成服务示例执行失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 示例1:创建问卷
|
|
||||||
*/
|
|
||||||
private void createQuestionnaireExample() {
|
|
||||||
log.info("--- 示例1:创建客户满意度问卷 ---");
|
|
||||||
|
|
||||||
try {
|
|
||||||
CreateQuestionnaireRequest request = new CreateQuestionnaireRequest();
|
|
||||||
request.setName("客户满意度调查");
|
|
||||||
request.setDescription("用于了解客户对我们服务的满意度");
|
|
||||||
request.setIsAnonymous(true);
|
|
||||||
request.setMaxAnswers(1000);
|
|
||||||
|
|
||||||
// 添加单选题
|
|
||||||
CreateQuestionRequest question1 = new CreateQuestionRequest();
|
|
||||||
question1.setTitle("您对我们的服务满意吗?");
|
|
||||||
question1.setType(1); // 单选题
|
|
||||||
question1.setIsRequired(true);
|
|
||||||
question1.setSort(1);
|
|
||||||
|
|
||||||
List<CreateQuestionOptionRequest> options1 = new ArrayList<>();
|
|
||||||
options1.add(new CreateQuestionOptionRequest("非常满意", "5", 1));
|
|
||||||
options1.add(new CreateQuestionOptionRequest("满意", "4", 2));
|
|
||||||
options1.add(new CreateQuestionOptionRequest("一般", "3", 3));
|
|
||||||
options1.add(new CreateQuestionOptionRequest("不满意", "2", 4));
|
|
||||||
options1.add(new CreateQuestionOptionRequest("非常不满意", "1", 5));
|
|
||||||
question1.setOptions(options1);
|
|
||||||
|
|
||||||
// 添加多选题
|
|
||||||
CreateQuestionRequest question2 = new CreateQuestionRequest();
|
|
||||||
question2.setTitle("您感兴趣的服务有哪些?");
|
|
||||||
question2.setType(2); // 多选题
|
|
||||||
question2.setIsRequired(false);
|
|
||||||
question2.setSort(2);
|
|
||||||
|
|
||||||
List<CreateQuestionOptionRequest> options2 = new ArrayList<>();
|
|
||||||
options2.add(new CreateQuestionOptionRequest("技术支持", "tech_support", 1));
|
|
||||||
options2.add(new CreateQuestionOptionRequest("产品培训", "training", 2));
|
|
||||||
options2.add(new CreateQuestionOptionRequest("定制开发", "custom_dev", 3));
|
|
||||||
options2.add(new CreateQuestionOptionRequest("其他", "others", 4));
|
|
||||||
question2.setOptions(options2);
|
|
||||||
|
|
||||||
// 添加文本域题
|
|
||||||
CreateQuestionRequest question3 = new CreateQuestionRequest();
|
|
||||||
question3.setTitle("您还有什么建议吗?");
|
|
||||||
question3.setType(4); // 文本域题
|
|
||||||
question3.setIsRequired(false);
|
|
||||||
question3.setSort(3);
|
|
||||||
question3.setOptions(null); // 文本域题不需要选项
|
|
||||||
|
|
||||||
request.setQuestions(Arrays.asList(question1, question2, question3));
|
|
||||||
|
|
||||||
QuestionnaireResponse response = questionnaireService.createQuestionnaire(request, "admin");
|
|
||||||
log.info("✅ 问卷创建成功,ID: {}, 名称: {}", response.getId(), response.getName());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("❌ 创建问卷示例失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 示例2:查询问卷
|
|
||||||
*/
|
|
||||||
private void queryQuestionnaireExample() {
|
|
||||||
log.info("--- 示例2:查询问卷示例 ---");
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 获取问卷列表(支持 fallback)
|
|
||||||
PageResponse<QuestionnaireResponse> listResponse = questionnaireService.getQuestionnaireList(1, 10, null, null, null);
|
|
||||||
log.info("✅ 问卷列表查询成功,总数: {}, 当前页数据: {}",
|
|
||||||
listResponse.getTotal(), listResponse.getList().size());
|
|
||||||
|
|
||||||
if (listResponse.getList() != null && !listResponse.getList().isEmpty()) {
|
|
||||||
Long questionnaireId = listResponse.getList().get(0).getId();
|
|
||||||
|
|
||||||
// 获取问卷详情(支持 fallback)
|
|
||||||
QuestionnaireResponse detailResponse = questionnaireService.getQuestionnaire(questionnaireId);
|
|
||||||
log.info("✅ 问卷详情查询成功,ID: {}, 名称: {}, 问题数: {}",
|
|
||||||
detailResponse.getId(), detailResponse.getName(),
|
|
||||||
detailResponse.getQuestions() != null ? detailResponse.getQuestions().size() : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("❌ 查询问卷示例失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 示例3:提交答案
|
|
||||||
*/
|
|
||||||
private void submitAnswerExample() {
|
|
||||||
log.info("--- 示例3:提交问卷答案示例 ---");
|
|
||||||
|
|
||||||
try {
|
|
||||||
SubmitAnswerRequest request = new SubmitAnswerRequest();
|
|
||||||
request.setQuestionnaireId(1001L);
|
|
||||||
request.setUserId("user123");
|
|
||||||
|
|
||||||
List<AnswerRequest> answers = new ArrayList<>();
|
|
||||||
// 单选题答案
|
|
||||||
answers.add(new AnswerRequest(123L, "4")); // 满意
|
|
||||||
// 多选题答案
|
|
||||||
answers.add(new AnswerRequest(124L, "tech_support,training")); // 技术支持和产品培训
|
|
||||||
// 文本域题答案
|
|
||||||
answers.add(new AnswerRequest(125L, "服务很好,希望能增加更多实用功能"));
|
|
||||||
|
|
||||||
request.setAnswers(answers);
|
|
||||||
|
|
||||||
ResponseDetailResponse response = questionnaireService.submitAnswer(request);
|
|
||||||
log.info("✅ 问卷答案提交成功,回答ID: {}, 提交时间: {}",
|
|
||||||
response.getId(), response.getSubmittedAt());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("❌ 提交答案示例失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 示例4:统计查询
|
|
||||||
*/
|
|
||||||
private void statisticsExample() {
|
|
||||||
log.info("--- 示例4:问卷统计查询示例 ---");
|
|
||||||
|
|
||||||
try {
|
|
||||||
Long questionnaireId = 1001L;
|
|
||||||
|
|
||||||
// 获取问卷统计(支持 fallback)
|
|
||||||
QuestionnaireStatistics stats = questionnaireService.getStatistics(questionnaireId);
|
|
||||||
log.info("✅ 统计查询成功,总回答数: {}, 完成率: {}%, 平均用时: {}秒",
|
|
||||||
stats.getTotalResponses(),
|
|
||||||
stats.getCompletionRate() != null ? stats.getCompletionRate() * 100 : 0,
|
|
||||||
stats.getAverageTime());
|
|
||||||
|
|
||||||
// 获取回答记录列表(支持 fallback)
|
|
||||||
questionnaireService.getResponseList(1, 10, questionnaireId, null, null, null);
|
|
||||||
log.info("✅ 回答记录列表查询成功");
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("❌ 统计查询示例失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 示例5:Fallback 缓存管理
|
|
||||||
*/
|
|
||||||
private void fallbackCacheExample() {
|
|
||||||
log.info("--- 示例5:Fallback 缓存管理示例 ---");
|
|
||||||
|
|
||||||
try {
|
|
||||||
String serviceName = "zt-questionnaire";
|
|
||||||
|
|
||||||
// 检查缓存状态
|
|
||||||
boolean hasQuestionnaireCache = fallbackService.hasFallbackCache(serviceName, "questionnaire:1001");
|
|
||||||
boolean hasListCache = fallbackService.hasFallbackCache(serviceName, "questionnaire:list:1:10:null:null:null");
|
|
||||||
log.info("✅ 缓存状态检查 - 问卷缓存: {}, 列表缓存: {}", hasQuestionnaireCache, hasListCache);
|
|
||||||
|
|
||||||
// 获取缓存统计
|
|
||||||
IntegrationFallbackService.FallbackCacheStats stats = fallbackService.getFallbackCacheStats(serviceName);
|
|
||||||
log.info("✅ 缓存统计 - 缓存项目数: {}, TTL: {} 天",
|
|
||||||
stats.getTotalCacheCount(), stats.getFallbackTtlDays());
|
|
||||||
|
|
||||||
// 清理特定缓存示例(仅演示,实际使用时谨慎操作)
|
|
||||||
// fallbackService.clearFallbackCache(serviceName, "questionnaire:1001");
|
|
||||||
// log.info("✅ 已清理问卷缓存");
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("❌ Fallback 缓存管理示例失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 问卷管理工作流示例
|
|
||||||
*/
|
|
||||||
public void questionnaireWorkflowExample(String userId) {
|
|
||||||
log.info("--- 问卷管理工作流示例 ---");
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. 创建问卷
|
|
||||||
CreateQuestionnaireRequest createRequest = createSampleQuestionnaire();
|
|
||||||
QuestionnaireResponse questionnaire = questionnaireService.createQuestionnaire(createRequest, userId);
|
|
||||||
log.info("✅ 步骤1 - 问卷创建成功: {}", questionnaire.getName());
|
|
||||||
|
|
||||||
Long questionnaireId = questionnaire.getId();
|
|
||||||
|
|
||||||
// 2. 发布问卷
|
|
||||||
QuestionnaireResponse published = questionnaireService.publishQuestionnaire(questionnaireId, userId);
|
|
||||||
log.info("✅ 步骤2 - 问卷发布成功,状态: {}", published.getStatusText());
|
|
||||||
|
|
||||||
// 3. 模拟用户提交答案
|
|
||||||
SubmitAnswerRequest answerRequest = createSampleAnswers(questionnaireId);
|
|
||||||
ResponseDetailResponse answerResponse = questionnaireService.submitAnswer(answerRequest);
|
|
||||||
log.info("✅ 步骤3 - 答案提交成功: {}", answerResponse.getId());
|
|
||||||
|
|
||||||
// 4. 查看统计数据
|
|
||||||
QuestionnaireStatistics statistics = questionnaireService.getStatistics(questionnaireId);
|
|
||||||
log.info("✅ 步骤4 - 统计查询成功,回答数: {}", statistics.getTotalResponses());
|
|
||||||
|
|
||||||
// 5. 停止问卷
|
|
||||||
QuestionnaireResponse stopped = questionnaireService.stopQuestionnaire(questionnaireId, userId);
|
|
||||||
log.info("✅ 步骤5 - 问卷停止成功,状态: {}", stopped.getStatusText());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("❌ 问卷管理工作流示例失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private CreateQuestionnaireRequest createSampleQuestionnaire() {
|
|
||||||
CreateQuestionnaireRequest request = new CreateQuestionnaireRequest();
|
|
||||||
request.setName("产品体验调查");
|
|
||||||
request.setDescription("收集用户对产品的使用体验反馈");
|
|
||||||
request.setIsAnonymous(false);
|
|
||||||
request.setMaxAnswers(500);
|
|
||||||
|
|
||||||
// 评分题
|
|
||||||
CreateQuestionRequest ratingQuestion = new CreateQuestionRequest();
|
|
||||||
ratingQuestion.setTitle("请对我们的产品进行评分(1-10分)");
|
|
||||||
ratingQuestion.setType(5); // 评分题
|
|
||||||
ratingQuestion.setIsRequired(true);
|
|
||||||
ratingQuestion.setSort(1);
|
|
||||||
ratingQuestion.setOptions(null); // 评分题不需要选项
|
|
||||||
|
|
||||||
// 填空题
|
|
||||||
CreateQuestionRequest textQuestion = new CreateQuestionRequest();
|
|
||||||
textQuestion.setTitle("请输入您的姓名");
|
|
||||||
textQuestion.setType(3); // 填空题
|
|
||||||
textQuestion.setIsRequired(true);
|
|
||||||
textQuestion.setSort(2);
|
|
||||||
textQuestion.setOptions(null); // 填空题不需要选项
|
|
||||||
|
|
||||||
request.setQuestions(Arrays.asList(ratingQuestion, textQuestion));
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
private SubmitAnswerRequest createSampleAnswers(Long questionnaireId) {
|
|
||||||
SubmitAnswerRequest request = new SubmitAnswerRequest();
|
|
||||||
request.setQuestionnaireId(questionnaireId);
|
|
||||||
request.setUserId("test_user");
|
|
||||||
|
|
||||||
List<AnswerRequest> answers = new ArrayList<>();
|
|
||||||
answers.add(new AnswerRequest(1L, "8")); // 评分题答案
|
|
||||||
answers.add(new AnswerRequest(2L, "张三")); // 填空题答案
|
|
||||||
|
|
||||||
request.setAnswers(answers);
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -18,12 +18,6 @@ public interface RenderWorkerV2Client {
|
|||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
CommonResponse<RenderWorkerV2DTO> getWorker(@PathVariable("id") Long id);
|
CommonResponse<RenderWorkerV2DTO> getWorker(@PathVariable("id") Long id);
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取工作器含配置信息
|
|
||||||
*/
|
|
||||||
@GetMapping("/{id}/with-config")
|
|
||||||
CommonResponse<RenderWorkerV2WithConfigDTO> getWorkerWithConfig(@PathVariable("id") Long id);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建工作器
|
* 创建工作器
|
||||||
*/
|
*/
|
||||||
@@ -52,25 +46,9 @@ public interface RenderWorkerV2Client {
|
|||||||
@RequestParam(required = false) Integer isEnabled,
|
@RequestParam(required = false) Integer isEnabled,
|
||||||
@RequestParam(required = false) String name);
|
@RequestParam(required = false) String name);
|
||||||
|
|
||||||
/**
|
|
||||||
* 分页查询工作器列表(含配置信息)
|
|
||||||
*/
|
|
||||||
@GetMapping("/with-config")
|
|
||||||
CommonResponse<PageResponse<RenderWorkerV2WithConfigDTO>> listWorkersWithConfig(
|
|
||||||
@RequestParam(defaultValue = "1") Integer page,
|
|
||||||
@RequestParam(defaultValue = "10") Integer pageSize,
|
|
||||||
@RequestParam(required = false) Integer isEnabled,
|
|
||||||
@RequestParam(required = false) String name);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据key获取工作器核心信息
|
* 根据key获取工作器核心信息
|
||||||
*/
|
*/
|
||||||
@GetMapping("/key/{key}")
|
@GetMapping("/key/{key}")
|
||||||
CommonResponse<RenderWorkerV2DTO> getWorkerByKey(@PathVariable("key") String key);
|
CommonResponse<RenderWorkerV2DTO> getWorkerByKey(@PathVariable("key") String key);
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据key获取工作器完整信息(含配置)
|
|
||||||
*/
|
|
||||||
@GetMapping("/key/{key}/with-config")
|
|
||||||
CommonResponse<RenderWorkerV2WithConfigDTO> getWorkerWithConfigByKey(@PathVariable("key") String key);
|
|
||||||
}
|
}
|
@@ -70,7 +70,7 @@ public class RenderWorkerConfigIntegrationService {
|
|||||||
log.debug("获取渲染工作器平铺配置, workerId: {}", workerId);
|
log.debug("获取渲染工作器平铺配置, workerId: {}", workerId);
|
||||||
return fallbackService.executeWithFallback(
|
return fallbackService.executeWithFallback(
|
||||||
SERVICE_NAME,
|
SERVICE_NAME,
|
||||||
"worker:flat:config:" + workerId,
|
"worker:config:" + workerId,
|
||||||
() -> {
|
() -> {
|
||||||
List<RenderWorkerConfigV2DTO> configs = getWorkerConfigsInternal(workerId);
|
List<RenderWorkerConfigV2DTO> configs = getWorkerConfigsInternal(workerId);
|
||||||
return flattenConfigs(configs);
|
return flattenConfigs(configs);
|
||||||
|
@@ -42,22 +42,6 @@ public class RenderWorkerIntegrationService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取工作器详细信息(含配置)(带降级)
|
|
||||||
*/
|
|
||||||
public RenderWorkerV2WithConfigDTO getWorkerWithConfig(Long id) {
|
|
||||||
log.debug("获取渲染工作器详细信息, id: {}", id);
|
|
||||||
return fallbackService.executeWithFallback(
|
|
||||||
SERVICE_NAME,
|
|
||||||
"worker:config:" + id,
|
|
||||||
() -> {
|
|
||||||
CommonResponse<RenderWorkerV2WithConfigDTO> response = renderWorkerV2Client.getWorkerWithConfig(id);
|
|
||||||
return handleResponse(response, "获取渲染工作器详细信息失败");
|
|
||||||
},
|
|
||||||
RenderWorkerV2WithConfigDTO.class
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建工作器(直接调用,不降级)
|
* 创建工作器(直接调用,不降级)
|
||||||
*/
|
*/
|
||||||
@@ -96,18 +80,6 @@ public class RenderWorkerIntegrationService {
|
|||||||
return handleResponse(response, "查询渲染工作器列表失败");
|
return handleResponse(response, "查询渲染工作器列表失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 分页查询工作器列表(含配置信息)(不降级)
|
|
||||||
*/
|
|
||||||
public PageResponse<RenderWorkerV2WithConfigDTO> listWorkersWithConfig(Integer page, Integer pageSize,
|
|
||||||
Integer isEnabled, String name) {
|
|
||||||
log.debug("分页查询渲染工作器列表(含配置), page: {}, pageSize: {}, isEnabled: {}, name: {}",
|
|
||||||
page, pageSize, isEnabled, name);
|
|
||||||
CommonResponse<PageResponse<RenderWorkerV2WithConfigDTO>> response =
|
|
||||||
renderWorkerV2Client.listWorkersWithConfig(page, pageSize, isEnabled, name);
|
|
||||||
return handleResponse(response, "查询渲染工作器列表(含配置)失败");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据key获取工作器核心信息(带降级)
|
* 根据key获取工作器核心信息(带降级)
|
||||||
*/
|
*/
|
||||||
@@ -124,22 +96,6 @@ public class RenderWorkerIntegrationService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据key获取工作器详细信息(含配置)(带降级)
|
|
||||||
*/
|
|
||||||
public RenderWorkerV2WithConfigDTO getWorkerWithConfigByKey(String key) {
|
|
||||||
log.debug("根据key获取渲染工作器详细信息, key: {}", key);
|
|
||||||
return fallbackService.executeWithFallback(
|
|
||||||
SERVICE_NAME,
|
|
||||||
"worker:key:config:" + key,
|
|
||||||
() -> {
|
|
||||||
CommonResponse<RenderWorkerV2WithConfigDTO> response = renderWorkerV2Client.getWorkerWithConfigByKey(key);
|
|
||||||
return handleResponse(response, "根据key获取渲染工作器详细信息失败");
|
|
||||||
},
|
|
||||||
RenderWorkerV2WithConfigDTO.class
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理通用响应
|
* 处理通用响应
|
||||||
*/
|
*/
|
||||||
|
@@ -18,9 +18,6 @@ public interface ScenicConfigV2Client {
|
|||||||
CommonResponse<ScenicConfigV2DTO> getConfigByKey(@PathVariable("scenicId") Long scenicId,
|
CommonResponse<ScenicConfigV2DTO> getConfigByKey(@PathVariable("scenicId") Long scenicId,
|
||||||
@PathVariable("configKey") String configKey);
|
@PathVariable("configKey") String configKey);
|
||||||
|
|
||||||
@GetMapping("/{scenicId}/keys")
|
|
||||||
CommonResponse<Map<String, Object>> getFlatConfigs(@PathVariable("scenicId") Long scenicId);
|
|
||||||
|
|
||||||
@PostMapping("/{scenicId}")
|
@PostMapping("/{scenicId}")
|
||||||
CommonResponse<ScenicConfigV2DTO> createConfig(@PathVariable("scenicId") Long scenicId,
|
CommonResponse<ScenicConfigV2DTO> createConfig(@PathVariable("scenicId") Long scenicId,
|
||||||
@RequestBody CreateConfigRequest request);
|
@RequestBody CreateConfigRequest request);
|
||||||
@@ -37,8 +34,4 @@ public interface ScenicConfigV2Client {
|
|||||||
@PostMapping("/{scenicId}/batch")
|
@PostMapping("/{scenicId}/batch")
|
||||||
CommonResponse<BatchUpdateResponse> batchUpdateConfigs(@PathVariable("scenicId") Long scenicId,
|
CommonResponse<BatchUpdateResponse> batchUpdateConfigs(@PathVariable("scenicId") Long scenicId,
|
||||||
@RequestBody BatchConfigRequest request);
|
@RequestBody BatchConfigRequest request);
|
||||||
|
|
||||||
@PostMapping("/{scenicId}/batchFlatUpdate")
|
|
||||||
CommonResponse<BatchUpdateResponse> batchFlatUpdateConfigs(@PathVariable("scenicId") Long scenicId,
|
|
||||||
@RequestBody Map<String, Object> configs);
|
|
||||||
}
|
}
|
@@ -5,7 +5,6 @@ import com.ycwl.basic.integration.scenic.dto.filter.ScenicFilterPageResponse;
|
|||||||
import com.ycwl.basic.integration.scenic.dto.filter.ScenicFilterRequest;
|
import com.ycwl.basic.integration.scenic.dto.filter.ScenicFilterRequest;
|
||||||
import com.ycwl.basic.integration.scenic.dto.scenic.CreateScenicRequest;
|
import com.ycwl.basic.integration.scenic.dto.scenic.CreateScenicRequest;
|
||||||
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
|
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
|
||||||
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2WithConfigDTO;
|
|
||||||
import com.ycwl.basic.integration.common.response.PageResponse;
|
import com.ycwl.basic.integration.common.response.PageResponse;
|
||||||
import com.ycwl.basic.integration.scenic.dto.scenic.UpdateScenicRequest;
|
import com.ycwl.basic.integration.scenic.dto.scenic.UpdateScenicRequest;
|
||||||
import org.springframework.cloud.openfeign.FeignClient;
|
import org.springframework.cloud.openfeign.FeignClient;
|
||||||
@@ -19,10 +18,6 @@ public interface ScenicV2Client {
|
|||||||
@GetMapping("/{scenicId}")
|
@GetMapping("/{scenicId}")
|
||||||
CommonResponse<ScenicV2DTO> getScenic(@PathVariable("scenicId") Long scenicId);
|
CommonResponse<ScenicV2DTO> getScenic(@PathVariable("scenicId") Long scenicId);
|
||||||
|
|
||||||
@GetMapping("/{scenicId}/with-config")
|
|
||||||
CommonResponse<ScenicV2WithConfigDTO> getScenicWithConfig(@PathVariable("scenicId") Long scenicId);
|
|
||||||
|
|
||||||
|
|
||||||
@PostMapping("/")
|
@PostMapping("/")
|
||||||
CommonResponse<ScenicV2DTO> createScenic(@RequestBody CreateScenicRequest request);
|
CommonResponse<ScenicV2DTO> createScenic(@RequestBody CreateScenicRequest request);
|
||||||
|
|
||||||
@@ -41,10 +36,4 @@ public interface ScenicV2Client {
|
|||||||
@RequestParam(defaultValue = "10") Integer pageSize,
|
@RequestParam(defaultValue = "10") Integer pageSize,
|
||||||
@RequestParam(required = false) Integer status,
|
@RequestParam(required = false) Integer status,
|
||||||
@RequestParam(required = false) String name);
|
@RequestParam(required = false) String name);
|
||||||
|
|
||||||
@GetMapping("/with-config")
|
|
||||||
CommonResponse<PageResponse<ScenicV2WithConfigDTO>> listScenicsWithConfig(@RequestParam(defaultValue = "1") Integer page,
|
|
||||||
@RequestParam(defaultValue = "10") Integer pageSize,
|
|
||||||
@RequestParam(required = false) Integer status,
|
|
||||||
@RequestParam(required = false) String name);
|
|
||||||
}
|
}
|
@@ -1,14 +0,0 @@
|
|||||||
package com.ycwl.basic.integration.scenic.dto.scenic;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@EqualsAndHashCode(callSuper = true)
|
|
||||||
public class ScenicV2WithConfigDTO extends ScenicV2DTO {
|
|
||||||
@JsonProperty("config")
|
|
||||||
private Map<String, Object> config;
|
|
||||||
}
|
|
@@ -1,184 +0,0 @@
|
|||||||
package com.ycwl.basic.integration.scenic.example;
|
|
||||||
|
|
||||||
import com.ycwl.basic.integration.common.service.IntegrationFallbackService;
|
|
||||||
import com.ycwl.basic.integration.scenic.dto.config.*;
|
|
||||||
import com.ycwl.basic.integration.scenic.dto.filter.*;
|
|
||||||
import com.ycwl.basic.integration.scenic.dto.scenic.*;
|
|
||||||
import com.ycwl.basic.integration.scenic.service.ScenicConfigIntegrationService;
|
|
||||||
import com.ycwl.basic.integration.scenic.service.ScenicIntegrationService;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 景区集成示例(包含降级机制)
|
|
||||||
* 演示景区集成和失败降级策略的使用
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class ScenicIntegrationExample {
|
|
||||||
|
|
||||||
private final ScenicIntegrationService scenicIntegrationService;
|
|
||||||
private final ScenicConfigIntegrationService scenicConfigIntegrationService;
|
|
||||||
private final IntegrationFallbackService fallbackService;
|
|
||||||
|
|
||||||
private static final String SERVICE_NAME = "zt-scenic";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 示例:创建景区并设置配置
|
|
||||||
*/
|
|
||||||
public void createScenicWithConfig() {
|
|
||||||
try {
|
|
||||||
// 1. 创建景区
|
|
||||||
CreateScenicRequest createRequest = new CreateScenicRequest();
|
|
||||||
createRequest.setName("测试景区");
|
|
||||||
createRequest.setMpId(1001);
|
|
||||||
|
|
||||||
var scenic = scenicIntegrationService.createScenic(createRequest);
|
|
||||||
log.info("创建景区成功: {}", scenic.getName());
|
|
||||||
|
|
||||||
// 2. 为景区添加配置
|
|
||||||
CreateConfigRequest configRequest = new CreateConfigRequest();
|
|
||||||
configRequest.setConfigKey("tour_time");
|
|
||||||
configRequest.setConfigValue("120");
|
|
||||||
configRequest.setConfigType("int");
|
|
||||||
configRequest.setDescription("游览时长");
|
|
||||||
|
|
||||||
var config = scenicConfigIntegrationService.createConfig(
|
|
||||||
Long.valueOf(scenic.getId()), configRequest);
|
|
||||||
log.info("创建配置成功: {} = {}", config.getConfigKey(), config.getConfigValue());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("创建景区和配置失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 示例:筛选景区
|
|
||||||
*/
|
|
||||||
public void filterScenics() {
|
|
||||||
try {
|
|
||||||
FilterCondition condition = new FilterCondition();
|
|
||||||
condition.setConfigKey("tour_time");
|
|
||||||
condition.setConfigValue("120");
|
|
||||||
condition.setOperator("gte");
|
|
||||||
|
|
||||||
ScenicFilterRequest filterRequest = new ScenicFilterRequest();
|
|
||||||
filterRequest.setFilters(Collections.singletonList(condition));
|
|
||||||
filterRequest.setPage(1);
|
|
||||||
filterRequest.setPageSize(10);
|
|
||||||
|
|
||||||
var result = scenicIntegrationService.filterScenics(filterRequest);
|
|
||||||
log.info("筛选到 {} 个景区", result.getTotal());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("筛选景区失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 演示基础景区操作的降级机制
|
|
||||||
*/
|
|
||||||
public void basicScenicOperationsExample() {
|
|
||||||
log.info("=== 基础景区操作示例(含降级机制) ===");
|
|
||||||
|
|
||||||
Long scenicId = 2001L;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 获取景区信息 - 自动降级
|
|
||||||
ScenicV2DTO scenic = scenicIntegrationService.getScenic(scenicId);
|
|
||||||
log.info("获取景区成功: {}", scenic.getName());
|
|
||||||
|
|
||||||
// 获取景区配置信息 - 自动降级
|
|
||||||
ScenicV2WithConfigDTO scenicWithConfig = scenicIntegrationService.getScenicWithConfig(scenicId);
|
|
||||||
log.info("获取景区配置成功,配置数量: {}", scenicWithConfig.getConfig().size());
|
|
||||||
|
|
||||||
// 获取扁平化配置 - 自动降级
|
|
||||||
Map<String, Object> flatConfig = scenicIntegrationService.getScenicFlatConfig(scenicId);
|
|
||||||
log.info("获取扁平化配置成功,配置项数量: {}", flatConfig.size());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("景区操作降级失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 演示景区配置管理的降级机制
|
|
||||||
*/
|
|
||||||
public void scenicConfigManagementFallbackExample() {
|
|
||||||
log.info("=== 景区配置管理示例(含降级机制) ===");
|
|
||||||
|
|
||||||
Long scenicId = 2001L;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 获取扁平化配置 - 自动降级
|
|
||||||
Map<String, Object> flatConfigs = scenicConfigIntegrationService.getFlatConfigs(scenicId);
|
|
||||||
log.info("获取扁平化配置成功,配置项数量: {}", flatConfigs.size());
|
|
||||||
|
|
||||||
// 批量更新配置 - 直接操作,失败时抛出异常
|
|
||||||
Map<String, Object> updates = new HashMap<>();
|
|
||||||
updates.put("max_visitors", "5000");
|
|
||||||
updates.put("opening_hours", "08:00-18:00");
|
|
||||||
|
|
||||||
BatchUpdateResponse result = scenicConfigIntegrationService.batchFlatUpdateConfigs(scenicId, updates);
|
|
||||||
log.info("批量更新配置完成: 成功 {}, 失败 {}", result.getSuccess(), result.getFailed());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("景区配置管理操作失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 演示降级缓存管理
|
|
||||||
*/
|
|
||||||
public void fallbackCacheManagementExample() {
|
|
||||||
log.info("=== 景区降级缓存管理示例 ===");
|
|
||||||
|
|
||||||
String scenicCacheKey = "scenic:2001";
|
|
||||||
String configCacheKey = "scenic:flat:configs:2001";
|
|
||||||
|
|
||||||
// 检查降级缓存状态
|
|
||||||
boolean hasScenicCache = fallbackService.hasFallbackCache(SERVICE_NAME, scenicCacheKey);
|
|
||||||
boolean hasConfigCache = fallbackService.hasFallbackCache(SERVICE_NAME, configCacheKey);
|
|
||||||
|
|
||||||
log.info("景区降级缓存存在: {}", hasScenicCache);
|
|
||||||
log.info("配置降级缓存存在: {}", hasConfigCache);
|
|
||||||
|
|
||||||
// 获取降级缓存统计信息
|
|
||||||
IntegrationFallbackService.FallbackCacheStats stats = fallbackService.getFallbackCacheStats(SERVICE_NAME);
|
|
||||||
log.info("景区服务降级缓存统计: 缓存数量={}, TTL={}天",
|
|
||||||
stats.getTotalCacheCount(), stats.getFallbackTtlDays());
|
|
||||||
|
|
||||||
// 清理特定的降级缓存
|
|
||||||
if (hasScenicCache) {
|
|
||||||
fallbackService.clearFallbackCache(SERVICE_NAME, scenicCacheKey);
|
|
||||||
log.info("已清理景区降级缓存");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果缓存过多,批量清理
|
|
||||||
if (stats.getTotalCacheCount() > 50) {
|
|
||||||
fallbackService.clearAllFallbackCache(SERVICE_NAME);
|
|
||||||
log.info("已批量清理所有景区降级缓存");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 运行所有示例
|
|
||||||
*/
|
|
||||||
public void runAllExamples() {
|
|
||||||
log.info("开始运行景区集成示例(包含降级机制)...");
|
|
||||||
|
|
||||||
createScenicWithConfig();
|
|
||||||
filterScenics();
|
|
||||||
basicScenicOperationsExample();
|
|
||||||
scenicConfigManagementFallbackExample();
|
|
||||||
fallbackCacheManagementExample();
|
|
||||||
|
|
||||||
log.info("景区集成示例运行完成");
|
|
||||||
}
|
|
||||||
}
|
|
@@ -48,19 +48,6 @@ public class ScenicConfigIntegrationService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, Object> getFlatConfigs(Long scenicId) {
|
|
||||||
log.debug("获取景区扁平化配置, scenicId: {}", scenicId);
|
|
||||||
return fallbackService.executeWithFallback(
|
|
||||||
SERVICE_NAME,
|
|
||||||
"scenic:flat:configs:" + scenicId,
|
|
||||||
() -> {
|
|
||||||
CommonResponse<Map<String, Object>> response = scenicConfigV2Client.getFlatConfigs(scenicId);
|
|
||||||
return handleResponse(response, "获取景区扁平化配置失败");
|
|
||||||
},
|
|
||||||
Map.class
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ScenicConfigV2DTO createConfig(Long scenicId, CreateConfigRequest request) {
|
public ScenicConfigV2DTO createConfig(Long scenicId, CreateConfigRequest request) {
|
||||||
log.debug("创建景区配置, scenicId: {}, configKey: {}", scenicId, request.getConfigKey());
|
log.debug("创建景区配置, scenicId: {}, configKey: {}", scenicId, request.getConfigKey());
|
||||||
CommonResponse<ScenicConfigV2DTO> response = scenicConfigV2Client.createConfig(scenicId, request);
|
CommonResponse<ScenicConfigV2DTO> response = scenicConfigV2Client.createConfig(scenicId, request);
|
||||||
@@ -85,12 +72,6 @@ public class ScenicConfigIntegrationService {
|
|||||||
return handleResponse(response, "批量更新景区配置失败");
|
return handleResponse(response, "批量更新景区配置失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
public BatchUpdateResponse batchFlatUpdateConfigs(Long scenicId, Map<String, Object> configs) {
|
|
||||||
log.debug("扁平化批量更新景区配置, scenicId: {}, configs count: {}", scenicId, configs.size());
|
|
||||||
CommonResponse<BatchUpdateResponse> response = scenicConfigV2Client.batchFlatUpdateConfigs(scenicId, configs);
|
|
||||||
return handleResponse(response, "扁平化批量更新景区配置失败");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private <T> T handleResponse(CommonResponse<T> response, String errorMessage) {
|
private <T> T handleResponse(CommonResponse<T> response, String errorMessage) {
|
||||||
if (response == null || !response.isSuccess()) {
|
if (response == null || !response.isSuccess()) {
|
||||||
|
@@ -9,7 +9,6 @@ import com.ycwl.basic.integration.scenic.dto.filter.ScenicFilterPageResponse;
|
|||||||
import com.ycwl.basic.integration.scenic.dto.filter.ScenicFilterRequest;
|
import com.ycwl.basic.integration.scenic.dto.filter.ScenicFilterRequest;
|
||||||
import com.ycwl.basic.integration.scenic.dto.scenic.CreateScenicRequest;
|
import com.ycwl.basic.integration.scenic.dto.scenic.CreateScenicRequest;
|
||||||
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
|
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
|
||||||
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2WithConfigDTO;
|
|
||||||
import com.ycwl.basic.integration.common.response.PageResponse;
|
import com.ycwl.basic.integration.common.response.PageResponse;
|
||||||
import com.ycwl.basic.integration.scenic.dto.scenic.UpdateScenicRequest;
|
import com.ycwl.basic.integration.scenic.dto.scenic.UpdateScenicRequest;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -42,32 +41,6 @@ public class ScenicIntegrationService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ScenicV2WithConfigDTO getScenicWithConfig(Long scenicId) {
|
|
||||||
log.debug("获取景区配置信息, scenicId: {}", scenicId);
|
|
||||||
return fallbackService.executeWithFallback(
|
|
||||||
SERVICE_NAME,
|
|
||||||
"scenic:config:" + scenicId,
|
|
||||||
() -> {
|
|
||||||
CommonResponse<ScenicV2WithConfigDTO> response = scenicV2Client.getScenicWithConfig(scenicId);
|
|
||||||
return handleResponse(response, "获取景区配置信息失败");
|
|
||||||
},
|
|
||||||
ScenicV2WithConfigDTO.class
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<String, Object> getScenicFlatConfig(Long scenicId) {
|
|
||||||
log.debug("获取景区扁平化配置, scenicId: {}", scenicId);
|
|
||||||
return fallbackService.executeWithFallback(
|
|
||||||
SERVICE_NAME,
|
|
||||||
"scenic:flat:config:" + scenicId,
|
|
||||||
() -> {
|
|
||||||
CommonResponse<Map<String, Object>> response = scenicConfigV2Client.getFlatConfigs(scenicId);
|
|
||||||
return handleResponse(response, "获取景区扁平化配置失败");
|
|
||||||
},
|
|
||||||
Map.class
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ScenicV2DTO createScenic(CreateScenicRequest request) {
|
public ScenicV2DTO createScenic(CreateScenicRequest request) {
|
||||||
log.debug("创建景区, name: {}", request.getName());
|
log.debug("创建景区, name: {}", request.getName());
|
||||||
CommonResponse<ScenicV2DTO> response = scenicV2Client.createScenic(request);
|
CommonResponse<ScenicV2DTO> response = scenicV2Client.createScenic(request);
|
||||||
@@ -98,12 +71,6 @@ public class ScenicIntegrationService {
|
|||||||
return handleResponse(response, "分页查询景区列表失败");
|
return handleResponse(response, "分页查询景区列表失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
public PageResponse<ScenicV2WithConfigDTO> listScenicsWithConfig(Integer page, Integer pageSize, Integer status, String name) {
|
|
||||||
log.debug("分页查询景区带配置列表, page: {}, pageSize: {}, status: {}, name: {}", page, pageSize, status, name);
|
|
||||||
CommonResponse<PageResponse<ScenicV2WithConfigDTO>> response = scenicV2Client.listScenicsWithConfig(page, pageSize, status, name);
|
|
||||||
return handleResponse(response, "分页查询景区带配置列表失败");
|
|
||||||
}
|
|
||||||
|
|
||||||
private <T> T handleResponse(CommonResponse<T> response, String errorMessage) {
|
private <T> T handleResponse(CommonResponse<T> response, String errorMessage) {
|
||||||
if (response == null || !response.isSuccess()) {
|
if (response == null || !response.isSuccess()) {
|
||||||
String msg = response != null && response.getMessage() != null
|
String msg = response != null && response.getMessage() != null
|
||||||
|
@@ -29,4 +29,6 @@ public interface FaceSampleMapper {
|
|||||||
List<FaceSampleEntity> listEntityBeforeDate(Long scenicId, Date endDate);
|
List<FaceSampleEntity> listEntityBeforeDate(Long scenicId, Date endDate);
|
||||||
|
|
||||||
void updateScore(Long id, Float score);
|
void updateScore(Long id, Float score);
|
||||||
|
|
||||||
|
void updateStatus(Long id, Integer status);
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
package com.ycwl.basic.mapper;
|
package com.ycwl.basic.mapper;
|
||||||
|
|
||||||
|
import com.ycwl.basic.model.pc.printer.entity.MemberPrintEntity;
|
||||||
import com.ycwl.basic.model.pc.printer.entity.PrintTaskEntity;
|
import com.ycwl.basic.model.pc.printer.entity.PrintTaskEntity;
|
||||||
import com.ycwl.basic.model.pc.printer.entity.PrinterEntity;
|
import com.ycwl.basic.model.pc.printer.entity.PrinterEntity;
|
||||||
import com.ycwl.basic.model.pc.printer.resp.MemberPrintResp;
|
import com.ycwl.basic.model.pc.printer.resp.MemberPrintResp;
|
||||||
@@ -35,7 +36,7 @@ public interface PrinterMapper {
|
|||||||
|
|
||||||
int deleteUserPhoto(Long memberId, Long scenicId, Long relationId);
|
int deleteUserPhoto(Long memberId, Long scenicId, Long relationId);
|
||||||
|
|
||||||
int addUserPhoto(Long memberId, Long scenicId, String url);
|
int addUserPhoto(MemberPrintEntity entity);
|
||||||
|
|
||||||
MemberPrintResp getUserPhoto(Long memberId, Long scenicId, Long id);
|
MemberPrintResp getUserPhoto(Long memberId, Long scenicId, Long id);
|
||||||
|
|
||||||
|
@@ -56,6 +56,12 @@ public interface SourceMapper {
|
|||||||
|
|
||||||
int addRelations(List<MemberSourceEntity> list);
|
int addRelations(List<MemberSourceEntity> list);
|
||||||
|
|
||||||
|
List<MemberSourceEntity> filterExistingRelations(List<MemberSourceEntity> list);
|
||||||
|
|
||||||
|
boolean sourceExists(Long sourceId);
|
||||||
|
|
||||||
|
List<MemberSourceEntity> filterValidSourceRelations(List<MemberSourceEntity> list);
|
||||||
|
|
||||||
int updateRelation(MemberSourceEntity memberSourceEntity);
|
int updateRelation(MemberSourceEntity memberSourceEntity);
|
||||||
int freeRelations(List<Long> ids, int type);
|
int freeRelations(List<Long> ids, int type);
|
||||||
|
|
||||||
@@ -69,7 +75,7 @@ public interface SourceMapper {
|
|||||||
List<SourceEntity> listVideoByFaceRelation(Long memberId, Long faceId);
|
List<SourceEntity> listVideoByFaceRelation(Long memberId, Long faceId);
|
||||||
|
|
||||||
List<SourceEntity> listImageByFaceRelation(Long memberId, Long faceId);
|
List<SourceEntity> listImageByFaceRelation(Long memberId, Long faceId);
|
||||||
List<MemberSourceEntity> listByFaceRelation(Long memberId, Long faceId, Integer type);
|
List<MemberSourceEntity> listByFaceRelation(Long faceId, Integer type);
|
||||||
|
|
||||||
SourceEntity getEntity(Long id);
|
SourceEntity getEntity(Long id);
|
||||||
|
|
||||||
@@ -92,4 +98,11 @@ public interface SourceMapper {
|
|||||||
* @return type=2的source列表
|
* @return type=2的source列表
|
||||||
*/
|
*/
|
||||||
List<SourceEntity> listImageSourcesByFaceId(Long faceId);
|
List<SourceEntity> listImageSourcesByFaceId(Long faceId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从ZT-Source消息添加素材
|
||||||
|
* @param source 素材实体
|
||||||
|
* @return 影响行数
|
||||||
|
*/
|
||||||
|
int addFromZTSource(SourceEntity source);
|
||||||
}
|
}
|
||||||
|
@@ -38,7 +38,7 @@ public interface VideoMapper {
|
|||||||
|
|
||||||
MemberVideoEntity queryRelationByMemberTask(Long userId, Long taskId);
|
MemberVideoEntity queryRelationByMemberTask(Long userId, Long taskId);
|
||||||
List<MemberVideoEntity> listRelationByTask(Long taskId);
|
List<MemberVideoEntity> listRelationByTask(Long taskId);
|
||||||
List<MemberVideoEntity> listRelationByFace(Long userId, Long faceId);
|
List<MemberVideoEntity> listRelationByFace(Long faceId);
|
||||||
List<MemberVideoEntity> listRelationByFaceAndTemplate(Long faceId, Long templateId);
|
List<MemberVideoEntity> listRelationByFaceAndTemplate(Long faceId, Long templateId);
|
||||||
|
|
||||||
List<TaskEntity> listTaskByScenicRelation(Long userId, Long scenicId);
|
List<TaskEntity> listTaskByScenicRelation(Long userId, Long scenicId);
|
||||||
|
@@ -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;
|
||||||
|
@@ -51,18 +51,18 @@ public class AppStatisticsFunnelVO {
|
|||||||
return "-"; // TODO: REAL
|
return "-"; // TODO: REAL
|
||||||
}
|
}
|
||||||
|
|
||||||
// 扫码访问人数_上传头像人数_转化率
|
// 扫码访问人数_推送订阅人数_转化率
|
||||||
@JsonProperty("scaom_ufom")
|
@JsonProperty("scaom_ufom")
|
||||||
public String getScaom_ufom() {
|
public String getScaom_ufom() {
|
||||||
if (uploadFaceOfMemberNum == 0 || scanCodeVisitorOfMemberNum == 0) {
|
if (scanCodeVisitorOfMemberNum == 0 || pushOfMemberNum == 0) {
|
||||||
return "0.00";
|
return "0.00";
|
||||||
}
|
}
|
||||||
return new BigDecimal(uploadFaceOfMemberNum)
|
return new BigDecimal(pushOfMemberNum)
|
||||||
.multiply(new BigDecimal(100))
|
.multiply(new BigDecimal(100))
|
||||||
.divide(new BigDecimal(scanCodeVisitorOfMemberNum), 2, RoundingMode.HALF_UP)
|
.divide(new BigDecimal(scanCodeVisitorOfMemberNum), 2, RoundingMode.HALF_UP)
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
// 上传头像人数_推送订阅人数_转化率
|
// 推送订阅人数_上传头像人数_转化率
|
||||||
@JsonProperty("ufom_pom")
|
@JsonProperty("ufom_pom")
|
||||||
public String getUfom_pom() {
|
public String getUfom_pom() {
|
||||||
if (pushOfMemberNum == 0 || uploadFaceOfMemberNum == 0) {
|
if (pushOfMemberNum == 0 || uploadFaceOfMemberNum == 0) {
|
||||||
@@ -76,12 +76,12 @@ public class AppStatisticsFunnelVO {
|
|||||||
// 上传头像人数_生成视频人数_转化率
|
// 上传头像人数_生成视频人数_转化率
|
||||||
@JsonProperty("pom_cvom")
|
@JsonProperty("pom_cvom")
|
||||||
public String getPom_cvom() {
|
public String getPom_cvom() {
|
||||||
if (uploadFaceOfMemberNum == 0 || pushOfMemberNum == 0) {
|
if (uploadFaceOfMemberNum == 0 || completeVideoOfMemberNum == 0) {
|
||||||
return "0.00";
|
return "0.00";
|
||||||
}
|
}
|
||||||
return new BigDecimal(completeVideoOfMemberNum)
|
return new BigDecimal(completeVideoOfMemberNum)
|
||||||
.multiply(new BigDecimal(100))
|
.multiply(new BigDecimal(100))
|
||||||
.divide(new BigDecimal(pushOfMemberNum), 2, RoundingMode.HALF_UP)
|
.divide(new BigDecimal(uploadFaceOfMemberNum), 2, RoundingMode.HALF_UP)
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
// 生成视频人数_预览视频人数_转化率
|
// 生成视频人数_预览视频人数_转化率
|
||||||
|
@@ -10,4 +10,5 @@ import lombok.NoArgsConstructor;
|
|||||||
public class GoodsListRespVO {
|
public class GoodsListRespVO {
|
||||||
private Long goodsId;
|
private Long goodsId;
|
||||||
private String goodsName;
|
private String goodsName;
|
||||||
|
private Integer goodsType;
|
||||||
}
|
}
|
||||||
|
@@ -1,16 +1,7 @@
|
|||||||
package com.ycwl.basic.model.pc.scenic.resp;
|
package com.ycwl.basic.model.pc.scenic.resp;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
|
||||||
import com.ycwl.basic.facebody.enums.FaceBodyAdapterType;
|
|
||||||
import com.ycwl.basic.pay.enums.PayAdapterType;
|
|
||||||
import com.ycwl.basic.storage.enums.StorageType;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Author:longbinbin
|
* @Author:longbinbin
|
||||||
* @Date:2024/12/2 10:53
|
* @Date:2024/12/2 10:53
|
||||||
@@ -19,34 +10,49 @@ import java.util.Date;
|
|||||||
@Data
|
@Data
|
||||||
public class ScenicConfigResp {
|
public class ScenicConfigResp {
|
||||||
|
|
||||||
|
// ========== 基础配置 ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 预约流程,1-预约,2-在线,3-全部
|
* 水印URL
|
||||||
*/
|
*/
|
||||||
private Integer bookRoutine;
|
private String watermarkUrl;
|
||||||
private Integer forceFinishTime;
|
|
||||||
private Integer tourTime;
|
|
||||||
/**
|
/**
|
||||||
* 样本保存时间
|
* 视频存储天数
|
||||||
*/
|
|
||||||
private Integer sampleStoreDay;
|
|
||||||
private Integer faceStoreDay;
|
|
||||||
/**
|
|
||||||
* 视频保存时间
|
|
||||||
*/
|
*/
|
||||||
private Integer videoStoreDay;
|
private Integer videoStoreDay;
|
||||||
private Boolean allFree;
|
|
||||||
private Boolean disableSourceVideo;
|
|
||||||
private Boolean disableSourceImage;
|
|
||||||
private Integer antiScreenRecordType;
|
|
||||||
private Integer videoSourceStoreDay;
|
|
||||||
private Integer imageSourceStoreDay;
|
|
||||||
private Integer userSourceExpireDay;
|
|
||||||
private BigDecimal brokerDirectRate;
|
|
||||||
|
|
||||||
private String imageSourcePackHint = "";
|
/**
|
||||||
private String videoSourcePackHint = "";
|
* 防录屏类型配置
|
||||||
private Boolean voucherEnable;
|
*/
|
||||||
private Boolean enableVoucher;
|
private Integer antiScreenRecordType;
|
||||||
|
|
||||||
|
// ========== 功能开关 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分组功能开关
|
||||||
|
*/
|
||||||
private Boolean groupingEnable;
|
private Boolean groupingEnable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优惠券功能开关
|
||||||
|
*/
|
||||||
|
private Boolean voucherEnable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 等待时显示照片开关
|
||||||
|
*/
|
||||||
private Boolean showPhotoWhenWaiting;
|
private Boolean showPhotoWhenWaiting;
|
||||||
|
|
||||||
|
// ========== 提示文案 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片素材包提示文案
|
||||||
|
*/
|
||||||
|
private String imageSourcePackHint = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频素材包提示文案
|
||||||
|
*/
|
||||||
|
private String videoSourcePackHint = "";
|
||||||
}
|
}
|
||||||
|
@@ -14,4 +14,5 @@ public class TemplateConfigEntity {
|
|||||||
private Date createDate;
|
private Date createDate;
|
||||||
private Integer minimalPlaceholderFill;
|
private Integer minimalPlaceholderFill;
|
||||||
private Integer automaticPlaceholderFill;
|
private Integer automaticPlaceholderFill;
|
||||||
|
private Integer duplicateEnable;
|
||||||
}
|
}
|
||||||
|
@@ -25,6 +25,7 @@ public class TemplateRespVO {
|
|||||||
*/
|
*/
|
||||||
// 模版名称
|
// 模版名称
|
||||||
private String name;
|
private String name;
|
||||||
|
private String group;
|
||||||
/**
|
/**
|
||||||
* 父模版ID
|
* 父模版ID
|
||||||
*/
|
*/
|
||||||
|
@@ -1,51 +0,0 @@
|
|||||||
package com.ycwl.basic.notify;
|
|
||||||
|
|
||||||
import com.ycwl.basic.notify.adapters.INotifyAdapter;
|
|
||||||
import com.ycwl.basic.notify.adapters.ServerChanNotifyAdapter;
|
|
||||||
import com.ycwl.basic.notify.adapters.WxMpSrvNotifyAdapter;
|
|
||||||
import com.ycwl.basic.notify.enums.NotifyType;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class NotifyFactory {
|
|
||||||
public static INotifyAdapter get(NotifyType type) {
|
|
||||||
return switch (type) {
|
|
||||||
case SERVER_CHAN -> new ServerChanNotifyAdapter();
|
|
||||||
case WX_MP_SRV -> new WxMpSrvNotifyAdapter();
|
|
||||||
default -> throw new RuntimeException("不支持的通知类型");
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static INotifyAdapter get(NotifyType type, Map<String, String> config) {
|
|
||||||
INotifyAdapter adapter = get(type);
|
|
||||||
adapter.loadConfig(config);
|
|
||||||
return adapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static Map<String, INotifyAdapter> namedNotifier = new HashMap<>();
|
|
||||||
protected static INotifyAdapter defaultNotifier = null;
|
|
||||||
|
|
||||||
public static void register(String name, INotifyAdapter adapter) {
|
|
||||||
namedNotifier.put(name, adapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static INotifyAdapter via(String name) {
|
|
||||||
INotifyAdapter adapter = namedNotifier.get(name);
|
|
||||||
if (adapter == null) {
|
|
||||||
throw new RuntimeException("未定义的通知方式:"+name);
|
|
||||||
}
|
|
||||||
return adapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static INotifyAdapter via() {
|
|
||||||
if (defaultNotifier == null) {
|
|
||||||
throw new RuntimeException("未定义默认通知方式");
|
|
||||||
}
|
|
||||||
return defaultNotifier;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void setDefault(String defaultStorage) {
|
|
||||||
NotifyFactory.defaultNotifier = via(defaultStorage);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,11 +0,0 @@
|
|||||||
package com.ycwl.basic.notify.adapters;
|
|
||||||
|
|
||||||
import com.ycwl.basic.notify.entity.NotifyContent;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public interface INotifyAdapter {
|
|
||||||
void loadConfig(Map<String, String> _config);
|
|
||||||
|
|
||||||
void sendTo(NotifyContent notifyContent, String to);
|
|
||||||
}
|
|
@@ -1,54 +0,0 @@
|
|||||||
package com.ycwl.basic.notify.adapters;
|
|
||||||
|
|
||||||
import cn.hutool.http.HttpUtil;
|
|
||||||
import com.ycwl.basic.notify.entity.NotifyContent;
|
|
||||||
import com.ycwl.basic.notify.entity.ServerChanConfig;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
public class ServerChanNotifyAdapter implements INotifyAdapter {
|
|
||||||
ServerChanConfig config;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void loadConfig(Map<String, String> _config) {
|
|
||||||
ServerChanConfig config = new ServerChanConfig();
|
|
||||||
config.setKey(_config.get("key"));
|
|
||||||
config.checkEverythingOK();
|
|
||||||
this.config = config;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sendTo(NotifyContent notifyContent, String to) {
|
|
||||||
scSend(notifyContent.getTitle(), notifyContent.getContent(), config.getKey());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String scSend(String title, String content, String key) {
|
|
||||||
try {
|
|
||||||
String api;
|
|
||||||
|
|
||||||
// 判断 sendkey 是否以 "sctp" 开头,并提取数字部分拼接 URL
|
|
||||||
if (key.startsWith("sctp")) {
|
|
||||||
Pattern pattern = Pattern.compile("sctp(\\d+)t");
|
|
||||||
Matcher matcher = pattern.matcher(key);
|
|
||||||
if (matcher.find()) {
|
|
||||||
String num = matcher.group(1);
|
|
||||||
api = "https://" + num + ".push.ft07.com/send/" + key +".send";
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException("Invalid sendkey format for sctp");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
api = "https://sctapi.ftqq.com/" + key + ".send";
|
|
||||||
}
|
|
||||||
Map<String, Object> body = new HashMap<>();
|
|
||||||
body.put("title", title);
|
|
||||||
body.put("desp", content);
|
|
||||||
return HttpUtil.post(api, body);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,60 +0,0 @@
|
|||||||
package com.ycwl.basic.notify.adapters;
|
|
||||||
|
|
||||||
import cn.hutool.http.HttpUtil;
|
|
||||||
import com.ycwl.basic.utils.JacksonUtil;
|
|
||||||
import com.ycwl.basic.notify.entity.NotifyContent;
|
|
||||||
import com.ycwl.basic.notify.entity.WxMpSrvConfig;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class WxMpSrvNotifyAdapter implements INotifyAdapter{
|
|
||||||
private WxMpSrvConfig config;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void loadConfig(Map<String, String> _config) {
|
|
||||||
WxMpSrvConfig config = new WxMpSrvConfig();
|
|
||||||
config.setAppId(_config.get("appId"));
|
|
||||||
config.setAppSecret(_config.get("appSecret"));
|
|
||||||
if (_config.containsKey("state")) {
|
|
||||||
config.setState(_config.get("state"));
|
|
||||||
}
|
|
||||||
config.checkEverythingOK();
|
|
||||||
this.config = config;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sendTo(NotifyContent notifyContent, String openId) {
|
|
||||||
Map<String, Object> params = notifyContent.getParams();
|
|
||||||
params.put("touser", openId);
|
|
||||||
params.put("miniprogram_state", config.getState());
|
|
||||||
sendServiceNotification(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final String SEND_TEMPLATE_MESSAGE_URL = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=%s";
|
|
||||||
|
|
||||||
private static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
|
|
||||||
private String ACCESS_TOKEN = "";
|
|
||||||
private Date expireTime = new Date();
|
|
||||||
|
|
||||||
private String getAccessToken() {
|
|
||||||
if (ACCESS_TOKEN != null && !ACCESS_TOKEN.isEmpty()) {
|
|
||||||
if (expireTime.getTime() > System.currentTimeMillis()) {
|
|
||||||
return ACCESS_TOKEN;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String url = String.format(ACCESS_TOKEN_URL, config.getAppId(), config.getAppSecret());
|
|
||||||
String response = HttpUtil.get(url);
|
|
||||||
Map<String, Object> jsonObject = JacksonUtil.parseObject(response, Map.class);
|
|
||||||
ACCESS_TOKEN = (String) jsonObject.get("access_token");
|
|
||||||
Integer expiresIn = (Integer) jsonObject.get("expires_in");
|
|
||||||
expireTime = new Date(System.currentTimeMillis() + (expiresIn != null ? expiresIn : 7200) * 1000);
|
|
||||||
return ACCESS_TOKEN;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void sendServiceNotification(Map<String, Object> params) {
|
|
||||||
String url = String.format(SEND_TEMPLATE_MESSAGE_URL, getAccessToken());
|
|
||||||
String response = HttpUtil.post(url, JacksonUtil.toJSONString(params));
|
|
||||||
System.out.println(response);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,22 +0,0 @@
|
|||||||
package com.ycwl.basic.notify.entity;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class NotifyContent {
|
|
||||||
private String title;
|
|
||||||
private String content;
|
|
||||||
private Map<String, Object> params = new HashMap<>();
|
|
||||||
|
|
||||||
public NotifyContent(String title, String content) {
|
|
||||||
this.title = title;
|
|
||||||
this.content = content;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,11 +0,0 @@
|
|||||||
package com.ycwl.basic.notify.entity;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class ServerChanConfig {
|
|
||||||
private String key;
|
|
||||||
|
|
||||||
public void checkEverythingOK() {
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,15 +0,0 @@
|
|||||||
package com.ycwl.basic.notify.entity;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class WxMpSrvConfig {
|
|
||||||
private String appId;
|
|
||||||
private String appSecret;
|
|
||||||
private String state = "formal";
|
|
||||||
private String templateId;
|
|
||||||
|
|
||||||
public void checkEverythingOK() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,15 +0,0 @@
|
|||||||
package com.ycwl.basic.notify.enums;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
public enum NotifyType {
|
|
||||||
WX_MP_SRV("WX_MP_SRV"),
|
|
||||||
SERVER_CHAN("SERVER_CHAN");
|
|
||||||
|
|
||||||
private final String type;
|
|
||||||
|
|
||||||
NotifyType(String type) {
|
|
||||||
this.type = type;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,32 +0,0 @@
|
|||||||
package com.ycwl.basic.notify.starter;
|
|
||||||
|
|
||||||
import com.ycwl.basic.notify.NotifyFactory;
|
|
||||||
import com.ycwl.basic.notify.adapters.INotifyAdapter;
|
|
||||||
import com.ycwl.basic.notify.starter.config.NotifyConfigItem;
|
|
||||||
import com.ycwl.basic.notify.starter.config.OverallNotifyConfig;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
public class NotifyAutoConfigurator {
|
|
||||||
private final OverallNotifyConfig config;
|
|
||||||
public NotifyAutoConfigurator(OverallNotifyConfig config) {
|
|
||||||
this.config = config;
|
|
||||||
if (config != null) {
|
|
||||||
if (config.getConfigs() != null) {
|
|
||||||
loadConfig();
|
|
||||||
}
|
|
||||||
if (StringUtils.isNotBlank(config.getDefaultUse())) {
|
|
||||||
NotifyFactory.setDefault(config.getDefaultUse());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadConfig() {
|
|
||||||
for (NotifyConfigItem item : config.getConfigs()) {
|
|
||||||
INotifyAdapter adapter = NotifyFactory.get(item.getType());
|
|
||||||
adapter.loadConfig(item.getConfig());
|
|
||||||
NotifyFactory.register(item.getName(), adapter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,13 +0,0 @@
|
|||||||
package com.ycwl.basic.notify.starter.config;
|
|
||||||
|
|
||||||
import com.ycwl.basic.notify.enums.NotifyType;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class NotifyConfigItem {
|
|
||||||
private String name;
|
|
||||||
private NotifyType type;
|
|
||||||
private Map<String, String> config;
|
|
||||||
}
|
|
@@ -1,15 +0,0 @@
|
|||||||
package com.ycwl.basic.notify.starter.config;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
@ConfigurationProperties(prefix = "notify")
|
|
||||||
@Data
|
|
||||||
public class OverallNotifyConfig {
|
|
||||||
private String defaultUse;
|
|
||||||
private List<NotifyConfigItem> configs;
|
|
||||||
}
|
|
@@ -144,6 +144,17 @@ public enum ProductType {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 商品价格配置控制字段
|
||||||
|
`PriceProductConfig` 实体包含以下优惠控制字段:
|
||||||
|
- `canUseCoupon`: 是否可使用优惠券
|
||||||
|
- `canUseVoucher`: 是否可使用券码
|
||||||
|
- `canUseOnePrice`: 是否可使用一口价优惠(新增)
|
||||||
|
|
||||||
|
#### 一口价优惠控制机制
|
||||||
|
- 当购物车中任何商品的 `canUseOnePrice` 为 `false` 时,将跳过整个购物车的一口价优惠检测
|
||||||
|
- 配置优先级:具体商品配置 > 商品类型默认配置 > 系统默认(支持)
|
||||||
|
- 异常情况下默认支持一口价优惠,确保业务流程不受影响
|
||||||
|
|
||||||
#### 分层定价
|
#### 分层定价
|
||||||
支持基于数量的分层定价策略,通过 `PriceTierConfig` 配置不同数量区间的单价。
|
支持基于数量的分层定价策略,通过 `PriceTierConfig` 配置不同数量区间的单价。
|
||||||
|
|
||||||
@@ -339,6 +350,7 @@ public interface IDiscountDetectionService {
|
|||||||
#### OnePricePurchaseDiscountProvider (优先级: 120)
|
#### OnePricePurchaseDiscountProvider (优先级: 120)
|
||||||
- 处理一口价优惠逻辑(景区级统一价格)
|
- 处理一口价优惠逻辑(景区级统一价格)
|
||||||
- **最高优先级**,优先于所有其他优惠类型
|
- **最高优先级**,优先于所有其他优惠类型
|
||||||
|
- 商品级别控制:检查购物车中所有商品的 `canUseOnePrice` 配置,任一商品不支持则跳过检测
|
||||||
- 仅当一口价小于当前金额时产生优惠;是否可与券码/优惠券叠加由配置 `canUseCoupon/canUseVoucher` 决定
|
- 仅当一口价小于当前金额时产生优惠;是否可与券码/优惠券叠加由配置 `canUseCoupon/canUseVoucher` 决定
|
||||||
|
|
||||||
#### BundleDiscountProvider (优先级: 100)
|
#### BundleDiscountProvider (优先级: 100)
|
||||||
@@ -370,7 +382,7 @@ public interface IDiscountDetectionService {
|
|||||||
|
|
||||||
特殊情况:
|
特殊情况:
|
||||||
- 全场免费券码:直接最终价=0,停止后续优惠
|
- 全场免费券码:直接最终价=0,停止后续优惠
|
||||||
- 一口价:可叠加性由配置 canUseCoupon / canUseVoucher 控制
|
- 一口价:可叠加性由配置 canUseCoupon / canUseVoucher 控制;商品级别由 canUseOnePrice 控制参与检测
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 扩展支持
|
#### 扩展支持
|
||||||
@@ -535,7 +547,7 @@ public class PriceCalculationResult {
|
|||||||
## 数据库设计
|
## 数据库设计
|
||||||
|
|
||||||
### 核心表结构(摘)
|
### 核心表结构(摘)
|
||||||
- `price_product_config`: 商品价格基础配置
|
- `price_product_config`: 商品价格基础配置(包含 `can_use_coupon`、`can_use_voucher`、`can_use_one_price` 优惠控制字段)
|
||||||
- `price_tier_config`: 分层定价配置
|
- `price_tier_config`: 分层定价配置
|
||||||
- `price_bundle_config`: 套餐配置
|
- `price_bundle_config`: 套餐配置
|
||||||
- `price_coupon_config`: 优惠券配置
|
- `price_coupon_config`: 优惠券配置
|
||||||
|
@@ -69,6 +69,11 @@ public class PriceProductConfig {
|
|||||||
*/
|
*/
|
||||||
private Boolean canUseVoucher;
|
private Boolean canUseVoucher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否可使用一口价优惠
|
||||||
|
*/
|
||||||
|
private Boolean canUseOnePrice;
|
||||||
|
|
||||||
@TableField("create_time")
|
@TableField("create_time")
|
||||||
private Date createTime;
|
private Date createTime;
|
||||||
|
|
||||||
|
@@ -57,15 +57,15 @@ public interface PriceProductConfigMapper extends BaseMapper<PriceProductConfig>
|
|||||||
/**
|
/**
|
||||||
* 插入商品价格配置
|
* 插入商品价格配置
|
||||||
*/
|
*/
|
||||||
@Insert("INSERT INTO price_product_config (product_type, product_id, scenic_id, product_name, base_price, original_price, unit, is_active, can_use_coupon, can_use_voucher, create_time, update_time) " +
|
@Insert("INSERT INTO price_product_config (product_type, product_id, scenic_id, product_name, base_price, original_price, unit, is_active, can_use_coupon, can_use_voucher, can_use_one_price, create_time, update_time) " +
|
||||||
"VALUES (#{productType}, #{productId}, #{scenicId}, #{productName}, #{basePrice}, #{originalPrice}, #{unit}, #{isActive}, #{canUseCoupon}, #{canUseVoucher}, NOW(), NOW())")
|
"VALUES (#{productType}, #{productId}, #{scenicId}, #{productName}, #{basePrice}, #{originalPrice}, #{unit}, #{isActive}, #{canUseCoupon}, #{canUseVoucher}, #{canUseOnePrice}, NOW(), NOW())")
|
||||||
int insertProductConfig(PriceProductConfig config);
|
int insertProductConfig(PriceProductConfig config);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新商品价格配置
|
* 更新商品价格配置
|
||||||
*/
|
*/
|
||||||
@Update("UPDATE price_product_config SET product_id = #{productId}, scenic_id = #{scenicId}, product_name = #{productName}, base_price = #{basePrice}, " +
|
@Update("UPDATE price_product_config SET product_id = #{productId}, scenic_id = #{scenicId}, product_name = #{productName}, base_price = #{basePrice}, " +
|
||||||
"original_price = #{originalPrice}, unit = #{unit}, is_active = #{isActive}, can_use_coupon = #{canUseCoupon}, can_use_voucher = #{canUseVoucher}, update_time = NOW() WHERE id = #{id}")
|
"original_price = #{originalPrice}, unit = #{unit}, is_active = #{isActive}, can_use_coupon = #{canUseCoupon}, can_use_voucher = #{canUseVoucher}, can_use_one_price = #{canUseOnePrice}, update_time = NOW() WHERE id = #{id}")
|
||||||
int updateProductConfig(PriceProductConfig config);
|
int updateProductConfig(PriceProductConfig config);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -4,8 +4,11 @@ import com.ycwl.basic.pricing.dto.DiscountDetectionContext;
|
|||||||
import com.ycwl.basic.pricing.dto.DiscountInfo;
|
import com.ycwl.basic.pricing.dto.DiscountInfo;
|
||||||
import com.ycwl.basic.pricing.dto.DiscountResult;
|
import com.ycwl.basic.pricing.dto.DiscountResult;
|
||||||
import com.ycwl.basic.pricing.dto.OnePriceInfo;
|
import com.ycwl.basic.pricing.dto.OnePriceInfo;
|
||||||
|
import com.ycwl.basic.pricing.dto.ProductItem;
|
||||||
|
import com.ycwl.basic.pricing.entity.PriceProductConfig;
|
||||||
import com.ycwl.basic.pricing.service.IDiscountProvider;
|
import com.ycwl.basic.pricing.service.IDiscountProvider;
|
||||||
import com.ycwl.basic.pricing.service.IOnePricePurchaseService;
|
import com.ycwl.basic.pricing.service.IOnePricePurchaseService;
|
||||||
|
import com.ycwl.basic.pricing.service.IProductConfigService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
@@ -23,6 +26,7 @@ import java.util.List;
|
|||||||
public class OnePricePurchaseDiscountProvider implements IDiscountProvider {
|
public class OnePricePurchaseDiscountProvider implements IDiscountProvider {
|
||||||
|
|
||||||
private final IOnePricePurchaseService onePricePurchaseService;
|
private final IOnePricePurchaseService onePricePurchaseService;
|
||||||
|
private final IProductConfigService productConfigService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getProviderType() {
|
public String getProviderType() {
|
||||||
@@ -50,6 +54,12 @@ public class OnePricePurchaseDiscountProvider implements IDiscountProvider {
|
|||||||
return discounts;
|
return discounts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查商品是否支持一口价优惠
|
||||||
|
if (!areAllProductsSupportOnePrice(context.getProducts())) {
|
||||||
|
log.debug("存在不支持一口价优惠的商品,跳过一口价检测");
|
||||||
|
return discounts;
|
||||||
|
}
|
||||||
|
|
||||||
// 获取一口价信息
|
// 获取一口价信息
|
||||||
OnePriceInfo onePriceInfo = onePricePurchaseService.getOnePriceInfo(
|
OnePriceInfo onePriceInfo = onePricePurchaseService.getOnePriceInfo(
|
||||||
context.getScenicId(), context.getCurrentAmount());
|
context.getScenicId(), context.getCurrentAmount());
|
||||||
@@ -170,6 +180,54 @@ public class OnePricePurchaseDiscountProvider implements IDiscountProvider {
|
|||||||
return BigDecimal.ZERO;
|
return BigDecimal.ZERO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查购物车中的所有商品是否都支持一口价优惠
|
||||||
|
*/
|
||||||
|
private boolean areAllProductsSupportOnePrice(List<ProductItem> products) {
|
||||||
|
if (products == null || products.isEmpty()) {
|
||||||
|
return true; // 空购物车时默认支持
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ProductItem product : products) {
|
||||||
|
try {
|
||||||
|
// 查询商品配置
|
||||||
|
PriceProductConfig productConfig = productConfigService.getProductConfig(
|
||||||
|
product.getProductType().getCode(), product.getProductId());
|
||||||
|
|
||||||
|
if (productConfig != null) {
|
||||||
|
// 检查商品是否支持一口价优惠
|
||||||
|
if (Boolean.FALSE.equals(productConfig.getCanUseOnePrice())) {
|
||||||
|
log.debug("商品 {}({}) 不支持一口价优惠",
|
||||||
|
product.getProductType().getCode(), product.getProductId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果找不到具体商品配置,尝试查询 default 配置
|
||||||
|
PriceProductConfig defaultConfig = productConfigService.getProductConfig(
|
||||||
|
product.getProductType().getCode(), "default");
|
||||||
|
|
||||||
|
if (defaultConfig != null) {
|
||||||
|
if (Boolean.FALSE.equals(defaultConfig.getCanUseOnePrice())) {
|
||||||
|
log.debug("商品类型 {} 的默认配置不支持一口价优惠",
|
||||||
|
product.getProductType().getCode());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果既没有具体配置也没有默认配置,默认支持一口价优惠
|
||||||
|
log.debug("商品 {}({}) 未找到价格配置,默认支持一口价优惠",
|
||||||
|
product.getProductType().getCode(), product.getProductId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("检查商品 {}({}) 一口价优惠支持情况时发生异常,默认支持",
|
||||||
|
product.getProductType().getCode(), product.getProductId(), e);
|
||||||
|
// 异常情况下默认支持,避免影响正常业务流程
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查优惠叠加规则
|
* 检查优惠叠加规则
|
||||||
*/
|
*/
|
||||||
|
@@ -7,7 +7,9 @@ import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
|
|||||||
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
|
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
|
||||||
import com.ycwl.basic.utils.SnowFlakeUtil;
|
import com.ycwl.basic.utils.SnowFlakeUtil;
|
||||||
import com.ycwl.basic.integration.device.service.DeviceIntegrationService;
|
import com.ycwl.basic.integration.device.service.DeviceIntegrationService;
|
||||||
|
import com.ycwl.basic.integration.device.service.DeviceStatusIntegrationService;
|
||||||
import com.ycwl.basic.integration.device.dto.device.DeviceV2DTO;
|
import com.ycwl.basic.integration.device.dto.device.DeviceV2DTO;
|
||||||
|
import com.ycwl.basic.integration.device.dto.status.DeviceStatusDTO;
|
||||||
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
|
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
@@ -33,6 +35,8 @@ public class DeviceRepository {
|
|||||||
public static final String DEVICE_CACHE_KEY = "device:%s";
|
public static final String DEVICE_CACHE_KEY = "device:%s";
|
||||||
@Autowired
|
@Autowired
|
||||||
private DeviceConfigIntegrationService deviceConfigIntegrationService;
|
private DeviceConfigIntegrationService deviceConfigIntegrationService;
|
||||||
|
@Autowired
|
||||||
|
private DeviceStatusIntegrationService deviceStatusIntegrationService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将DeviceV2DTO转换为DeviceEntity
|
* 将DeviceV2DTO转换为DeviceEntity
|
||||||
@@ -57,6 +61,35 @@ public class DeviceRepository {
|
|||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将DeviceV2DTO和DeviceStatusDTO合并转换为DeviceEntity
|
||||||
|
*/
|
||||||
|
private DeviceEntity convertToEntityWithStatus(DeviceV2DTO deviceDto, DeviceStatusDTO statusDto) {
|
||||||
|
if (deviceDto == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceEntity entity = convertToEntity(deviceDto);
|
||||||
|
|
||||||
|
// 合并状态信息
|
||||||
|
if (statusDto != null) {
|
||||||
|
// Boolean转Integer: true→1, false→0
|
||||||
|
entity.setOnline(statusDto.getIsOnline() != null && statusDto.getIsOnline() ? 1 : 0);
|
||||||
|
// 添加空值检查,避免NullPointerException
|
||||||
|
if (statusDto.getLastActiveTime() != null) {
|
||||||
|
entity.setKeepaliveAt(statusDto.getLastActiveTime());
|
||||||
|
}
|
||||||
|
if (statusDto.getClientIP() != null) {
|
||||||
|
entity.setIpAddr(statusDto.getClientIP());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 默认离线状态
|
||||||
|
entity.setOnline(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
public DeviceEntity getDevice(Long deviceId) {
|
public DeviceEntity getDevice(Long deviceId) {
|
||||||
log.debug("获取设备信息, deviceId: {}", deviceId);
|
log.debug("获取设备信息, deviceId: {}", deviceId);
|
||||||
DeviceV2DTO deviceDto = deviceIntegrationService.getDevice(deviceId);
|
DeviceV2DTO deviceDto = deviceIntegrationService.getDevice(deviceId);
|
||||||
@@ -144,11 +177,32 @@ public class DeviceRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public DeviceEntity getOnlineStatus(Long deviceId) {
|
public DeviceEntity getOnlineStatus(Long deviceId) {
|
||||||
if (redisTemplate.hasKey(String.format(DEVICE_ONLINE_CACHE_KEY, deviceId))) {
|
log.debug("获取设备在线状态, deviceId: {}", deviceId);
|
||||||
return JacksonUtil.parseObject(redisTemplate.opsForValue().get(String.format(DEVICE_ONLINE_CACHE_KEY, deviceId)), DeviceEntity.class);
|
try {
|
||||||
} else {
|
// 首先获取设备基本信息
|
||||||
|
DeviceV2DTO deviceDto = deviceIntegrationService.getDevice(deviceId);
|
||||||
|
if (deviceDto == null) {
|
||||||
|
log.warn("设备不存在, deviceId: {}", deviceId);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 通过设备编号获取设备状态
|
||||||
|
DeviceStatusDTO statusDto = deviceStatusIntegrationService.getDeviceStatus(deviceDto.getNo());
|
||||||
|
|
||||||
|
// 合并设备信息和状态信息
|
||||||
|
return convertToEntityWithStatus(deviceDto, statusDto);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取设备在线状态异常, deviceId: {}", deviceId, e);
|
||||||
|
// 降级处理:尝试仅返回设备基本信息
|
||||||
|
try {
|
||||||
|
DeviceV2DTO deviceDto = deviceIntegrationService.getDevice(deviceId);
|
||||||
|
return convertToEntityWithStatus(deviceDto, null);
|
||||||
|
} catch (Exception fallbackException) {
|
||||||
|
log.error("降级获取设备信息也失败, deviceId: {}", deviceId, fallbackException);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateDeviceCache(DeviceEntity device) {
|
private void updateDeviceCache(DeviceEntity device) {
|
||||||
|
@@ -0,0 +1,113 @@
|
|||||||
|
package com.ycwl.basic.repository;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.ycwl.basic.mapper.SourceMapper;
|
||||||
|
import com.ycwl.basic.mapper.VideoMapper;
|
||||||
|
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
||||||
|
import com.ycwl.basic.model.pc.video.entity.MemberVideoEntity;
|
||||||
|
import com.ycwl.basic.utils.JacksonUtil;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class MemberRelationRepository {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RedisTemplate<String, String> redisTemplate;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private VideoMapper videoMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SourceMapper sourceMapper;
|
||||||
|
|
||||||
|
public static final String MEMBER_RELATION_BY_FACE_CACHE_KEY = "member_relation:face:%d:v";
|
||||||
|
public static final String MEMBER_RELATION_BY_FACE_TEMPLATE_CACHE_KEY = "member_relation:face:%d:v:template:%d";
|
||||||
|
public static final String MEMBER_SOURCE_BY_FACE_TYPE_CACHE_KEY = "member_relation:face:%d:s:type:%d";
|
||||||
|
|
||||||
|
public List<MemberVideoEntity> listRelationByFace(Long faceId) {
|
||||||
|
String cacheKey = String.format(MEMBER_RELATION_BY_FACE_CACHE_KEY, faceId);
|
||||||
|
|
||||||
|
if (redisTemplate.hasKey(cacheKey)) {
|
||||||
|
String cacheValue = redisTemplate.opsForValue().get(cacheKey);
|
||||||
|
return JacksonUtil.fromJson(cacheValue, new TypeReference<List<MemberVideoEntity>>() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MemberVideoEntity> result = videoMapper.listRelationByFace(faceId);
|
||||||
|
if (result != null) {
|
||||||
|
redisTemplate.opsForValue().set(cacheKey, JacksonUtil.toJson(result), 1, TimeUnit.HOURS);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<MemberVideoEntity> listRelationByFaceAndTemplate(Long faceId, Long templateId) {
|
||||||
|
String cacheKey = String.format(MEMBER_RELATION_BY_FACE_TEMPLATE_CACHE_KEY, faceId, templateId);
|
||||||
|
|
||||||
|
if (redisTemplate.hasKey(cacheKey)) {
|
||||||
|
String cacheValue = redisTemplate.opsForValue().get(cacheKey);
|
||||||
|
return JacksonUtil.fromJson(cacheValue, new TypeReference<List<MemberVideoEntity>>() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MemberVideoEntity> result = videoMapper.listRelationByFaceAndTemplate(faceId, templateId);
|
||||||
|
if (result != null) {
|
||||||
|
redisTemplate.opsForValue().set(cacheKey, JacksonUtil.toJson(result), 1, TimeUnit.HOURS);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<MemberSourceEntity> listSourceByFaceRelation(Long faceId, Integer type) {
|
||||||
|
String cacheKey = String.format(MEMBER_SOURCE_BY_FACE_TYPE_CACHE_KEY, faceId, type);
|
||||||
|
|
||||||
|
if (redisTemplate.hasKey(cacheKey)) {
|
||||||
|
String cacheValue = redisTemplate.opsForValue().get(cacheKey);
|
||||||
|
return JacksonUtil.fromJson(cacheValue, new TypeReference<List<MemberSourceEntity>>() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MemberSourceEntity> result = sourceMapper.listByFaceRelation(faceId, type);
|
||||||
|
if (result != null) {
|
||||||
|
redisTemplate.opsForValue().set(cacheKey, JacksonUtil.toJson(result), 1, TimeUnit.HOURS);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearCacheByFace(Long faceId) {
|
||||||
|
String pattern = "member_relation:face:" + faceId + ":*";
|
||||||
|
Set<String> keys = redisTemplate.keys(pattern);
|
||||||
|
if (keys != null && !keys.isEmpty()) {
|
||||||
|
redisTemplate.delete(keys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearVCacheByFace(Long faceId) {
|
||||||
|
String pattern = "member_relation:face:" + faceId + ":v:*";
|
||||||
|
Set<String> keys = redisTemplate.keys(pattern);
|
||||||
|
if (keys != null && !keys.isEmpty()) {
|
||||||
|
redisTemplate.delete(keys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearSCacheByFace(Long faceId) {
|
||||||
|
String pattern = "member_relation:face:" + faceId + ":s:*";
|
||||||
|
Set<String> keys = redisTemplate.keys(pattern);
|
||||||
|
if (keys != null && !keys.isEmpty()) {
|
||||||
|
redisTemplate.delete(keys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearAllCache() {
|
||||||
|
String pattern = "member_relation:*";
|
||||||
|
Set<String> keys = redisTemplate.keys(pattern);
|
||||||
|
if (keys != null && !keys.isEmpty()) {
|
||||||
|
redisTemplate.delete(keys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -98,6 +98,10 @@ public class OrderRepository {
|
|||||||
return checkUserBuyItem(userId, 0, videoId);
|
return checkUserBuyItem(userId, 0, videoId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean checkUserBuyTemplate(Long userId, Long templateId) {
|
||||||
|
return checkUserBuyItem(userId, -1, templateId);
|
||||||
|
}
|
||||||
|
|
||||||
public void clearUserBuyItemCache(Long userId, int goodsType, Long goodsId) {
|
public void clearUserBuyItemCache(Long userId, int goodsType, Long goodsId) {
|
||||||
redisTemplate.delete(String.format(ORDER_USER_TYPE_BUY_ITEM_CACHE_KEY, userId, goodsType, goodsId));
|
redisTemplate.delete(String.format(ORDER_USER_TYPE_BUY_ITEM_CACHE_KEY, userId, goodsType, goodsId));
|
||||||
}
|
}
|
||||||
|
@@ -4,7 +4,6 @@ import com.ycwl.basic.facebody.enums.FaceBodyAdapterType;
|
|||||||
import com.ycwl.basic.integration.common.util.ConfigValueUtil;
|
import com.ycwl.basic.integration.common.util.ConfigValueUtil;
|
||||||
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
|
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
|
||||||
import com.ycwl.basic.integration.common.response.PageResponse;
|
import com.ycwl.basic.integration.common.response.PageResponse;
|
||||||
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2WithConfigDTO;
|
|
||||||
import com.ycwl.basic.integration.scenic.service.ScenicIntegrationService;
|
import com.ycwl.basic.integration.scenic.service.ScenicIntegrationService;
|
||||||
import com.ycwl.basic.integration.scenic.service.ScenicConfigIntegrationService;
|
import com.ycwl.basic.integration.scenic.service.ScenicConfigIntegrationService;
|
||||||
import com.ycwl.basic.integration.scenic.dto.config.ScenicConfigV2DTO;
|
import com.ycwl.basic.integration.scenic.dto.config.ScenicConfigV2DTO;
|
||||||
@@ -52,8 +51,11 @@ public class ScenicRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ScenicEntity getScenic(Long id) {
|
public ScenicEntity getScenic(Long id) {
|
||||||
ScenicV2WithConfigDTO scenicDTO = scenicIntegrationService.getScenicWithConfig(id);
|
// 分别获取景区基础信息和配置信息
|
||||||
ScenicEntity scenicEntity = convertToScenicEntity(scenicDTO);
|
ScenicV2DTO scenicBasic = scenicIntegrationService.getScenic(id);
|
||||||
|
ScenicConfigManager configManager = getScenicConfigManager(id);
|
||||||
|
|
||||||
|
ScenicEntity scenicEntity = convertToScenicEntity(scenicBasic, configManager);
|
||||||
return scenicEntity;
|
return scenicEntity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,7 +222,7 @@ public class ScenicRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ScenicEntity convertToScenicEntity(ScenicV2WithConfigDTO dto) {
|
private ScenicEntity convertToScenicEntity(ScenicV2DTO dto, ScenicConfigManager configManager) {
|
||||||
if (dto == null) {
|
if (dto == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -229,19 +231,17 @@ public class ScenicRepository {
|
|||||||
entity.setName(dto.getName());
|
entity.setName(dto.getName());
|
||||||
entity.setMpId(dto.getMpId());
|
entity.setMpId(dto.getMpId());
|
||||||
entity.setStatus(dto.getStatus().toString());
|
entity.setStatus(dto.getStatus().toString());
|
||||||
if (dto.getConfig() != null) {
|
entity.setAddress(configManager.getString("address"));
|
||||||
entity.setAddress(ConfigValueUtil.getStringValue(dto.getConfig(), "address"));
|
entity.setArea(configManager.getString("area"));
|
||||||
entity.setArea(ConfigValueUtil.getStringValue(dto.getConfig(), "area"));
|
entity.setCity(configManager.getString("city"));
|
||||||
entity.setCity(ConfigValueUtil.getStringValue(dto.getConfig(), "city"));
|
entity.setProvince(configManager.getString("province"));
|
||||||
entity.setProvince(ConfigValueUtil.getStringValue(dto.getConfig(), "province"));
|
entity.setLatitude(configManager.getBigDecimal("latitude"));
|
||||||
entity.setLatitude(ConfigValueUtil.getBigDecimalValue(dto.getConfig(), "latitude"));
|
entity.setLongitude(configManager.getBigDecimal("longitude"));
|
||||||
entity.setLongitude(ConfigValueUtil.getBigDecimalValue(dto.getConfig(), "longitude"));
|
entity.setRadius(configManager.getBigDecimal("radius"));
|
||||||
entity.setRadius(ConfigValueUtil.getBigDecimalValue(dto.getConfig(), "radius"));
|
entity.setPhone(configManager.getString("phone"));
|
||||||
entity.setPhone(ConfigValueUtil.getStringValue(dto.getConfig(), "phone"));
|
entity.setLogoUrl(configManager.getString("logo_url"));
|
||||||
entity.setLogoUrl(ConfigValueUtil.getStringValue(dto.getConfig(), "logoUrl"));
|
entity.setCoverUrl(configManager.getString("cover_url"));
|
||||||
entity.setCoverUrl(ConfigValueUtil.getStringValue(dto.getConfig(), "coverUrl"));
|
entity.setKfCodeUrl(configManager.getString("kf_code_url"));
|
||||||
entity.setKfCodeUrl(ConfigValueUtil.getStringValue(dto.getConfig(), "kfCodeUrl"));
|
|
||||||
}
|
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -33,6 +33,8 @@ public class SourceRepository {
|
|||||||
private TemplateRepository templateRepository;
|
private TemplateRepository templateRepository;
|
||||||
@Autowired
|
@Autowired
|
||||||
private DeviceRepository deviceRepository;
|
private DeviceRepository deviceRepository;
|
||||||
|
@Autowired
|
||||||
|
private MemberRelationRepository memberRelationRepository;
|
||||||
|
|
||||||
public void addSource(SourceEntity source) {
|
public void addSource(SourceEntity source) {
|
||||||
sourceMapper.add(source);
|
sourceMapper.add(source);
|
||||||
@@ -46,6 +48,7 @@ public class SourceRepository {
|
|||||||
memberSource.setOrderId(orderId);
|
memberSource.setOrderId(orderId);
|
||||||
memberSource.setIsBuy(1);
|
memberSource.setIsBuy(1);
|
||||||
sourceMapper.updateRelation(memberSource);
|
sourceMapper.updateRelation(memberSource);
|
||||||
|
memberRelationRepository.clearSCacheByFace(faceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUserNotBuyItem(Long memberId, int type, Long faceId) {
|
public void setUserNotBuyItem(Long memberId, int type, Long faceId) {
|
||||||
@@ -56,6 +59,7 @@ public class SourceRepository {
|
|||||||
memberSource.setOrderId(null);
|
memberSource.setOrderId(null);
|
||||||
memberSource.setIsBuy(0);
|
memberSource.setIsBuy(0);
|
||||||
sourceMapper.updateRelation(memberSource);
|
sourceMapper.updateRelation(memberSource);
|
||||||
|
memberRelationRepository.clearSCacheByFace(faceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean getUserIsBuy(Long userId, int type, Long faceId) {
|
public boolean getUserIsBuy(Long userId, int type, Long faceId) {
|
||||||
|
@@ -2,8 +2,6 @@ package com.ycwl.basic.repository;
|
|||||||
|
|
||||||
import com.ycwl.basic.biz.PriceBiz;
|
import com.ycwl.basic.biz.PriceBiz;
|
||||||
import com.ycwl.basic.model.mobile.order.IsBuyBatchRespVO;
|
import com.ycwl.basic.model.mobile.order.IsBuyBatchRespVO;
|
||||||
import com.ycwl.basic.model.mobile.order.IsBuyRespVO;
|
|
||||||
import com.ycwl.basic.pricing.dto.DiscountDetectionContext;
|
|
||||||
import com.ycwl.basic.pricing.dto.VoucherInfo;
|
import com.ycwl.basic.pricing.dto.VoucherInfo;
|
||||||
import com.ycwl.basic.pricing.enums.VoucherDiscountType;
|
import com.ycwl.basic.pricing.enums.VoucherDiscountType;
|
||||||
import com.ycwl.basic.pricing.service.IVoucherService;
|
import com.ycwl.basic.pricing.service.IVoucherService;
|
||||||
@@ -36,6 +34,8 @@ public class VideoRepository {
|
|||||||
private PriceBiz priceBiz;
|
private PriceBiz priceBiz;
|
||||||
@Autowired
|
@Autowired
|
||||||
private IVoucherService iVoucherService;
|
private IVoucherService iVoucherService;
|
||||||
|
@Autowired
|
||||||
|
private MemberRelationRepository memberRelationRepository;
|
||||||
|
|
||||||
public VideoEntity getVideo(Long videoId) {
|
public VideoEntity getVideo(Long videoId) {
|
||||||
if (redisTemplate.hasKey(String.format(VIDEO_CACHE_KEY, videoId))) {
|
if (redisTemplate.hasKey(String.format(VIDEO_CACHE_KEY, videoId))) {
|
||||||
@@ -84,6 +84,12 @@ public class VideoRepository {
|
|||||||
memberVideo.setIsBuy(1);
|
memberVideo.setIsBuy(1);
|
||||||
memberVideo.setOrderId(orderId);
|
memberVideo.setOrderId(orderId);
|
||||||
videoMapper.updateRelation(memberVideo);
|
videoMapper.updateRelation(memberVideo);
|
||||||
|
|
||||||
|
// 清理视频关系缓存
|
||||||
|
MemberVideoEntity existingVideo = videoMapper.queryUserVideo(memberId, videoId);
|
||||||
|
if (existingVideo != null && existingVideo.getFaceId() != null) {
|
||||||
|
memberRelationRepository.clearVCacheByFace(existingVideo.getFaceId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUserNotBuyItem(Long memberId, Long videoId) {
|
public void setUserNotBuyItem(Long memberId, Long videoId) {
|
||||||
@@ -93,6 +99,12 @@ public class VideoRepository {
|
|||||||
memberVideo.setIsBuy(0);
|
memberVideo.setIsBuy(0);
|
||||||
memberVideo.setOrderId(null);
|
memberVideo.setOrderId(null);
|
||||||
videoMapper.updateRelation(memberVideo);
|
videoMapper.updateRelation(memberVideo);
|
||||||
|
|
||||||
|
// 清理视频关系缓存
|
||||||
|
MemberVideoEntity existingVideo = videoMapper.queryUserVideo(memberId, videoId);
|
||||||
|
if (existingVideo != null && existingVideo.getFaceId() != null) {
|
||||||
|
memberRelationRepository.clearVCacheByFace(existingVideo.getFaceId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean getUserIsBuy(Long userId, Long videoId) {
|
public boolean getUserIsBuy(Long userId, Long videoId) {
|
||||||
@@ -133,7 +145,4 @@ public class VideoRepository {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<MemberVideoEntity> getVideoByFaceAndTemplateId(Long memberId, Long faceId, String templateId) {
|
|
||||||
return videoMapper.userFaceTemplateVideo(memberId, faceId, Long.valueOf(templateId));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,68 @@
|
|||||||
|
package com.ycwl.basic.service;
|
||||||
|
|
||||||
|
import com.ycwl.basic.dto.ZTSourceMessage;
|
||||||
|
import com.ycwl.basic.utils.JacksonUtil;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.kafka.annotation.KafkaListener;
|
||||||
|
import org.springframework.kafka.support.Acknowledgment;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ZT-Source Kafka消费者服务
|
||||||
|
* 监听zt-source topic并处理素材消息
|
||||||
|
*
|
||||||
|
* @author system
|
||||||
|
* @date 2024/12/27
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@ConditionalOnProperty(name = "kafka.enabled", havingValue = "true")
|
||||||
|
public class ZTSourceConsumerService {
|
||||||
|
|
||||||
|
private static final String ZT_SOURCE_TOPIC = "zt-source";
|
||||||
|
|
||||||
|
private final ZTSourceDataService ztSourceDataService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听zt-source topic消息
|
||||||
|
* 先解析消息并输出业务日志,然后手动确认处理
|
||||||
|
*
|
||||||
|
* @param message 消息JSON字符串
|
||||||
|
* @param ack 手动ACK确认
|
||||||
|
*/
|
||||||
|
@KafkaListener(topics = ZT_SOURCE_TOPIC, containerFactory = "manualCommitKafkaListenerContainerFactory")
|
||||||
|
public void handleZTSourceMessage(String message, Acknowledgment ack) {
|
||||||
|
ZTSourceMessage sourceMessage = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 先解析消息
|
||||||
|
sourceMessage = JacksonUtil.parseObject(message, ZTSourceMessage.class);
|
||||||
|
|
||||||
|
// 输出业务相关的日志信息
|
||||||
|
log.debug("接收到ZT-Source消息, sourceId: {}, deviceId: {}, faceSampleId: {}",
|
||||||
|
sourceMessage.getSourceId(), sourceMessage.getDeviceId(), sourceMessage.getFaceSampleId());
|
||||||
|
|
||||||
|
// 处理消息
|
||||||
|
boolean processed = ztSourceDataService.processZTSourceMessage(sourceMessage);
|
||||||
|
|
||||||
|
if (processed) {
|
||||||
|
// 只有在处理成功后才手动提交
|
||||||
|
ack.acknowledge();
|
||||||
|
log.info("ZT-Source消息处理成功并已提交, sourceId: {}", sourceMessage.getSourceId());
|
||||||
|
} else {
|
||||||
|
log.warn("ZT-Source消息处理被跳过(非照片类型),消息不会被提交, sourceId: {}, sourceType: {}",
|
||||||
|
sourceMessage.getSourceId(), sourceMessage.getSourceType());
|
||||||
|
// 对于非照片类型,也提交消息避免重复消费
|
||||||
|
ack.acknowledge();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
String sourceId = sourceMessage != null ? sourceMessage.getSourceId().toString() : "unknown";
|
||||||
|
log.error("处理ZT-Source消息失败,消息不会被提交: sourceId={}, error={}", sourceId, e.getMessage(), e);
|
||||||
|
// 不调用ack.acknowledge(),消息保持未提交状态,可以重新消费
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
138
src/main/java/com/ycwl/basic/service/ZTSourceDataService.java
Normal file
138
src/main/java/com/ycwl/basic/service/ZTSourceDataService.java
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
package com.ycwl.basic.service;
|
||||||
|
|
||||||
|
import com.ycwl.basic.dto.ZTSourceMessage;
|
||||||
|
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
|
||||||
|
import com.ycwl.basic.mapper.SourceMapper;
|
||||||
|
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
|
||||||
|
import com.ycwl.basic.repository.DeviceRepository;
|
||||||
|
import com.ycwl.basic.utils.SnowFlakeUtil;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.logging.log4j.util.Strings;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.sql.Timestamp;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ZT-Source数据处理服务
|
||||||
|
* 负责将ZT-Source消息转换并保存到数据库
|
||||||
|
*
|
||||||
|
* @author system
|
||||||
|
* @date 2024/12/27
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ZTSourceDataService {
|
||||||
|
|
||||||
|
private final SourceMapper sourceMapper;
|
||||||
|
private final DeviceRepository deviceRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理ZT-Source消息,仅处理照片类型(sourceType=2)
|
||||||
|
*
|
||||||
|
* @param message ZT-Source消息
|
||||||
|
* @return 是否处理成功
|
||||||
|
*/
|
||||||
|
public boolean processZTSourceMessage(ZTSourceMessage message) {
|
||||||
|
try {
|
||||||
|
// 仅处理照片类型的消息
|
||||||
|
if (!message.isPhoto()) {
|
||||||
|
log.debug("跳过非照片类型消息: sourceId={}, sourceType={}",
|
||||||
|
message.getSourceId(), message.getSourceType());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查必要字段
|
||||||
|
if (!validateMessage(message)) {
|
||||||
|
log.warn("消息校验失败: {}", message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为SourceEntity
|
||||||
|
SourceEntity sourceEntity = convertToSourceEntity(message);
|
||||||
|
DeviceConfigManager configManager = deviceRepository.getDeviceConfigManager(sourceEntity.getDeviceId());
|
||||||
|
if (configManager != null) {
|
||||||
|
if (Strings.isNotBlank(configManager.getString("crop_config"))) {
|
||||||
|
sourceEntity.setUrl(message.getThumbnailUrl());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到数据库
|
||||||
|
int result = sourceMapper.addFromZTSource(sourceEntity);
|
||||||
|
|
||||||
|
if (result > 0) {
|
||||||
|
log.info("成功保存ZT-Source照片素材: sourceId={}, entityId={}, scenicId={}, deviceId={}",
|
||||||
|
message.getSourceId(), sourceEntity.getId(), message.getScenicId(), message.getDeviceId());
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
log.error("保存ZT-Source照片素材失败: sourceId={}", message.getSourceId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("处理ZT-Source消息异常: sourceId={}, error={}",
|
||||||
|
message.getSourceId(), e.getMessage(), e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验消息必要字段
|
||||||
|
*/
|
||||||
|
private boolean validateMessage(ZTSourceMessage message) {
|
||||||
|
if (message.getScenicId() == null) {
|
||||||
|
log.warn("scenicId不能为空: sourceId={}", message.getSourceId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.getDeviceId() == null) {
|
||||||
|
log.warn("deviceId不能为空: sourceId={}", message.getSourceId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.getSourceUrl() == null || message.getSourceUrl().trim().isEmpty()) {
|
||||||
|
log.warn("sourceUrl不能为空: sourceId={}", message.getSourceId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将ZTSourceMessage转换为SourceEntity
|
||||||
|
*/
|
||||||
|
private SourceEntity convertToSourceEntity(ZTSourceMessage message) {
|
||||||
|
SourceEntity entity = new SourceEntity();
|
||||||
|
|
||||||
|
// 生成ID
|
||||||
|
entity.setId(SnowFlakeUtil.getLongId());
|
||||||
|
|
||||||
|
// 基本字段映射
|
||||||
|
entity.setScenicId(message.getScenicId());
|
||||||
|
entity.setDeviceId(message.getDeviceId());
|
||||||
|
entity.setUrl(message.getSourceUrl()); // 使用sourceUrl,不使用缩略图
|
||||||
|
entity.setType(2); // 照片类型
|
||||||
|
|
||||||
|
// 人脸样本ID处理
|
||||||
|
entity.setFaceSampleId(message.getFaceSampleId());
|
||||||
|
|
||||||
|
// 位置信息JSON处理
|
||||||
|
entity.setPosJson(message.getPosJson());
|
||||||
|
|
||||||
|
// 时间处理
|
||||||
|
Date shootTime = message.getShootTime();
|
||||||
|
if (shootTime != null) {
|
||||||
|
entity.setCreateTime(shootTime);
|
||||||
|
} else {
|
||||||
|
entity.setCreateTime(new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("转换ZTSourceMessage到SourceEntity: sourceId={} -> entityId={}",
|
||||||
|
message.getSourceId(), entity.getId());
|
||||||
|
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
}
|
@@ -294,7 +294,9 @@ public class AppScenicServiceImpl implements AppScenicService {
|
|||||||
if (onlineStatus != null) {
|
if (onlineStatus != null) {
|
||||||
deviceRespVO.setUpdateAt(onlineStatus.getKeepaliveAt());
|
deviceRespVO.setUpdateAt(onlineStatus.getKeepaliveAt());
|
||||||
deviceRespVO.setKeepaliveAt(onlineStatus.getKeepaliveAt());
|
deviceRespVO.setKeepaliveAt(onlineStatus.getKeepaliveAt());
|
||||||
if (new Date().getTime() - onlineStatus.getKeepaliveAt().getTime() > 300000) {
|
if (onlineStatus.getKeepaliveAt() == null) {
|
||||||
|
deviceRespVO.setOnline(0);
|
||||||
|
} else if (new Date().getTime() - onlineStatus.getKeepaliveAt().getTime() > 300000) {
|
||||||
deviceRespVO.setOnline(0);
|
deviceRespVO.setOnline(0);
|
||||||
} else {
|
} else {
|
||||||
deviceRespVO.setOnline(onlineStatus.getOnline());
|
deviceRespVO.setOnline(onlineStatus.getOnline());
|
||||||
|
@@ -4,6 +4,8 @@ import cn.hutool.core.date.DateUtil;
|
|||||||
import cn.hutool.http.HttpUtil;
|
import cn.hutool.http.HttpUtil;
|
||||||
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
|
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
|
||||||
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
|
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
|
||||||
|
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
||||||
|
import com.ycwl.basic.repository.MemberRelationRepository;
|
||||||
import com.ycwl.basic.utils.JacksonUtil;
|
import com.ycwl.basic.utils.JacksonUtil;
|
||||||
import com.ycwl.basic.biz.CouponBiz;
|
import com.ycwl.basic.biz.CouponBiz;
|
||||||
import com.ycwl.basic.biz.OrderBiz;
|
import com.ycwl.basic.biz.OrderBiz;
|
||||||
@@ -105,6 +107,8 @@ public class GoodsServiceImpl implements GoodsService {
|
|||||||
private TemplateBiz templateBiz;
|
private TemplateBiz templateBiz;
|
||||||
@Autowired
|
@Autowired
|
||||||
private VideoUpdateConfig videoUpdateConfig;
|
private VideoUpdateConfig videoUpdateConfig;
|
||||||
|
@Autowired
|
||||||
|
private MemberRelationRepository memberRelationRepository;
|
||||||
|
|
||||||
public ApiResponse<List<GoodsPageVO>> goodsList(GoodsReqQuery query) {
|
public ApiResponse<List<GoodsPageVO>> goodsList(GoodsReqQuery query) {
|
||||||
Long scenicId = query.getScenicId();
|
Long scenicId = query.getScenicId();
|
||||||
@@ -362,7 +366,7 @@ public class GoodsServiceImpl implements GoodsService {
|
|||||||
response.setStatus(2);
|
response.setStatus(2);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
List<MemberVideoEntity> taskList = videoMapper.listRelationByFace(userId, faceId);
|
List<MemberVideoEntity> taskList = videoMapper.listRelationByFace(faceId);
|
||||||
if (faceCutStatus != 1 && taskList.isEmpty()) {
|
if (faceCutStatus != 1 && taskList.isEmpty()) {
|
||||||
// 视频切成了能够获取视频的状态,但是没有任务,还是显示正在处理
|
// 视频切成了能够获取视频的状态,但是没有任务,还是显示正在处理
|
||||||
response.setStatus(0);
|
response.setStatus(0);
|
||||||
@@ -404,7 +408,7 @@ public class GoodsServiceImpl implements GoodsService {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
// 重查一下
|
// 重查一下
|
||||||
taskList = videoMapper.listRelationByFace(userId, faceId);
|
taskList = videoMapper.listRelationByFace(faceId);
|
||||||
MemberVideoEntity lastVideo = taskList.getLast();
|
MemberVideoEntity lastVideo = taskList.getLast();
|
||||||
if (null == lastVideo.getVideoId()) {
|
if (null == lastVideo.getVideoId()) {
|
||||||
response.setTemplateId(lastVideo.getTemplateId());
|
response.setTemplateId(lastVideo.getTemplateId());
|
||||||
@@ -779,14 +783,8 @@ public class GoodsServiceImpl implements GoodsService {
|
|||||||
if (face == null) {
|
if (face == null) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
Integer sourceType = query.getSourceType();
|
List<MemberSourceEntity> memberSourceEntities = memberRelationRepository.listSourceByFaceRelation(face.getId(), query.getSourceType());
|
||||||
SourceReqQuery sourceReqQuery = new SourceReqQuery();
|
return memberSourceEntities.size();
|
||||||
sourceReqQuery.setScenicId(face.getScenicId());
|
|
||||||
sourceReqQuery.setIsBuy(query.getIsBuy());
|
|
||||||
sourceReqQuery.setMemberId(face.getMemberId());
|
|
||||||
sourceReqQuery.setType(sourceType);
|
|
||||||
sourceReqQuery.setFaceId(query.getFaceId());
|
|
||||||
return sourceMapper.countUser(sourceReqQuery);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@@ -25,12 +25,10 @@ import com.ycwl.basic.model.mobile.goods.VideoTaskStatusVO;
|
|||||||
import com.ycwl.basic.model.mobile.order.IsBuyRespVO;
|
import com.ycwl.basic.model.mobile.order.IsBuyRespVO;
|
||||||
import com.ycwl.basic.model.mobile.scenic.content.ContentPageVO;
|
import com.ycwl.basic.model.mobile.scenic.content.ContentPageVO;
|
||||||
import com.ycwl.basic.model.mobile.statistic.req.StatisticsRecordAddReq;
|
import com.ycwl.basic.model.mobile.statistic.req.StatisticsRecordAddReq;
|
||||||
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
|
|
||||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||||
import com.ycwl.basic.model.pc.face.req.FaceReqQuery;
|
import com.ycwl.basic.model.pc.face.req.FaceReqQuery;
|
||||||
import com.ycwl.basic.model.pc.face.resp.FaceRespVO;
|
import com.ycwl.basic.model.pc.face.resp.FaceRespVO;
|
||||||
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
||||||
import com.ycwl.basic.model.pc.faceSample.resp.FaceSampleRespVO;
|
|
||||||
import com.ycwl.basic.model.pc.mp.MpConfigEntity;
|
import com.ycwl.basic.model.pc.mp.MpConfigEntity;
|
||||||
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
|
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
|
||||||
import com.ycwl.basic.model.pc.project.resp.ProjectRespVO;
|
import com.ycwl.basic.model.pc.project.resp.ProjectRespVO;
|
||||||
@@ -40,13 +38,16 @@ import com.ycwl.basic.model.pc.source.req.SourceReqQuery;
|
|||||||
import com.ycwl.basic.model.pc.source.resp.SourceRespVO;
|
import com.ycwl.basic.model.pc.source.resp.SourceRespVO;
|
||||||
import com.ycwl.basic.model.pc.order.entity.OrderEntity;
|
import com.ycwl.basic.model.pc.order.entity.OrderEntity;
|
||||||
import com.ycwl.basic.model.pc.task.entity.TaskEntity;
|
import com.ycwl.basic.model.pc.task.entity.TaskEntity;
|
||||||
|
import com.ycwl.basic.model.pc.template.resp.TemplateRespVO;
|
||||||
import com.ycwl.basic.model.pc.video.entity.MemberVideoEntity;
|
import com.ycwl.basic.model.pc.video.entity.MemberVideoEntity;
|
||||||
import com.ycwl.basic.model.pc.video.entity.VideoEntity;
|
import com.ycwl.basic.model.pc.video.entity.VideoEntity;
|
||||||
import com.ycwl.basic.model.repository.TaskUpdateResult;
|
import com.ycwl.basic.model.repository.TaskUpdateResult;
|
||||||
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
|
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
|
||||||
import com.ycwl.basic.repository.DeviceRepository;
|
import com.ycwl.basic.repository.DeviceRepository;
|
||||||
import com.ycwl.basic.repository.FaceRepository;
|
import com.ycwl.basic.repository.FaceRepository;
|
||||||
|
import com.ycwl.basic.repository.MemberRelationRepository;
|
||||||
import com.ycwl.basic.repository.ScenicRepository;
|
import com.ycwl.basic.repository.ScenicRepository;
|
||||||
|
import com.ycwl.basic.repository.TemplateRepository;
|
||||||
import com.ycwl.basic.repository.VideoRepository;
|
import com.ycwl.basic.repository.VideoRepository;
|
||||||
import com.ycwl.basic.repository.VideoTaskRepository;
|
import com.ycwl.basic.repository.VideoTaskRepository;
|
||||||
import com.ycwl.basic.service.mobile.GoodsService;
|
import com.ycwl.basic.service.mobile.GoodsService;
|
||||||
@@ -71,15 +72,16 @@ import java.io.File;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Random;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import static com.ycwl.basic.constant.FaceConstant.FACE_LOW_THRESHOLD_PFX;
|
import static com.ycwl.basic.constant.FaceConstant.FACE_LOW_THRESHOLD_PFX;
|
||||||
import static com.ycwl.basic.constant.FaceConstant.FACE_RECOGNITION_COUNT_PFX;
|
import static com.ycwl.basic.constant.FaceConstant.FACE_RECOGNITION_COUNT_PFX;
|
||||||
@@ -134,6 +136,10 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
private GoodsService goodsService;
|
private GoodsService goodsService;
|
||||||
@Autowired
|
@Autowired
|
||||||
private ProjectMapper projectMapper;
|
private ProjectMapper projectMapper;
|
||||||
|
@Autowired
|
||||||
|
private MemberRelationRepository memberRelationRepository;
|
||||||
|
@Autowired
|
||||||
|
private TemplateRepository templateRepository;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ApiResponse<PageInfo<FaceRespVO>> pageQuery(FaceReqQuery faceReqQuery) {
|
public ApiResponse<PageInfo<FaceRespVO>> pageQuery(FaceReqQuery faceReqQuery) {
|
||||||
@@ -339,9 +345,18 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
handleVideoRecreation(scenicConfig, memberSourceEntityList, faceId,
|
handleVideoRecreation(scenicConfig, memberSourceEntityList, faceId,
|
||||||
face.getMemberId(), sampleListIds, isNew);
|
face.getMemberId(), sampleListIds, isNew);
|
||||||
|
|
||||||
// 保存关联关系并创建任务
|
// 过滤已存在的关联关系和无效的source引用,防止数据不一致
|
||||||
sourceMapper.addRelations(memberSourceEntityList);
|
List<MemberSourceEntity> existingFiltered = sourceMapper.filterExistingRelations(memberSourceEntityList);
|
||||||
taskTaskService.autoCreateTaskByFaceId(face.getId());
|
List<MemberSourceEntity> validFiltered = sourceMapper.filterValidSourceRelations(existingFiltered);
|
||||||
|
if (!validFiltered.isEmpty()) {
|
||||||
|
sourceMapper.addRelations(validFiltered);
|
||||||
|
log.debug("创建关联关系: faceId={}, 原始数量={}, 过滤后数量={}",
|
||||||
|
faceId, memberSourceEntityList.size(), validFiltered.size());
|
||||||
|
} else {
|
||||||
|
log.warn("没有有效的关联关系可创建: faceId={}, 原始数量={}", faceId, memberSourceEntityList.size());
|
||||||
|
}
|
||||||
|
memberRelationRepository.clearSCacheByFace(faceId);
|
||||||
|
taskTaskService.autoCreateTaskByFaceId(faceId);
|
||||||
|
|
||||||
log.info("人脸匹配完成:faceId={}, 匹配样本数={}, 关联源文件数={}, 免费数={}",
|
log.info("人脸匹配完成:faceId={}, 匹配样本数={}, 关联源文件数={}, 免费数={}",
|
||||||
faceId, sampleListIds.size(), memberSourceEntityList.size(), freeSourceIds.size());
|
faceId, sampleListIds.size(), memberSourceEntityList.size(), freeSourceIds.size());
|
||||||
@@ -476,7 +491,21 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
return sourceEntities.stream().map(sourceEntity -> {
|
List<SourceEntity> filteredSourceEntities = sourceEntities.stream()
|
||||||
|
.sorted(Comparator.comparing(SourceEntity::getCreateTime).reversed())
|
||||||
|
.collect(Collectors.groupingBy(SourceEntity::getDeviceId))
|
||||||
|
.entrySet()
|
||||||
|
.stream().flatMap(entry -> {
|
||||||
|
DeviceConfigManager configManager = deviceRepository.getDeviceConfigManager(entry.getKey());
|
||||||
|
if (configManager.getInteger("limit_video", 0) > 0) {
|
||||||
|
return Stream.concat(
|
||||||
|
entry.getValue().stream().filter(item -> item.getType() == 2),
|
||||||
|
entry.getValue().stream().filter(item -> item.getType() == 1).limit(Math.min(entry.getValue().size(), configManager.getInteger("limit_video", 0)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return entry.getValue().stream();
|
||||||
|
}).toList();
|
||||||
|
return filteredSourceEntities.stream().map(sourceEntity -> {
|
||||||
DeviceConfigManager deviceConfig = deviceRepository.getDeviceConfigManager(sourceEntity.getDeviceId());
|
DeviceConfigManager deviceConfig = deviceRepository.getDeviceConfigManager(sourceEntity.getDeviceId());
|
||||||
MemberSourceEntity memberSourceEntity = new MemberSourceEntity();
|
MemberSourceEntity memberSourceEntity = new MemberSourceEntity();
|
||||||
memberSourceEntity.setScenicId(face.getScenicId());
|
memberSourceEntity.setScenicId(face.getScenicId());
|
||||||
@@ -614,13 +643,31 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
.filter(item -> Integer.valueOf(2).equals(item.getType()))
|
.filter(item -> Integer.valueOf(2).equals(item.getType()))
|
||||||
.count();
|
.count();
|
||||||
|
|
||||||
|
List<FaceSampleEntity> faceSampleList = faceRepository.getFaceSampleList(faceId);
|
||||||
|
if (faceSampleList.isEmpty()) {
|
||||||
|
log.info("faceId:{} sample list not exist", faceId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<Long> faceSampleIds = faceSampleList.stream()
|
||||||
|
.sorted(Comparator.comparing(FaceSampleEntity::getCreateAt).reversed())
|
||||||
|
.collect(Collectors.groupingBy(FaceSampleEntity::getDeviceId))
|
||||||
|
.entrySet()
|
||||||
|
.stream().flatMap(entry -> {
|
||||||
|
DeviceConfigManager configManager = deviceRepository.getDeviceConfigManager(entry.getKey());
|
||||||
|
if (configManager.getInteger("limit_video", 0) > 0) {
|
||||||
|
return entry.getValue().subList(0, Math.min(entry.getValue().size(), configManager.getInteger("limit_video", 0))).stream();
|
||||||
|
}
|
||||||
|
return entry.getValue().stream();
|
||||||
|
}).toList()
|
||||||
|
.stream().map(FaceSampleEntity::getId).toList();
|
||||||
|
log.info("视频切分任务: faceId={}, 原始数量={}, 筛选后数量={}", faceId, faceSampleList.size(), faceSampleIds.size());
|
||||||
log.debug("视频重切逻辑:视频数量 {}, 照片数量 {}", videoCount, photoCount);
|
log.debug("视频重切逻辑:视频数量 {}, 照片数量 {}", videoCount, photoCount);
|
||||||
|
|
||||||
// 只有照片数量大于视频数量时才创建重切任务
|
// 只有照片数量大于视频数量时才创建重切任务
|
||||||
if (photoCount > videoCount) {
|
if (photoCount > videoCount) {
|
||||||
VideoPieceGetter.Task task = new VideoPieceGetter.Task();
|
VideoPieceGetter.Task task = new VideoPieceGetter.Task();
|
||||||
task.faceId = faceId;
|
task.faceId = faceId;
|
||||||
task.faceSampleIds = sampleListIds;
|
task.faceSampleIds = faceSampleIds;
|
||||||
task.templateId = null;
|
task.templateId = null;
|
||||||
task.memberId = memberId;
|
task.memberId = memberId;
|
||||||
task.callback = () -> {
|
task.callback = () -> {
|
||||||
@@ -652,14 +699,32 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ContentPageVO> faceContentList(Long faceId) {
|
public List<ContentPageVO> faceContentList(Long faceId) {
|
||||||
FaceRespVO faceRespVO = faceMapper.getById(faceId);
|
FaceEntity face = faceRepository.getFace(faceId);
|
||||||
if (faceRespVO == null) {
|
if (face == null) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
Long userId = faceRespVO.getMemberId();
|
Long userId = face.getMemberId();
|
||||||
List<ContentPageVO> contentList = templateMapper.listFor(faceRespVO.getScenicId());
|
List<TemplateRespVO> templateList = templateRepository.getTemplateListByScenicId(face.getScenicId());
|
||||||
contentList.forEach(contentPageVO -> {
|
List<ContentPageVO> contentList = templateList.stream().map(template -> {
|
||||||
List<MemberVideoEntity> memberVideoEntityList = videoMapper.userFaceTemplateVideo(userId, faceId, contentPageVO.getTemplateId());
|
/// select t.id templateId, t.scenic_id, t.`group`, t.`name`, pid, t.cover_url templateCoverUrl,
|
||||||
|
/// 0 as sourceType, sort,
|
||||||
|
/// t.create_time, t.price
|
||||||
|
/// from template t
|
||||||
|
/// where t.scenic_id = #{scenicId} and pid = 0 and t.status = 1
|
||||||
|
/// order by sort
|
||||||
|
ContentPageVO content = new ContentPageVO();
|
||||||
|
content.setTemplateId(template.getId());
|
||||||
|
content.setScenicId(template.getScenicId());
|
||||||
|
content.setGroup(template.getGroup());
|
||||||
|
content.setName(template.getName());
|
||||||
|
content.setTemplateCoverUrl(template.getCoverUrl());
|
||||||
|
content.setSourceType(0);
|
||||||
|
content.setSort(template.getSort());
|
||||||
|
content.setGoodsType(0);
|
||||||
|
content.setScenicName(template.getScenicName());
|
||||||
|
return content;
|
||||||
|
}).peek(contentPageVO -> {
|
||||||
|
List<MemberVideoEntity> memberVideoEntityList = memberRelationRepository.listRelationByFaceAndTemplate(faceId, contentPageVO.getTemplateId());
|
||||||
contentPageVO.setGoodsType(0);
|
contentPageVO.setGoodsType(0);
|
||||||
contentPageVO.setContentType(1);
|
contentPageVO.setContentType(1);
|
||||||
contentPageVO.setSort(contentPageVO.getSort());
|
contentPageVO.setSort(contentPageVO.getSort());
|
||||||
@@ -668,6 +733,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());
|
||||||
@@ -692,14 +758,19 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
contentPageVO.setLockType(1);
|
contentPageVO.setLockType(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
IsBuyRespVO buy = orderBiz.isBuy(userId, contentPageVO.getScenicId(), contentPageVO.getGoodsType(), contentPageVO.getContentId());
|
boolean buy = orderBiz.checkUserBuyItem(userId, contentPageVO.getGoodsType(), contentPageVO.getContentId());
|
||||||
if (buy.isBuy()) {
|
if (!buy) {
|
||||||
contentPageVO.setIsBuy(1);
|
buy = orderBiz.checkUserBuyItem(userId, -1, contentPageVO.getTemplateId());
|
||||||
}
|
}
|
||||||
});
|
if (buy) {
|
||||||
|
contentPageVO.setIsBuy(1);
|
||||||
|
} else {
|
||||||
|
contentPageVO.setIsBuy(0);
|
||||||
|
}
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
|
||||||
SourceReqQuery sourceReqQuery = new SourceReqQuery();
|
SourceReqQuery sourceReqQuery = new SourceReqQuery();
|
||||||
sourceReqQuery.setScenicId(faceRespVO.getScenicId());
|
sourceReqQuery.setScenicId(face.getScenicId());
|
||||||
sourceReqQuery.setFaceId(faceId);
|
sourceReqQuery.setFaceId(faceId);
|
||||||
sourceReqQuery.setMemberId(userId);
|
sourceReqQuery.setMemberId(userId);
|
||||||
//查询源素材
|
//查询源素材
|
||||||
@@ -710,8 +781,8 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
sourceImageContent.setName("照片集");
|
sourceImageContent.setName("照片集");
|
||||||
sourceVideoContent.setSort(9999);
|
sourceVideoContent.setSort(9999);
|
||||||
sourceImageContent.setSort(9999);
|
sourceImageContent.setSort(9999);
|
||||||
sourceVideoContent.setScenicId(faceRespVO.getScenicId());
|
sourceVideoContent.setScenicId(face.getScenicId());
|
||||||
sourceImageContent.setScenicId(faceRespVO.getScenicId());
|
sourceImageContent.setScenicId(face.getScenicId());
|
||||||
sourceVideoContent.setGoodsType(1);
|
sourceVideoContent.setGoodsType(1);
|
||||||
sourceImageContent.setGoodsType(2);
|
sourceImageContent.setGoodsType(2);
|
||||||
sourceVideoContent.setContentType(2);
|
sourceVideoContent.setContentType(2);
|
||||||
@@ -720,9 +791,9 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
sourceImageContent.setLockType(-1);
|
sourceImageContent.setLockType(-1);
|
||||||
sourceVideoContent.setGroup("直出原片");
|
sourceVideoContent.setGroup("直出原片");
|
||||||
sourceImageContent.setGroup("直出原片");
|
sourceImageContent.setGroup("直出原片");
|
||||||
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(faceRespVO.getScenicId());
|
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
|
||||||
if (!Boolean.TRUE.equals(scenicConfig.getBoolean("disable_source_image"))) {
|
if (!Boolean.TRUE.equals(scenicConfig.getBoolean("disable_source_image"))) {
|
||||||
IsBuyRespVO isBuyRespVO = orderBiz.isBuy(userId, faceRespVO.getScenicId(), 2, faceId);
|
IsBuyRespVO isBuyRespVO = orderBiz.isBuy(userId, face.getScenicId(), 2, faceId);
|
||||||
sourceImageContent.setSourceType(isBuyRespVO.getGoodsType());
|
sourceImageContent.setSourceType(isBuyRespVO.getGoodsType());
|
||||||
sourceImageContent.setContentId(isBuyRespVO.getGoodsId());
|
sourceImageContent.setContentId(isBuyRespVO.getGoodsId());
|
||||||
if (isBuyRespVO.isBuy()) {
|
if (isBuyRespVO.isBuy()) {
|
||||||
@@ -730,7 +801,7 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
} else {
|
} else {
|
||||||
sourceImageContent.setIsBuy(0);
|
sourceImageContent.setIsBuy(0);
|
||||||
}
|
}
|
||||||
List<MemberSourceEntity> relations = sourceMapper.listByFaceRelation(faceRespVO.getMemberId(), faceId, 2);
|
List<MemberSourceEntity> relations = memberRelationRepository.listSourceByFaceRelation(faceId, 2);
|
||||||
if (!relations.isEmpty()) {
|
if (!relations.isEmpty()) {
|
||||||
sourceImageContent.setLockType(-1);
|
sourceImageContent.setLockType(-1);
|
||||||
} else {
|
} else {
|
||||||
@@ -741,7 +812,7 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
contentList.add(sourceImageContent);
|
contentList.add(sourceImageContent);
|
||||||
}
|
}
|
||||||
if (!Boolean.TRUE.equals(scenicConfig.getBoolean("disable_source_video"))) {
|
if (!Boolean.TRUE.equals(scenicConfig.getBoolean("disable_source_video"))) {
|
||||||
IsBuyRespVO isBuyRespVO = orderBiz.isBuy(userId, faceRespVO.getScenicId(), 1, faceId);
|
IsBuyRespVO isBuyRespVO = orderBiz.isBuy(userId, face.getScenicId(), 1, faceId);
|
||||||
sourceVideoContent.setSourceType(isBuyRespVO.getGoodsType());
|
sourceVideoContent.setSourceType(isBuyRespVO.getGoodsType());
|
||||||
sourceVideoContent.setContentId(isBuyRespVO.getGoodsId());
|
sourceVideoContent.setContentId(isBuyRespVO.getGoodsId());
|
||||||
if (isBuyRespVO.isBuy()) {
|
if (isBuyRespVO.isBuy()) {
|
||||||
@@ -749,7 +820,7 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
} else {
|
} else {
|
||||||
sourceVideoContent.setIsBuy(0);
|
sourceVideoContent.setIsBuy(0);
|
||||||
}
|
}
|
||||||
List<MemberSourceEntity> relations = sourceMapper.listByFaceRelation(faceRespVO.getMemberId(), faceId, 1);
|
List<MemberSourceEntity> relations = memberRelationRepository.listSourceByFaceRelation(faceId, 1);
|
||||||
if (!relations.isEmpty()) {
|
if (!relations.isEmpty()) {
|
||||||
sourceVideoContent.setLockType(-1);
|
sourceVideoContent.setLockType(-1);
|
||||||
} else {
|
} else {
|
||||||
@@ -885,12 +956,13 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
sourceReqQuery.setMemberId(face.getMemberId());
|
sourceReqQuery.setMemberId(face.getMemberId());
|
||||||
sourceReqQuery.setFaceId(faceId);
|
sourceReqQuery.setFaceId(faceId);
|
||||||
sourceReqQuery.setType(2);
|
sourceReqQuery.setType(2);
|
||||||
Integer countUser = sourceMapper.countUser(sourceReqQuery);
|
List<MemberSourceEntity> countUser = memberRelationRepository.listSourceByFaceRelation(faceId, 2);
|
||||||
if (countUser != null && countUser > 0) {
|
if (countUser != null && !countUser.isEmpty()) {
|
||||||
statusResp.setStep2Status(true);
|
statusResp.setStep2Status(true);
|
||||||
} else {
|
} else {
|
||||||
statusResp.setStep2Status(false);
|
statusResp.setStep2Status(false);
|
||||||
statusResp.setDisplayText("Hey,快去智能机位打卡吧");
|
statusResp.setDisplayText("Hey,快去智能机位打卡吧");
|
||||||
|
return statusResp;
|
||||||
}
|
}
|
||||||
VideoTaskStatusVO taskStatusByFaceId = goodsService.getTaskStatusByFaceId(faceId);
|
VideoTaskStatusVO taskStatusByFaceId = goodsService.getTaskStatusByFaceId(faceId);
|
||||||
if (Integer.valueOf(1).equals(taskStatusByFaceId.getStatus())) {
|
if (Integer.valueOf(1).equals(taskStatusByFaceId.getStatus())) {
|
||||||
@@ -1088,8 +1160,18 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
handleVideoRecreation(scenicConfig, memberSourceEntityList, faceId,
|
handleVideoRecreation(scenicConfig, memberSourceEntityList, faceId,
|
||||||
face.getMemberId(), sampleListIds, false);
|
face.getMemberId(), sampleListIds, false);
|
||||||
|
|
||||||
sourceMapper.addRelations(memberSourceEntityList);
|
// 过滤已存在的关联关系和无效的source引用,防止数据不一致
|
||||||
taskTaskService.autoCreateTaskByFaceId(face.getId());
|
List<MemberSourceEntity> existingFiltered = sourceMapper.filterExistingRelations(memberSourceEntityList);
|
||||||
|
List<MemberSourceEntity> validFiltered = sourceMapper.filterValidSourceRelations(existingFiltered);
|
||||||
|
if (!validFiltered.isEmpty()) {
|
||||||
|
sourceMapper.addRelations(validFiltered);
|
||||||
|
log.debug("创建关联关系: faceId={}, 原始数量={}, 过滤后数量={}",
|
||||||
|
faceId, memberSourceEntityList.size(), validFiltered.size());
|
||||||
|
} else {
|
||||||
|
log.warn("没有有效的关联关系可创建: faceId={}, 原始数量={}", faceId, memberSourceEntityList.size());
|
||||||
|
}
|
||||||
|
memberRelationRepository.clearSCacheByFace(faceId);
|
||||||
|
taskTaskService.autoCreateTaskByFaceId(faceId);
|
||||||
|
|
||||||
log.info("自定义人脸匹配完成:faceId={}, 匹配样本数={}, 关联源文件数={}, 免费数={}",
|
log.info("自定义人脸匹配完成:faceId={}, 匹配样本数={}, 关联源文件数={}, 免费数={}",
|
||||||
faceId, sampleListIds.size(), memberSourceEntityList.size(), freeSourceIds.size());
|
faceId, sampleListIds.size(), memberSourceEntityList.size(), freeSourceIds.size());
|
||||||
|
@@ -51,6 +51,7 @@ import com.ycwl.basic.model.wx.WXPayOrderReqVO;
|
|||||||
import com.ycwl.basic.pay.adapter.IPayAdapter;
|
import com.ycwl.basic.pay.adapter.IPayAdapter;
|
||||||
import com.ycwl.basic.pay.entity.PayResponse;
|
import com.ycwl.basic.pay.entity.PayResponse;
|
||||||
import com.ycwl.basic.repository.FaceRepository;
|
import com.ycwl.basic.repository.FaceRepository;
|
||||||
|
import com.ycwl.basic.repository.MemberRelationRepository;
|
||||||
import com.ycwl.basic.repository.PriceRepository;
|
import com.ycwl.basic.repository.PriceRepository;
|
||||||
import com.ycwl.basic.repository.ScenicRepository;
|
import com.ycwl.basic.repository.ScenicRepository;
|
||||||
import com.ycwl.basic.repository.TemplateRepository;
|
import com.ycwl.basic.repository.TemplateRepository;
|
||||||
@@ -135,6 +136,8 @@ public class OrderServiceImpl implements OrderService {
|
|||||||
private IVoucherService iVoucherService;
|
private IVoucherService iVoucherService;
|
||||||
@Autowired
|
@Autowired
|
||||||
private ICouponService iCouponService;
|
private ICouponService iCouponService;
|
||||||
|
@Autowired
|
||||||
|
private MemberRelationRepository memberRelationRepository;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ApiResponse<PageInfo<OrderRespVO>> pageQuery(OrderReqQuery query) {
|
public ApiResponse<PageInfo<OrderRespVO>> pageQuery(OrderReqQuery query) {
|
||||||
@@ -760,7 +763,7 @@ public class OrderServiceImpl implements OrderService {
|
|||||||
return ApiResponse.fail("您无权购买此内容!");
|
return ApiResponse.fail("您无权购买此内容!");
|
||||||
}
|
}
|
||||||
// 找下有没有照片
|
// 找下有没有照片
|
||||||
List<MemberSourceEntity> photoList = sourceMapper.listByFaceRelation(userId, batchOrderReqVO.getFaceId(), 2);
|
List<MemberSourceEntity> photoList = memberRelationRepository.listSourceByFaceRelation(batchOrderReqVO.getFaceId(), 2);
|
||||||
if (photoList.isEmpty()) {
|
if (photoList.isEmpty()) {
|
||||||
log.info("请先游玩后再来购买商品!");
|
log.info("请先游玩后再来购买商品!");
|
||||||
return ApiResponse.fail("请先游玩后再来购买商品!");
|
return ApiResponse.fail("请先游玩后再来购买商品!");
|
||||||
@@ -843,7 +846,7 @@ public class OrderServiceImpl implements OrderService {
|
|||||||
Long goodsId = switch (productItem.getProductType()) {
|
Long goodsId = switch (productItem.getProductType()) {
|
||||||
case PHOTO_SET, RECORDING_SET -> face.getId();
|
case PHOTO_SET, RECORDING_SET -> face.getId();
|
||||||
case VLOG_VIDEO -> {
|
case VLOG_VIDEO -> {
|
||||||
List<MemberVideoEntity> videos = videoRepository.getVideoByFaceAndTemplateId(face.getMemberId(), face.getId(), productItem.getProductId());
|
List<MemberVideoEntity> videos = memberRelationRepository.listRelationByFaceAndTemplate(face.getId(), Long.valueOf(productItem.getProductId()));
|
||||||
yield videos.getFirst().getVideoId();
|
yield videos.getFirst().getVideoId();
|
||||||
}
|
}
|
||||||
default -> 0L;
|
default -> 0L;
|
||||||
|
@@ -48,7 +48,7 @@ public interface PrinterService {
|
|||||||
|
|
||||||
PriceObj queryPrice(Long memberId, Long scenicId);
|
PriceObj queryPrice(Long memberId, Long scenicId);
|
||||||
|
|
||||||
boolean addUserPhotoFromSource(Long memberId, Long scenicId, FromSourceReq req);
|
List<Integer> addUserPhotoFromSource(Long memberId, Long scenicId, FromSourceReq req);
|
||||||
|
|
||||||
Map<String, Object> createOrder(Long memberId, Long scenicId, Integer printerId);
|
Map<String, Object> createOrder(Long memberId, Long scenicId, Integer printerId);
|
||||||
|
|
||||||
|
@@ -19,6 +19,7 @@ import com.ycwl.basic.pricing.dto.PriceCalculationResult;
|
|||||||
import com.ycwl.basic.pricing.dto.ProductItem;
|
import com.ycwl.basic.pricing.dto.ProductItem;
|
||||||
import com.ycwl.basic.pricing.enums.ProductType;
|
import com.ycwl.basic.pricing.enums.ProductType;
|
||||||
import com.ycwl.basic.pricing.service.IPriceCalculationService;
|
import com.ycwl.basic.pricing.service.IPriceCalculationService;
|
||||||
|
import com.ycwl.basic.model.pc.printer.entity.MemberPrintEntity;
|
||||||
import com.ycwl.basic.model.pc.printer.entity.PrintTaskEntity;
|
import com.ycwl.basic.model.pc.printer.entity.PrintTaskEntity;
|
||||||
import com.ycwl.basic.model.pc.printer.entity.PrinterEntity;
|
import com.ycwl.basic.model.pc.printer.entity.PrinterEntity;
|
||||||
import com.ycwl.basic.model.pc.printer.resp.MemberPrintResp;
|
import com.ycwl.basic.model.pc.printer.resp.MemberPrintResp;
|
||||||
@@ -39,16 +40,19 @@ import org.apache.commons.lang3.StringUtils;
|
|||||||
import org.apache.commons.lang3.Strings;
|
import org.apache.commons.lang3.Strings;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -188,7 +192,13 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean addUserPhoto(Long memberId, Long scenicId, String url) {
|
public boolean addUserPhoto(Long memberId, Long scenicId, String url) {
|
||||||
printerMapper.addUserPhoto(memberId, scenicId, url);
|
MemberPrintEntity entity = new MemberPrintEntity();
|
||||||
|
entity.setMemberId(memberId);
|
||||||
|
entity.setScenicId(scenicId);
|
||||||
|
entity.setOrigUrl(url);
|
||||||
|
entity.setCropUrl(url);
|
||||||
|
entity.setStatus(0);
|
||||||
|
printerMapper.addUserPhoto(entity);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,15 +267,34 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean addUserPhotoFromSource(Long memberId, Long scenicId, FromSourceReq req) {
|
public List<Integer> addUserPhotoFromSource(Long memberId, Long scenicId, FromSourceReq req) {
|
||||||
|
List<Integer> resultIds = new ArrayList<>();
|
||||||
req.getIds().forEach(id -> {
|
req.getIds().forEach(id -> {
|
||||||
SourceRespVO byId = sourceMapper.getById(id);
|
SourceRespVO byId = sourceMapper.getById(id);
|
||||||
if (byId == null) {
|
if (byId == null) {
|
||||||
|
resultIds.add(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
printerMapper.addUserPhoto(memberId, scenicId, byId.getUrl());
|
MemberPrintEntity entity = new MemberPrintEntity();
|
||||||
|
entity.setMemberId(memberId);
|
||||||
|
entity.setScenicId(scenicId);
|
||||||
|
entity.setOrigUrl(byId.getUrl());
|
||||||
|
entity.setCropUrl(byId.getUrl());
|
||||||
|
entity.setStatus(0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
int rows = printerMapper.addUserPhoto(entity);
|
||||||
|
if (rows > 0 && entity.getId() != null) {
|
||||||
|
resultIds.add(entity.getId());
|
||||||
|
} else {
|
||||||
|
resultIds.add(null);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("添加用户照片失败, memberId={}, scenicId={}, sourceId={}", memberId, scenicId, id, e);
|
||||||
|
resultIds.add(null);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return false;
|
return resultIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -377,8 +406,16 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
printerMapper.updateUserPhotoListToPrinter(memberId, scenicId, printerId);
|
printerMapper.updateUserPhotoListToPrinter(memberId, scenicId, printerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RedisTemplate<String, String> redisTemplate;
|
||||||
|
private static final String USER_PHOTO_LIST_TO_PRINTER = "USER_PHOTO_LIST_TO_PRINTER:";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setUserIsBuyItem(Long memberId, Long id, Long orderId) {
|
public void setUserIsBuyItem(Long memberId, Long id, Long orderId) {
|
||||||
|
if (redisTemplate.opsForValue().get(USER_PHOTO_LIST_TO_PRINTER + memberId + ":" + orderId) != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
redisTemplate.opsForValue().set(USER_PHOTO_LIST_TO_PRINTER + memberId + ":" + orderId, "1", 60, TimeUnit.SECONDS);
|
||||||
printerMapper.setUserIsBuyItem(memberId, id, orderId);
|
printerMapper.setUserIsBuyItem(memberId, id, orderId);
|
||||||
// 创建打印任务
|
// 创建打印任务
|
||||||
List<MemberPrintResp> userPhotoListByOrderId = getUserPhotoListByOrderId(orderId);
|
List<MemberPrintResp> userPhotoListByOrderId = getUserPhotoListByOrderId(orderId);
|
||||||
|
@@ -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);
|
||||||
|
|
||||||
|
@@ -3,6 +3,7 @@ package com.ycwl.basic.service.task.impl;
|
|||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
|
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
|
||||||
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
|
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
|
||||||
|
import com.ycwl.basic.repository.MemberRelationRepository;
|
||||||
import com.ycwl.basic.utils.JacksonUtil;
|
import com.ycwl.basic.utils.JacksonUtil;
|
||||||
import com.aliyuncs.facebody.model.v20191230.SearchFaceRequest;
|
import com.aliyuncs.facebody.model.v20191230.SearchFaceRequest;
|
||||||
import com.ycwl.basic.biz.OrderBiz;
|
import com.ycwl.basic.biz.OrderBiz;
|
||||||
@@ -48,6 +49,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -81,6 +83,8 @@ public class TaskFaceServiceImpl implements TaskFaceService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
@Lazy
|
@Lazy
|
||||||
private ScenicService scenicService;
|
private ScenicService scenicService;
|
||||||
|
@Autowired
|
||||||
|
private MemberRelationRepository memberRelationRepository;
|
||||||
|
|
||||||
private IAcsClient getClient() {
|
private IAcsClient getClient() {
|
||||||
AliFaceBodyAdapter use = (AliFaceBodyAdapter) FaceBodyFactory.use();
|
AliFaceBodyAdapter use = (AliFaceBodyAdapter) FaceBodyFactory.use();
|
||||||
@@ -150,10 +154,34 @@ public class TaskFaceServiceImpl implements TaskFaceService {
|
|||||||
memberSourceEntity.setIsBuy(0);
|
memberSourceEntity.setIsBuy(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sourceMapper.addRelations(memberSourceEntityList);
|
// 过滤已存在的关联关系和无效的source引用,防止数据不一致
|
||||||
|
List<MemberSourceEntity> existingFiltered = sourceMapper.filterExistingRelations(memberSourceEntityList);
|
||||||
|
List<MemberSourceEntity> validFiltered = sourceMapper.filterValidSourceRelations(existingFiltered);
|
||||||
|
if (!validFiltered.isEmpty()) {
|
||||||
|
sourceMapper.addRelations(validFiltered);
|
||||||
|
log.debug("创建关联关系: faceId={}, 原始数量={}, 过滤后数量={}",
|
||||||
|
faceId, memberSourceEntityList.size(), validFiltered.size());
|
||||||
|
} else {
|
||||||
|
log.warn("没有有效的关联关系可创建: faceId={}, 原始数量={}", faceId, memberSourceEntityList.size());
|
||||||
|
}
|
||||||
|
memberRelationRepository.clearSCacheByFace(faceId);
|
||||||
|
List<FaceSampleEntity> faceSampleList = faceRepository.getFaceSampleList(faceId);
|
||||||
|
List<Long> faceSampleIds = faceSampleList.stream()
|
||||||
|
.sorted(Comparator.comparing(FaceSampleEntity::getCreateAt).reversed())
|
||||||
|
.collect(Collectors.groupingBy(FaceSampleEntity::getDeviceId))
|
||||||
|
.entrySet()
|
||||||
|
.stream().flatMap(entry -> {
|
||||||
|
DeviceConfigManager configManager = deviceRepository.getDeviceConfigManager(entry.getKey());
|
||||||
|
if (configManager.getInteger("limit_video", 0) > 0) {
|
||||||
|
return entry.getValue().subList(0, Math.min(entry.getValue().size(), configManager.getInteger("limit_video", 0))).stream();
|
||||||
|
}
|
||||||
|
return entry.getValue().stream();
|
||||||
|
}).toList()
|
||||||
|
.stream().map(FaceSampleEntity::getId).toList();
|
||||||
|
log.info("视频切分任务: faceId={}, 原始数量={}, 筛选后数量={}", faceId, sampleListIds.size(), faceSampleIds.size());
|
||||||
VideoPieceGetter.Task task = new VideoPieceGetter.Task();
|
VideoPieceGetter.Task task = new VideoPieceGetter.Task();
|
||||||
task.faceId = faceEntity.getId();
|
task.faceId = faceEntity.getId();
|
||||||
task.faceSampleIds = sampleListIds;
|
task.faceSampleIds = faceSampleIds;
|
||||||
task.memberId = face.getMemberId();
|
task.memberId = face.getMemberId();
|
||||||
VideoPieceGetter.addTask(task);
|
VideoPieceGetter.addTask(task);
|
||||||
}
|
}
|
||||||
@@ -224,11 +252,18 @@ public class TaskFaceServiceImpl implements TaskFaceService {
|
|||||||
.anyMatch(record -> record.getScore() > _lowThreshold);
|
.anyMatch(record -> record.getScore() > _lowThreshold);
|
||||||
respVo.setLowThreshold(isLowThreshold);
|
respVo.setLowThreshold(isLowThreshold);
|
||||||
allFaceSampleIds = records.stream()
|
allFaceSampleIds = records.stream()
|
||||||
|
.sorted(Comparator.comparing(SearchFaceResultItem::getScore).reversed())
|
||||||
.map(SearchFaceResultItem::getExtData)
|
.map(SearchFaceResultItem::getExtData)
|
||||||
.filter(StringUtils::isNumeric)
|
.filter(StringUtils::isNumeric)
|
||||||
.map(Long::valueOf)
|
.map(Long::valueOf)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
List<FaceSampleEntity> allFaceSampleList = faceSampleMapper.listByIds(allFaceSampleIds);
|
List<FaceSampleEntity> allFaceSampleList = faceSampleMapper.listByIds(allFaceSampleIds);
|
||||||
|
// 按照allFaceSampleIds的顺序对allFaceSampleList进行排序
|
||||||
|
Map<Long, Integer> idIndexMap = new HashMap<>();
|
||||||
|
for (int i = 0; i < allFaceSampleIds.size(); i++) {
|
||||||
|
idIndexMap.put(allFaceSampleIds.get(i), i);
|
||||||
|
}
|
||||||
|
allFaceSampleList.sort(Comparator.comparing(sample -> idIndexMap.get(sample.getId())));
|
||||||
acceptFaceSampleIds = applySampleFilters(acceptFaceSampleIds, allFaceSampleList, scenicConfig);
|
acceptFaceSampleIds = applySampleFilters(acceptFaceSampleIds, allFaceSampleList, scenicConfig);
|
||||||
List<MatchLocalRecord> collect = new ArrayList<>();
|
List<MatchLocalRecord> collect = new ArrayList<>();
|
||||||
for (SearchFaceResultItem item : records) {
|
for (SearchFaceResultItem item : records) {
|
||||||
@@ -488,9 +523,8 @@ public class TaskFaceServiceImpl implements TaskFaceService {
|
|||||||
log.debug("设备照片限制:设备ID={}, 无限制,保留{}张照片",
|
log.debug("设备照片限制:设备ID={}, 无限制,保留{}张照片",
|
||||||
deviceId, deviceSampleIds.size());
|
deviceId, deviceSampleIds.size());
|
||||||
} else {
|
} else {
|
||||||
// 按创建时间倒序排序,取前N张
|
// 取前N张
|
||||||
List<FaceSampleEntity> limitedSamples = deviceSamples.stream()
|
List<FaceSampleEntity> limitedSamples = deviceSamples.stream()
|
||||||
.sorted(Comparator.comparing(FaceSampleEntity::getCreateAt).reversed())
|
|
||||||
.limit(limitPhoto)
|
.limit(limitPhoto)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user