You've already forked FrameTour-BE
Compare commits
561 Commits
8745cde2fb
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 5cc32ddf61 | |||
| 07987835ec | |||
| 0a3f4119d7 | |||
| 51c7de2474 | |||
| 773d7f2254 | |||
| af131131ed | |||
| 3f6f1508c5 | |||
| dbee1d9709 | |||
| 83d1096fdb | |||
| 82925d203c | |||
| 3b11ddef6a | |||
| 6e7b4729a8 | |||
| 917cb37ccf | |||
| 7c0a3a63bb | |||
| 478467e124 | |||
| d5befd75e1 | |||
| b2c55c9feb | |||
| fef616c837 | |||
| a5fe00052d | |||
| 349b702fc3 | |||
| 9f5a61247b | |||
| 9321422e56 | |||
| 1834fe3ddd | |||
| fa8f92d38b | |||
| df33e7929f | |||
| 554f55a7c1 | |||
| f71149fd06 | |||
| e8eb8d816b | |||
| 576d87d113 | |||
| a2378053a8 | |||
| c92ea20575 | |||
| bb71cf9458 | |||
| 7749faf807 | |||
| c42b055d5f | |||
| fe3bda28b4 | |||
| 66775ea48b | |||
| 125fadd6c5 | |||
| 1f4a16f0e6 | |||
| e9916d6aca | |||
| b71452b3ed | |||
| 4a82ee6c4d | |||
| 24bbb63bf7 | |||
| ee13ef09f7 | |||
| 33c3a194ca | |||
| 71a8d3b539 | |||
| 82626f615b | |||
| de2eadf764 | |||
| fd143830d3 | |||
| 68916f3f53 | |||
| e27ed7d971 | |||
| 7a19f18962 | |||
| eade5f8092 | |||
| 42540e2dc4 | |||
| 15dda645b9 | |||
| 17419d83e7 | |||
| ae92ba10a7 | |||
| af60cc1540 | |||
| 60b4473279 | |||
| ecd5378b26 | |||
| 8c08c8947e | |||
| b165840176 | |||
| 71d6400a1e | |||
| b3fa10e8fd | |||
| 96e75a458f | |||
| d2ad14175d | |||
| 06c0ade9b4 | |||
| 36f85dbb63 | |||
| 9becd6bfa7 | |||
| 788184fc04 | |||
| 3cf7c81162 | |||
| 88d9463e25 | |||
| 590a7c6191 | |||
| d590286b13 | |||
| b92568b842 | |||
| 1de760fc87 | |||
| 4e9aac4cf3 | |||
| aa43d14316 | |||
| a2d87e7fdc | |||
| 57be6aa983 | |||
| cacb22a7bd | |||
| 300edbe582 | |||
| 9219ea4ab0 | |||
| e292a0798d | |||
| 4244b42d4b | |||
| 8058bc21f5 | |||
| 6dd08ac4e7 | |||
| 610a183be1 | |||
| e9a59cd466 | |||
| d60d7d9ad8 | |||
| d483c222d0 | |||
| a7ef2cb35a | |||
| cbc0584706 | |||
| 67932c374b | |||
| 8a88c74df2 | |||
| 3ce3972875 | |||
| 1945639f90 | |||
| 40d5874560 | |||
| 95419fee66 | |||
| 333c4d3ca7 | |||
| 90efc908c5 | |||
| d2846e6d8e | |||
| 7b18d7c2af | |||
| bcebe5defe | |||
| 4a86849372 | |||
| e418a5ccdb | |||
| 4360ef1313 | |||
| 9278d4479f | |||
| 18bf51487d | |||
| 447e8799e8 | |||
| fd130c471f | |||
| c47c24a39a | |||
| 97e3ab19a0 | |||
| 5b27cac6b0 | |||
| 91f3632e2b | |||
| cd8ae491e2 | |||
| d0d238d31d | |||
| 2be30c6eb4 | |||
| fb82329a88 | |||
| 4f0d6dc44f | |||
| 302b6811c4 | |||
| c0daa4d3b2 | |||
| 83cfbc67e1 | |||
| 8f918570d9 | |||
| f4a3dc9cae | |||
| cd5ba23d59 | |||
| 038b2e6f08 | |||
| caad0c2cf0 | |||
| 259d99bde7 | |||
| 0e2122910f | |||
| e1a77a1614 | |||
| 8791cf5910 | |||
| a860319ea1 | |||
| d5fc5c2565 | |||
| 0db713b4a8 | |||
| 6ef710201c | |||
| 9123a1f6db | |||
| d458f918ed | |||
| 27e58d36d0 | |||
| 8c76c85ae2 | |||
| 8991d68673 | |||
| 3b93e07a66 | |||
| c8054c60ab | |||
| 2fd852c5c6 | |||
| aaa8d8310a | |||
| 8d2d0901fd | |||
| d1381c93b0 | |||
| 536f2866f6 | |||
| 4cbd0dc255 | |||
| 90cf0d44c9 | |||
| d387f11173 | |||
| f6d6a63977 | |||
| 67aebd5770 | |||
| 6d18a770b8 | |||
| b6cbb18a7f | |||
| cfb3625ac0 | |||
| cb17ea527b | |||
| 625ad910c9 | |||
| 778afaaa83 | |||
| de421cf0d5 | |||
| 3ddf7bd0e9 | |||
| 208202ba41 | |||
| 6e84a5fd43 | |||
| 8e48bd92cc | |||
| 23181e9f08 | |||
| 42e806df76 | |||
| a49e581915 | |||
| af60e95529 | |||
| bb2367c5a6 | |||
| 3d361200b0 | |||
| 5c49a5af9e | |||
| a5ffb86790 | |||
| 755ba1153e | |||
| ebf05ab189 | |||
| e2b450682b | |||
| 443f92ff92 | |||
| 630d344b5a | |||
| 9eb3fd3e58 | |||
| 3463dcc9ae | |||
| 1b9bebf7e4 | |||
| fa287f36ae | |||
| 0c3ada97f9 | |||
| a7a7e30364 | |||
| 17a33ada9f | |||
| d408c47963 | |||
| 8efd16ba56 | |||
| 9d708ae20c | |||
| 7c906d5529 | |||
| 88ad6d6b6f | |||
| 7835283f0f | |||
| 9ee466bd5e | |||
| 1a25848102 | |||
| c319398c58 | |||
| afc589bb39 | |||
| 4ccb563557 | |||
| 11face7935 | |||
| 515f68a6f4 | |||
| 3f396b4cb8 | |||
| a1b0687526 | |||
| 932081abf0 | |||
| 6462037dcd | |||
| 9b9e69cf52 | |||
| 6246d6ef46 | |||
| 19fae4bd00 | |||
| 661aa4567f | |||
| ec34437e9d | |||
| dde9f5d542 | |||
| 72e60c95e0 | |||
| 9df30a84e0 | |||
| acfaebfffa | |||
| 72e215c552 | |||
| ee5cc81864 | |||
| 1bbfe8d092 | |||
| 88c31d4fdc | |||
| fb75cbf230 | |||
| eda4ed2955 | |||
| c41611e5d0 | |||
| 747081901f | |||
| 616ab217e4 | |||
| 00db16e7bd | |||
| 1821ba9f58 | |||
| ea48f03bbc | |||
| 6b2089a9bc | |||
| 25c0e629c6 | |||
| 48c8518ae6 | |||
| 96d001dfc0 | |||
| fb3a08fdcf | |||
| dcc8cdeb6a | |||
| 054958ebf5 | |||
| fa0c3a1a43 | |||
| 779334a09e | |||
| 3a3bdee296 | |||
| 546ddfbb62 | |||
| 58b642f356 | |||
| fa8a8ed711 | |||
| 6886f87fe9 | |||
| 78a2a74fa6 | |||
| 222f974ad5 | |||
| 96a4d3ffeb | |||
| e99d75ba1b | |||
| c1b9a42c73 | |||
| 4c10c1d939 | |||
| 3000e18cb7 | |||
| bf014db7ff | |||
| 12cd9bd275 | |||
| 7c42c5c462 | |||
| 631d5c175f | |||
| 785de52780 | |||
| 2ee7e93201 | |||
| 65ae23a956 | |||
| b9ade63e8e | |||
| cdeb2e4d5a | |||
| cc38d6e095 | |||
| 82d86c8c3c | |||
| 5979b1a275 | |||
| a7fe0d715d | |||
| ef8a913636 | |||
| 73791a92d3 | |||
| f0ad0f58a9 | |||
| 73825cd1d6 | |||
| 745943fc23 | |||
| b6bde4ad62 | |||
| 07ebccad3c | |||
| 028178605e | |||
| 03162dec44 | |||
| 85cdfe9ea1 | |||
| 5e2fe0329d | |||
| 6f8b3c8cdf | |||
| 1efe4a1439 | |||
| e27f092f85 | |||
| 215a7e87ae | |||
| 636ab96e96 | |||
| cc68a8dbbd | |||
| e887fd47f2 | |||
| f07d808f3d | |||
| 2a662ae86d | |||
| e805fdac9a | |||
| 0db411c2e4 | |||
| 27930b1dca | |||
| b3e2601758 | |||
| d9049b8a29 | |||
| 0f0601e5eb | |||
| 0a57eeaeef | |||
| fb637bc7db | |||
| ca2b812574 | |||
| 841c89af04 | |||
| bed3a4e3c9 | |||
| 3f8b911e6f | |||
| ea4adcdeb7 | |||
| 6242a346ce | |||
| 2d2ed6fc1b | |||
| 1b312313b2 | |||
| 37033f1b16 | |||
| 7ea78e4a17 | |||
| 9fcb472717 | |||
| 70ef4eb09c | |||
| 58224a03c5 | |||
| 927c4955f8 | |||
| 27f356519e | |||
| c80086ba69 | |||
| 86d5f8ceb1 | |||
| ff708a3fc3 | |||
| 44b20890d5 | |||
| 19ca91778f | |||
| e5a58ae757 | |||
| 602eb84f9c | |||
| 8d24e7f9e5 | |||
| 8eed685373 | |||
| 2b79302874 | |||
| 56e1081304 | |||
| 658e741611 | |||
| d5cd1924f5 | |||
| 645afbaf0c | |||
| 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 | |||
| 0432b99524 | |||
| da71e6d16f | |||
| f29217ac1f | |||
| 638da8cd3d | |||
| 7ca59a1b0b | |||
| f10ede0d2c | |||
| 9226dfff1d | |||
| 67f5c274f7 | |||
| ff8fe33eb0 | |||
| 292157885a | |||
| ad33b1abef | |||
| 524627ea73 | |||
| 7a35551a7b | |||
| 7820a282d9 | |||
| 864fc2c127 | |||
| ab7deb5c8f | |||
| 079c5dc540 | |||
| 1220348bae | |||
| e9102e8e58 | |||
| e86dc85afe | |||
| b14754ec0a | |||
| a888ed3fe2 | |||
| fde4deb370 | |||
| 5212547b3a | |||
| 9a39592a98 | |||
| f3fdb44742 | |||
| ad111cdebb | |||
| 1c0c0393aa | |||
| 04f7c79679 | |||
| 6d3fecc1c8 | |||
| 5626498002 | |||
| 8975ce404c | |||
| 2a8bdaec28 | |||
| b323450708 | |||
| a5e882e693 | |||
| a2348e3692 | |||
| 6006fe460c | |||
| 1506ae95b8 | |||
| 8380b02fbb | |||
| c6681a249e | |||
| 90a21c0933 | |||
| 57266eb535 | |||
| 7cfcc44531 | |||
| 2f51470d43 | |||
| a61ecf7646 | |||
| 221f0175e6 | |||
| dcd5a8f930 | |||
| ce3f7aae1e | |||
| cce0b45e70 | |||
| f8c7cc2db6 | |||
| 4b58c03ad2 | |||
| 8ed38bd229 | |||
| ccddab37ea | |||
| 8c37f2bf2f | |||
| 89a2e19419 | |||
| 63c2fdfece | |||
| 048780071b | |||
| c5f7003077 | |||
| 5531c576e0 | |||
| e43809593b | |||
| bf672a8af7 | |||
| 91e68c3272 | |||
| 96c56bd8c1 | |||
| be2750c162 | |||
| b5b2c12a15 | |||
| dc2154c020 | |||
| 2f88699bb0 | |||
| c194c169be | |||
| 39bd18497c | |||
| 1d666c076e | |||
| 88974d7e9e | |||
| a956c54500 | |||
| a7e5c8cd95 | |||
| eaf959e1b8 | |||
| 0c56a7fa67 | |||
| 7839082352 | |||
| 32b5b39ea3 | |||
| 670e37e7a6 | |||
| 13640c88d3 | |||
| ac91921c28 | |||
| 9e9e245801 | |||
| 9587354d0a | |||
| 3697093bed | |||
| 1e3d2e9e3a | |||
| c593e0c7e9 | |||
| a641acee88 | |||
| ef61ce9d63 | |||
| aa683a62c4 | |||
| 5426f61328 | |||
| 9ec222a155 | |||
| a8711f6d19 | |||
| 7bb2905462 | |||
| fd4c708406 | |||
| 8504e29c80 | |||
| 948bd0d2e2 | |||
| ad0888ccc7 | |||
| 48eff52a47 | |||
| 7d171b5003 | |||
| bfe84546c6 | |||
| c28efbbb9c | |||
| 6fb3cb93a9 | |||
| 1fbe7c86d5 | |||
| c1ca4e8631 | |||
| c1d61f4ed5 | |||
| c3101ceb6b | |||
| 29637bc5e5 | |||
| 4ee79b5db8 | |||
| 502eca10f6 | |||
| e574f49177 | |||
| 7696c934b1 | |||
| c4acdc576a | |||
| 90dc7fea70 | |||
| e9e59cd33e | |||
| 5a89a7c60a | |||
| 24f692b69a | |||
| d9a2da49bb | |||
| 0aa834bdfa | |||
| b9c65cf030 | |||
| 58488d2cde | |||
| 32f7660dc0 | |||
| 180f89042c | |||
| a49450b795 | |||
| d7c6ce9f40 | |||
| ffad1c9f59 | |||
| 6039f337cb | |||
| c9f7080615 | |||
| 7b22baeb66 | |||
| 13bd60f24b | |||
| 1b1e5f1690 | |||
| b2a95ed862 | |||
| 933818d458 | |||
| 933a1209e7 | |||
| aa4a6c29c6 | |||
| 60ce65f3e4 | |||
| 50c84ac1c9 | |||
| 5210b50adb | |||
| bd077b9252 | |||
| 0a13bd8b12 | |||
| 5b757eda8d | |||
| 1cce9168c1 | |||
| b6efe7b7da | |||
| dbe0447987 | |||
| 4427c7fde1 | |||
| 480e40d78c | |||
| 4d70c33650 | |||
| 3b8a33c8eb | |||
| 8ad999f779 | |||
| bbfc61d75a | |||
| 7779b84c81 | |||
| 9a086fc86d | |||
| 657eb482bb | |||
| 7d2666128a | |||
| 5c416f6c09 | |||
| 829ab50b03 | |||
| 03fd80a313 | |||
| cf829bec2f | |||
| cbeaee751f | |||
| bea5255927 | |||
| 982e9180f1 | |||
| 0ff0b75910 | |||
| aa717d0c2a | |||
| 3efad90750 | |||
| 35b2e7c655 | |||
| 25b912c7af | |||
| 8e770a5b97 | |||
| 2dee78247e | |||
| 8c8a6baa5e | |||
| d35a1facbd | |||
| dac3b8d847 | |||
| ec24464cba | |||
| b475e38018 | |||
| ad7d1042f4 | |||
| 0bcf2aaccf | |||
| 98e5c3dc39 | |||
| e4da509964 | |||
| 9426d9c712 | |||
| d34603062a |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
.idea/
|
||||
logs/
|
||||
target/
|
||||
|
||||
.serena
|
||||
.claude
|
||||
.vscode
|
||||
.vscode
|
||||
*.jpg
|
||||
|
||||
27
AGENTS.md
Normal file
27
AGENTS.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## 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 all 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).
|
||||
- Run single test: `mvn -DskipTests=false test -Dtest=ClassNameTest` (after removing testExcludes from maven-compiler-plugin).
|
||||
|
||||
## Code Style Guidelines
|
||||
- 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.
|
||||
- Error handling: Use custom exceptions in `exception` package; proper logging with SLF4J.
|
||||
- Testing: Spring Boot testing + JUnit; test names end with `Test` or `Tests` and mirror package structure.
|
||||
|
||||
## Project Structure
|
||||
- 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).
|
||||
|
||||
## 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.
|
||||
209
CLAUDE.md
209
CLAUDE.md
@@ -1,190 +1,27 @@
|
||||
# CLAUDE.md
|
||||
# Repository Guidelines
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
## 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 all 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).
|
||||
- Run single test: `mvn -DskipTests=false test -Dtest=ClassNameTest` (after removing testExcludes from maven-compiler-plugin).
|
||||
|
||||
## 构建和开发命令
|
||||
## Code Style Guidelines
|
||||
- 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.
|
||||
- Error handling: Use custom exceptions in `exception` package; proper logging with SLF4J.
|
||||
- Testing: Spring Boot testing + JUnit; test names end with `Test` or `Tests` and mirror package structure.
|
||||
|
||||
### 构建应用程序
|
||||
```bash
|
||||
# 清理构建(默认跳过测试)
|
||||
mvn clean package
|
||||
## Project Structure
|
||||
- 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).
|
||||
|
||||
# 清理构建并执行测试
|
||||
mvn clean package -DskipTests=false
|
||||
|
||||
# 运行应用程序
|
||||
mvn spring-boot:run
|
||||
```
|
||||
|
||||
### 测试命令
|
||||
```bash
|
||||
# 运行特定测试类
|
||||
mvn test -Dtest=FaceCleanerTest
|
||||
|
||||
# 运行特定包的测试
|
||||
mvn test -Dtest="com.ycwl.basic.storage.adapters.*Test"
|
||||
|
||||
# 运行所有测试
|
||||
mvn test -DskipTests=false
|
||||
```
|
||||
|
||||
### 开发环境配置
|
||||
应用程序使用 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. 遵循现有的错误处理和日志记录模式
|
||||
|
||||
## 价格查询系统 (Pricing Module)
|
||||
|
||||
### 核心架构
|
||||
价格查询系统是一个独立的业务模块,位于 `com.ycwl.basic.pricing` 包中,提供商品定价、优惠券管理和价格计算功能。
|
||||
|
||||
#### 关键组件
|
||||
- **PriceCalculationController** (`/api/pricing/calculate`):价格计算API
|
||||
- **CouponManagementController** (`/api/pricing/admin/coupons/`):优惠券管理API
|
||||
- **PricingConfigController** (`/api/pricing/config/`):价格配置管理API
|
||||
|
||||
#### 商品类型支持
|
||||
```java
|
||||
ProductType枚举定义了支持的商品类型:
|
||||
- VLOG_VIDEO: Vlog视频
|
||||
- RECORDING_SET: 录像集
|
||||
- PHOTO_SET: 照相集
|
||||
- PHOTO_PRINT: 照片打印
|
||||
- MACHINE_PRINT: 一体机打印
|
||||
```
|
||||
|
||||
#### 价格计算流程
|
||||
1. 接收PriceCalculationRequest(包含商品列表和用户ID)
|
||||
2. 查找商品基础配置和分层定价
|
||||
3. 处理套餐商品(BundleProductItem)
|
||||
4. 自动应用最优优惠券
|
||||
5. 返回PriceCalculationResult(包含原价、最终价格、优惠详情)
|
||||
|
||||
#### 优惠券系统
|
||||
- **CouponType**: PERCENTAGE(百分比)、FIXED_AMOUNT(固定金额)
|
||||
- **CouponStatus**: CLAIMED(已领取)、USED(已使用)、EXPIRED(已过期)
|
||||
- 支持商品类型限制 (`applicableProducts` JSON字段)
|
||||
- 最小消费金额和最大折扣限制
|
||||
- 时间有效期控制
|
||||
|
||||
#### 分页查询功能
|
||||
所有管理接口都支持分页查询,使用PageHelper实现:
|
||||
- 优惠券配置分页:支持按状态、名称筛选
|
||||
- 领取记录分页:支持按用户、优惠券、状态、时间范围筛选
|
||||
|
||||
#### 统计功能
|
||||
- 基础统计:领取数、使用数、可用数
|
||||
- 详细统计:使用率、平均使用天数
|
||||
- 时间范围统计:指定时间段的整体数据分析
|
||||
|
||||
### 开发模式
|
||||
|
||||
#### 添加新商品类型
|
||||
1. 在ProductType枚举中添加新类型
|
||||
2. 在PriceProductConfig表中配置default配置
|
||||
3. 根据需要添加分层定价(PriceTierConfig)
|
||||
4. 更新前端产品类型映射
|
||||
|
||||
#### 添加新优惠券类型
|
||||
1. 在CouponType枚举中添加类型
|
||||
2. 在CouponServiceImpl中实现计算逻辑
|
||||
3. 更新applicableProducts验证规则
|
||||
|
||||
#### 自定义TypeHandler使用
|
||||
项目使用自定义TypeHandler处理复杂JSON字段:
|
||||
- `BundleProductListTypeHandler`:处理套餐商品列表JSON序列化
|
||||
|
||||
### 测试策略
|
||||
- 单元测试:每个服务类都有对应测试类
|
||||
- 配置验证测试:DefaultConfigValidationTest验证default配置
|
||||
- JSON序列化测试:验证复杂对象的数据库存储
|
||||
- 分页功能测试:验证PageHelper集成
|
||||
## 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.
|
||||
|
||||
52
pom.xml
52
pom.xml
@@ -75,6 +75,12 @@
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- WebSocket -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Nacos服务发现 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
@@ -266,6 +272,31 @@
|
||||
<artifactId>mts20140618</artifactId>
|
||||
<version>5.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 智谱AI SDK -->
|
||||
<dependency>
|
||||
<groupId>ai.z.openapi</groupId>
|
||||
<artifactId>zai-sdk</artifactId>
|
||||
<version>0.1.3</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Kafka -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.kafka</groupId>
|
||||
<artifactId>spring-kafka</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Apache POI - 处理Excel文件 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi</artifactId>
|
||||
<version>5.4.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi-ooxml</artifactId>
|
||||
<version>5.4.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
@@ -293,16 +324,6 @@
|
||||
<skip>${skipTests}</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<!-- 跳过测试编译 -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<testExcludes>
|
||||
<testExclude>**/*Test.java</testExclude>
|
||||
</testExcludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
@@ -325,17 +346,6 @@
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>sonatype-nexus-staging</id>
|
||||
<name>Sonatype Nexus Staging</name>
|
||||
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.ycwl.basic;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||
@@ -9,8 +8,6 @@ import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
@SpringBootApplication
|
||||
@EnableDiscoveryClient
|
||||
@EnableFeignClients
|
||||
@MapperScan(basePackages = "com.ycwl.basic.mapper")
|
||||
@MapperScan(basePackages = "com.ycwl.basic.*.mapper")
|
||||
public class Application {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package com.ycwl.basic.biz;
|
||||
|
||||
import com.ycwl.basic.enums.StatisticEnum;
|
||||
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
|
||||
import com.ycwl.basic.mapper.OrderMapper;
|
||||
import com.ycwl.basic.mapper.SourceMapper;
|
||||
import com.ycwl.basic.mapper.StatisticsMapper;
|
||||
import com.ycwl.basic.mapper.VideoMapper;
|
||||
import com.ycwl.basic.model.mobile.order.IsBuyBatchRespVO;
|
||||
import com.ycwl.basic.model.mobile.order.IsBuyRespVO;
|
||||
import com.ycwl.basic.model.mobile.order.PriceObj;
|
||||
import com.ycwl.basic.model.mobile.statistic.req.StatisticsRecordAddReq;
|
||||
@@ -13,17 +15,13 @@ import com.ycwl.basic.model.pc.couponRecord.resp.CouponRecordQueryResp;
|
||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||
import com.ycwl.basic.model.pc.order.entity.OrderEntity;
|
||||
import com.ycwl.basic.model.pc.order.entity.OrderItemEntity;
|
||||
import com.ycwl.basic.model.pc.order.req.OrderUpdateReq;
|
||||
import com.ycwl.basic.model.pc.order.resp.OrderAppRespVO;
|
||||
import com.ycwl.basic.model.pc.order.resp.OrderItemVO;
|
||||
import com.ycwl.basic.model.pc.order.resp.OrderRespVO;
|
||||
import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity;
|
||||
import com.ycwl.basic.model.pc.scenic.entity.ScenicEntity;
|
||||
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
|
||||
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.VideoEntity;
|
||||
import com.ycwl.basic.model.pc.video.resp.VideoRespVO;
|
||||
import com.ycwl.basic.pricing.dto.PriceCalculationRequest;
|
||||
import com.ycwl.basic.pricing.dto.PriceCalculationResult;
|
||||
import com.ycwl.basic.pricing.dto.ProductItem;
|
||||
@@ -38,13 +36,11 @@ import com.ycwl.basic.repository.TemplateRepository;
|
||||
import com.ycwl.basic.repository.VideoRepository;
|
||||
import com.ycwl.basic.repository.VideoTaskRepository;
|
||||
import com.ycwl.basic.service.printer.PrinterService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
@@ -92,7 +88,6 @@ public class OrderBiz {
|
||||
PriceObj priceObj = new PriceObj();
|
||||
priceObj.setGoodsType(goodsType);
|
||||
priceObj.setGoodsId(goodsId);
|
||||
ScenicEntity scenic = scenicRepository.getScenic(scenicId);
|
||||
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId);
|
||||
if (scenicConfig != null) {
|
||||
if (Boolean.TRUE.equals(scenicConfig.getAllFree())) {
|
||||
@@ -117,13 +112,19 @@ public class OrderBiz {
|
||||
if (template == null) {
|
||||
return priceObj;
|
||||
}
|
||||
priceObj.setPrice(template.getPrice());
|
||||
BigDecimal slashPrice = template.getSlashPrice();
|
||||
if (slashPrice == null) {
|
||||
priceObj.setSlashPrice(priceObj.getPrice());
|
||||
} else {
|
||||
priceObj.setSlashPrice(slashPrice);
|
||||
}
|
||||
PriceCalculationRequest vlogCalculationRequest = new PriceCalculationRequest();
|
||||
ProductItem vlogProductItem = new ProductItem();
|
||||
vlogProductItem.setProductType(ProductType.VLOG_VIDEO);
|
||||
vlogProductItem.setProductId(template.getId().toString());
|
||||
vlogProductItem.setQuantity(videoTaskRepository.getTaskLensNum(video.getTaskId()));
|
||||
vlogProductItem.setScenicId(scenicId.toString());
|
||||
vlogCalculationRequest.setProducts(Collections.singletonList(vlogProductItem));
|
||||
vlogCalculationRequest.setFaceId(priceObj.getFaceId());
|
||||
vlogCalculationRequest.setPreviewOnly(true); // 仅查询价格,不实际使用优惠
|
||||
PriceCalculationResult vlogCalculationResult = iPriceCalculationService.calculatePrice(vlogCalculationRequest);
|
||||
priceObj.setPrice(vlogCalculationResult.getFinalAmount());
|
||||
priceObj.setSlashPrice(vlogCalculationResult.getOriginalAmount());
|
||||
priceObj.setFaceId(goodsId);
|
||||
priceObj.setScenicId(video.getScenicId());
|
||||
break;
|
||||
case 1: // source
|
||||
@@ -132,19 +133,35 @@ public class OrderBiz {
|
||||
PriceCalculationRequest calculationRequest = new PriceCalculationRequest();
|
||||
ProductItem productItem = new ProductItem();
|
||||
productItem.setProductType(goodsType == 1 ? ProductType.RECORDING_SET : ProductType.PHOTO_SET);
|
||||
productItem.setProductId(scenic.getId().toString());
|
||||
productItem.setProductId(scenicId.toString());
|
||||
productItem.setPurchaseCount(1);
|
||||
productItem.setScenicId(scenic.getId().toString());
|
||||
productItem.setScenicId(scenicId.toString());
|
||||
calculationRequest.setProducts(Collections.singletonList(productItem));
|
||||
if (face != null) {
|
||||
calculationRequest.setUserId(face.getMemberId());
|
||||
}
|
||||
calculationRequest.setFaceId(goodsId);
|
||||
calculationRequest.setPreviewOnly(true); // 仅查询价格,不实际使用优惠
|
||||
PriceCalculationResult priceCalculationResult = iPriceCalculationService.calculatePrice(calculationRequest);
|
||||
priceObj.setPrice(priceCalculationResult.getFinalAmount());
|
||||
priceObj.setSlashPrice(priceCalculationResult.getOriginalAmount());
|
||||
priceObj.setFaceId(goodsId);
|
||||
break;
|
||||
case 13:
|
||||
PriceCalculationRequest aiCamCalculationRequest = new PriceCalculationRequest();
|
||||
ProductItem aiCamProductItem = new ProductItem();
|
||||
aiCamProductItem.setProductType(ProductType.AI_CAM_PHOTO_SET);
|
||||
aiCamProductItem.setProductId(scenicId.toString());
|
||||
aiCamProductItem.setPurchaseCount(1);
|
||||
aiCamProductItem.setScenicId(scenicId.toString());
|
||||
aiCamCalculationRequest.setProducts(Collections.singletonList(aiCamProductItem));
|
||||
aiCamCalculationRequest.setPreviewOnly(true); // 仅查询价格,不实际使用优惠
|
||||
PriceCalculationResult aiCamPriceCalculationResult = iPriceCalculationService.calculatePrice(aiCamCalculationRequest);
|
||||
priceObj.setPrice(aiCamPriceCalculationResult.getFinalAmount());
|
||||
priceObj.setSlashPrice(aiCamPriceCalculationResult.getOriginalAmount());
|
||||
priceObj.setFaceId(goodsId);
|
||||
priceObj.setScenicId(scenicId);
|
||||
break;
|
||||
}
|
||||
return priceObj;
|
||||
}
|
||||
@@ -160,85 +177,46 @@ public class OrderBiz {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public IsBuyRespVO isBuy(Long userId, Long scenicId, int goodsType, Long goodsId) {
|
||||
public IsBuyRespVO isBuy(Long scenicId, Long memberId, Long faceId, int goodsType, Long goodsId) {
|
||||
IsBuyRespVO respVO = new IsBuyRespVO();
|
||||
boolean isBuy = orderRepository.checkUserBuyItem(userId, goodsType, goodsId);
|
||||
// 模板购买逻辑
|
||||
if (!isBuy) {
|
||||
if (goodsType == 0) {
|
||||
VideoEntity video = videoRepository.getVideo(goodsId);
|
||||
if (video == null) {
|
||||
respVO.setGoodsType(goodsType);
|
||||
respVO.setGoodsId(goodsId);
|
||||
OrderEntity orderEntity = orderMapper.getUserBuyFaceItem(memberId, faceId, goodsType, goodsId);
|
||||
if (orderEntity != null) {
|
||||
respVO.setOrderId(orderEntity.getId());
|
||||
respVO.setBuy(true);
|
||||
respVO.setFree(false);
|
||||
return respVO;
|
||||
}
|
||||
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(scenicId);
|
||||
if (Boolean.TRUE.equals(scenicConfig.getBoolean("all_free"))) {
|
||||
// 景区全免
|
||||
respVO.setFree(true);
|
||||
respVO.setOrigPrice(BigDecimal.ZERO);
|
||||
respVO.setSlashPrice(BigDecimal.ZERO);
|
||||
return respVO;
|
||||
}
|
||||
// 未来模板一口价
|
||||
if (goodsType == 0) {
|
||||
// 视频,可以买断模板
|
||||
VideoEntity video = videoRepository.getVideo(goodsId);
|
||||
if (video != null && video.getTemplateId() != null) {
|
||||
OrderEntity templateBuy = orderMapper.getUserBuyFaceItem(memberId, faceId, -1, video.getTemplateId());
|
||||
if (templateBuy != null) {
|
||||
respVO.setOrderId(templateBuy.getId());
|
||||
respVO.setBuy(true);
|
||||
respVO.setFree(false);
|
||||
return respVO;
|
||||
}
|
||||
TaskEntity task = videoTaskRepository.getTaskById(video.getTaskId());
|
||||
Long templateId = video.getTemplateId();
|
||||
// -1为整个模板购买
|
||||
OrderEntity orderEntity = orderRepository.getUserBuyItem(userId, -1, templateId);
|
||||
if (orderEntity != null && task != null) {
|
||||
respVO.setOrderId(orderEntity.getId());
|
||||
if (orderEntity.getFaceId() != null && task.getFaceId() != null) {
|
||||
isBuy = orderEntity.getFaceId().equals(task.getFaceId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 免费送逻辑,之前已经赠送了的
|
||||
if (!isBuy) {
|
||||
isBuy = switch (goodsType) {
|
||||
case 0 -> videoRepository.getUserIsBuy(userId, goodsId);
|
||||
case 1, 2 -> sourceRepository.getUserIsBuy(userId, goodsType, goodsId);
|
||||
default -> false;
|
||||
};
|
||||
} else {
|
||||
OrderEntity orderEntity = orderRepository.getUserBuyItem(userId, goodsType, goodsId);
|
||||
if (orderEntity != null) {
|
||||
respVO.setOrderId(orderEntity.getId());
|
||||
}
|
||||
}
|
||||
respVO.setBuy(isBuy);
|
||||
// 还是没买
|
||||
if (!isBuy) {
|
||||
PriceObj priceObj = queryPrice(scenicId, goodsType, goodsId);
|
||||
if (priceObj == null) {
|
||||
return respVO;
|
||||
}
|
||||
respVO.setFree(priceObj.isFree());
|
||||
respVO.setGoodsType(goodsType);
|
||||
respVO.setGoodsId(goodsId);
|
||||
respVO.setOrigPrice(priceObj.getPrice());
|
||||
respVO.setSlashPrice(priceObj.getSlashPrice());
|
||||
switch (goodsType) {
|
||||
case 0: // vlog
|
||||
VideoEntity video = videoRepository.getVideo(goodsId);
|
||||
TaskEntity taskById = videoTaskRepository.getTaskById(video.getTaskId());
|
||||
if (taskById != null) {
|
||||
CouponRecordQueryResp recordQueryResp = couponBiz.queryUserCouponRecord(scenicId, userId, taskById.getFaceId(), taskById.getTemplateId().toString());
|
||||
if (recordQueryResp.isUsable()) {
|
||||
respVO.setCouponId(recordQueryResp.getCouponId());
|
||||
respVO.setCouponRecordId(recordQueryResp.getId());
|
||||
CouponEntity coupon = recordQueryResp.getCoupon();
|
||||
if (coupon != null) {
|
||||
respVO.setCouponPrice(coupon.calculateDiscountPrice(priceObj.getPrice()));
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
case 2:
|
||||
CouponRecordQueryResp recordQueryResp = couponBiz.queryUserCouponRecord(scenicId, userId, goodsId, String.valueOf(goodsType));
|
||||
if (recordQueryResp.isUsable()) {
|
||||
respVO.setCouponId(recordQueryResp.getCouponId());
|
||||
respVO.setCouponRecordId(recordQueryResp.getId());
|
||||
CouponEntity coupon = recordQueryResp.getCoupon();
|
||||
if (coupon != null) {
|
||||
respVO.setCouponPrice(coupon.calculateDiscountPrice(priceObj.getPrice()));
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
PriceObj priceObj = queryPrice(scenicId, goodsType, goodsId);
|
||||
if (priceObj == null) {
|
||||
return respVO;
|
||||
}
|
||||
respVO.setBuy(false);
|
||||
respVO.setOrigPrice(priceObj.getPrice());
|
||||
respVO.setSlashPrice(priceObj.getSlashPrice());
|
||||
return respVO;
|
||||
}
|
||||
|
||||
@@ -252,13 +230,20 @@ public class OrderBiz {
|
||||
orderRepository.updateOrder(orderId, orderUpdate);
|
||||
orderItems.forEach(item -> {
|
||||
switch (item.getGoodsType()) {
|
||||
case -1: // vlog视频模板
|
||||
videoRepository.setUserIsBuyTemplate(order.getMemberId(), item.getGoodsId(), order.getId(), order.getFaceId());
|
||||
break;
|
||||
case 0: // vlog视频
|
||||
videoRepository.setUserIsBuyItem(order.getMemberId(), item.getGoodsId(), order.getId());
|
||||
break;
|
||||
case 1: // 视频原素材
|
||||
case 2: // 照片原素材
|
||||
case 13: // AI微单
|
||||
sourceRepository.setUserIsBuyItem(order.getMemberId(), item.getGoodsType(), item.getGoodsId(), order.getId());
|
||||
break;
|
||||
case 3:
|
||||
printerService.setUserIsBuyItem(order.getMemberId(), item.getGoodsId(), order.getId());
|
||||
break;
|
||||
}
|
||||
});
|
||||
orderRepository.clearOrderCache(orderId); // 更新完了,清理下
|
||||
@@ -266,38 +251,6 @@ public class OrderBiz {
|
||||
if (couponRecordId != null) {
|
||||
couponBiz.userUseCoupon(order.getMemberId(), order.getFaceId(), couponRecordId, orderId);
|
||||
}
|
||||
|
||||
//支付时间
|
||||
OrderAppRespVO orderDetail = orderMapper.appDetail(orderId);
|
||||
Date payAt = orderDetail.getPayAt();
|
||||
//商品创建时间
|
||||
Date goodsCreateTime = new Date();
|
||||
if (!orderDetail.getOrderItemList().isEmpty()) {
|
||||
OrderItemVO orderItemVO = orderDetail.getOrderItemList().getFirst();
|
||||
switch (orderItemVO.getGoodsType()) {
|
||||
case 0:
|
||||
VideoEntity video = videoRepository.getVideo(orderItemVO.getGoodsId());
|
||||
if (video != null) {
|
||||
goodsCreateTime = video.getCreateTime();
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
List<SourceEntity> imageSource = sourceMapper.listImageByFaceRelation(order.getMemberId(), orderItemVO.getGoodsId());
|
||||
Optional<SourceEntity> min = imageSource.stream().min(Comparator.comparing(SourceEntity::getCreateTime));
|
||||
if (min.isPresent()) {
|
||||
goodsCreateTime = min.get().getCreateTime();
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
List<SourceEntity> videoSource = sourceMapper.listImageByFaceRelation(order.getMemberId(), orderItemVO.getGoodsId());
|
||||
Optional<SourceEntity> minTime = videoSource.stream().min(Comparator.comparing(SourceEntity::getCreateTime));
|
||||
if (minTime.isPresent()) {
|
||||
goodsCreateTime = minTime.get().getCreateTime();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
StatisticsRecordAddReq statisticsRecordAddReq = new StatisticsRecordAddReq();
|
||||
statisticsRecordAddReq.setMemberId(order.getMemberId());
|
||||
Long enterType = statisticsMapper.getUserRecentEnterType(order.getMemberId(), order.getCreateAt());
|
||||
@@ -325,9 +278,11 @@ public class OrderBiz {
|
||||
switch (item.getGoodsType()) {
|
||||
case 0: // vlog视频
|
||||
videoRepository.setUserNotBuyItem(order.getMemberId(), item.getGoodsId());
|
||||
break;
|
||||
case 1: // 视频原素材
|
||||
case 2: // 照片原素材
|
||||
sourceRepository.setUserNotBuyItem(order.getMemberId(), item.getGoodsType(), item.getGoodsId());
|
||||
break;
|
||||
}
|
||||
});
|
||||
orderRepository.clearOrderCache(orderId); // 更新完了,清理下
|
||||
@@ -347,12 +302,26 @@ public class OrderBiz {
|
||||
switch (item.getGoodsType()) {
|
||||
case 0: // vlog视频
|
||||
videoRepository.setUserNotBuyItem(order.getMemberId(), item.getGoodsId());
|
||||
break;
|
||||
case 1: // 视频原素材
|
||||
case 2: // 照片原素材
|
||||
sourceRepository.setUserNotBuyItem(order.getMemberId(), item.getGoodsType(), item.getGoodsId());
|
||||
break;
|
||||
}
|
||||
});
|
||||
orderRepository.clearOrderCache(orderId); // 更新完了,清理下
|
||||
profitSharingBiz.revokeProfitSharing(order.getScenicId(), orderId, "订单已退款");
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否购买了指定商品,并额外校验订单的faceId是否匹配
|
||||
* @param userId 用户ID
|
||||
* @param faceId 人脸ID
|
||||
* @param goodsType 商品类型
|
||||
* @param goodsId 商品ID
|
||||
* @return 是否已购买且faceId匹配
|
||||
*/
|
||||
public boolean checkUserBuyFaceItem(Long userId, Long faceId, int goodsType, Long goodsId) {
|
||||
return orderRepository.checkUserBuyFaceItem(userId, faceId, goodsType, goodsId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,14 +7,23 @@ import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||
import com.ycwl.basic.model.pc.order.entity.OrderEntity;
|
||||
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.SimpleGoodsRespVO;
|
||||
import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity;
|
||||
import com.ycwl.basic.model.pc.template.resp.TemplateRespVO;
|
||||
import com.ycwl.basic.model.pc.video.entity.MemberVideoEntity;
|
||||
import com.ycwl.basic.product.capability.ProductTypeCapability;
|
||||
import com.ycwl.basic.product.service.IProductTypeCapabilityManagementService;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity;
|
||||
import com.ycwl.basic.puzzle.mapper.PuzzleTemplateMapper;
|
||||
import com.ycwl.basic.repository.FaceRepository;
|
||||
import com.ycwl.basic.repository.MemberRelationRepository;
|
||||
import com.ycwl.basic.repository.PriceRepository;
|
||||
import com.ycwl.basic.repository.ScenicRepository;
|
||||
import com.ycwl.basic.repository.TemplateRepository;
|
||||
import com.ycwl.basic.service.pc.FaceService;
|
||||
import org.apache.commons.lang3.Strings;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@@ -36,7 +45,16 @@ public class PriceBiz {
|
||||
@Autowired
|
||||
private FaceRepository faceRepository;
|
||||
@Autowired
|
||||
@Lazy
|
||||
private FaceService faceService;
|
||||
@Autowired
|
||||
private CouponBiz couponBiz;
|
||||
@Autowired
|
||||
private MemberRelationRepository memberRelationRepository;
|
||||
@Autowired
|
||||
private PuzzleTemplateMapper puzzleTemplateMapper;
|
||||
@Autowired
|
||||
private IProductTypeCapabilityManagementService productTypeCapabilityManagementService;
|
||||
|
||||
public List<GoodsListRespVO> listGoodsByScenic(Long scenicId) {
|
||||
List<GoodsListRespVO> goodsList = new ArrayList<>();
|
||||
@@ -46,17 +64,129 @@ public class PriceBiz {
|
||||
GoodsListRespVO goods = new GoodsListRespVO();
|
||||
goods.setGoodsId(template.getId());
|
||||
goods.setGoodsName(template.getName());
|
||||
goods.setGoodsType(0);
|
||||
return goods;
|
||||
}).forEach(goodsList::add);
|
||||
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId);
|
||||
if (scenicConfig != null) {
|
||||
if (!Boolean.TRUE.equals(scenicConfig.getDisableSourceVideo())) {
|
||||
goodsList.add(new GoodsListRespVO(1L, "录像集"));
|
||||
goodsList.add(new GoodsListRespVO(1L, "录像集", 1));
|
||||
}
|
||||
if (!Boolean.TRUE.equals(scenicConfig.getDisableSourceImage())) {
|
||||
goodsList.add(new GoodsListRespVO(2L, "照片集"));
|
||||
goodsList.add(new GoodsListRespVO(2L, "照片集", 2));
|
||||
}
|
||||
}
|
||||
// 拼图
|
||||
puzzleTemplateMapper.list(scenicId, null, 1).forEach(puzzleTemplate -> {
|
||||
GoodsListRespVO goods = new GoodsListRespVO();
|
||||
goods.setGoodsId(puzzleTemplate.getId());
|
||||
goods.setGoodsName(puzzleTemplate.getName());
|
||||
goods.setGoodsType(5);
|
||||
goodsList.add(goods);
|
||||
});
|
||||
return goodsList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据景区ID和商品类型查询简化的商品列表
|
||||
*
|
||||
* @param scenicId 景区ID
|
||||
* @param productType 商品类型(可选,为空时返回所有商品)
|
||||
* @return 简化的商品列表
|
||||
*/
|
||||
public List<SimpleGoodsRespVO> listSimpleGoodsByScenic(Long scenicId, String productType) {
|
||||
List<SimpleGoodsRespVO> goodsList = new ArrayList<>();
|
||||
|
||||
// 如果 productType 为空,兼容旧逻辑
|
||||
if (productType == null || productType.isEmpty()) {
|
||||
return listAllSimpleGoods(scenicId);
|
||||
}
|
||||
|
||||
// 根据 productType 查询不同数据源
|
||||
switch (productType) {
|
||||
case "VLOG_VIDEO":
|
||||
// 从 template 表查询视频模板
|
||||
List<TemplateRespVO> templateList = templateRepository.getTemplateListByScenicId(scenicId);
|
||||
templateList.stream()
|
||||
.map(template -> new SimpleGoodsRespVO(template.getId(), template.getName(), productType))
|
||||
.forEach(goodsList::add);
|
||||
break;
|
||||
|
||||
case "PHOTO_VLOG":
|
||||
// TODO
|
||||
goodsList.add(new SimpleGoodsRespVO(scenicId, "【待实现】pLog视频", productType));
|
||||
break;
|
||||
|
||||
case "PHOTO":
|
||||
goodsList.add(new SimpleGoodsRespVO(scenicId, "单张照片", productType));
|
||||
break;
|
||||
|
||||
case "PHOTO_SET":
|
||||
// 返回固定的照片集条目
|
||||
goodsList.add(new SimpleGoodsRespVO(scenicId, "照片集", productType));
|
||||
break;
|
||||
|
||||
case "AI_CAM_PHOTO_SET":
|
||||
// 返回固定的照片集条目
|
||||
goodsList.add(new SimpleGoodsRespVO(scenicId, "AI微单照片集", productType));
|
||||
break;
|
||||
|
||||
case "PHOTO_LOG":
|
||||
// 从 template 表查询pLog模板
|
||||
goodsList.add(new SimpleGoodsRespVO(scenicId, "pLog图<景区打包>", productType));
|
||||
List<PuzzleTemplateEntity> puzzleList = puzzleTemplateMapper.list(scenicId, null, null);
|
||||
puzzleList.stream()
|
||||
.map(template -> new SimpleGoodsRespVO(template.getId(), template.getName(), productType))
|
||||
.forEach(goodsList::add);
|
||||
break;
|
||||
|
||||
case "RECORDING_SET":
|
||||
// 返回固定的录像集条目
|
||||
goodsList.add(new SimpleGoodsRespVO(scenicId, "录像集", productType));
|
||||
break;
|
||||
|
||||
case "PHOTO_PRINT":
|
||||
// 打印类返回单一通用条目
|
||||
goodsList.add(new SimpleGoodsRespVO(scenicId, "照片打印", productType));
|
||||
break;
|
||||
case "PHOTO_PRINT_MU":
|
||||
// 打印类返回单一通用条目
|
||||
goodsList.add(new SimpleGoodsRespVO(scenicId, "手机照片打印", productType));
|
||||
break;
|
||||
case "PHOTO_PRINT_FX":
|
||||
// 打印类返回单一通用条目
|
||||
goodsList.add(new SimpleGoodsRespVO(scenicId, "效果图片打印", productType));
|
||||
break;
|
||||
case "MACHINE_PRINT":
|
||||
// 打印类返回单一通用条目
|
||||
goodsList.add(new SimpleGoodsRespVO(scenicId, "一体机打印", productType));
|
||||
break;
|
||||
|
||||
default:
|
||||
// 不支持的 productType,返回空列表
|
||||
break;
|
||||
}
|
||||
|
||||
return goodsList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容旧逻辑:返回所有商品
|
||||
* 通过查询系统中所有已知的 productType,将结果综合到一起
|
||||
*/
|
||||
private List<SimpleGoodsRespVO> listAllSimpleGoods(Long scenicId) {
|
||||
List<SimpleGoodsRespVO> goodsList = new ArrayList<>();
|
||||
|
||||
// 从 ProductTypeCapability 服务查询所有已知的商品类型(仅包含启用的)
|
||||
List<ProductTypeCapability> capabilities = productTypeCapabilityManagementService.queryAll(false);
|
||||
|
||||
// 轮询每个商品类型,获取对应的商品列表
|
||||
for (ProductTypeCapability capability : capabilities) {
|
||||
String productType = capability.getProductType();
|
||||
List<SimpleGoodsRespVO> typeGoodsList = listSimpleGoodsByScenic(scenicId, productType);
|
||||
goodsList.addAll(typeGoodsList);
|
||||
}
|
||||
|
||||
return goodsList;
|
||||
}
|
||||
|
||||
@@ -80,7 +210,7 @@ public class PriceBiz {
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public IsBuyBatchRespVO isBuy(Long userId, Long faceId, Long scenicId, Integer type, String goodsIds) {
|
||||
public IsBuyBatchRespVO isOnePriceBuy(Long userId, Long faceId, Long scenicId, Integer type, String goodsIds) {
|
||||
IsBuyBatchRespVO respVO = new IsBuyBatchRespVO();
|
||||
PriceConfigEntity priceConfig = priceRepository.getPriceConfigByScenicTypeGoods(scenicId, type, goodsIds);
|
||||
if (priceConfig == null) {
|
||||
@@ -138,6 +268,56 @@ public class PriceBiz {
|
||||
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.checkUserBuyFaceItem(userId, faceId, -1, videoEntities.getFirst().getVideoId());
|
||||
if (!hasPurchasedTemplate) {
|
||||
allContentsPurchased = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查源文件购买状态(录像集和照片集)
|
||||
if (allContentsPurchased) {
|
||||
if (scenicConfig != null) {
|
||||
// 检查录像集
|
||||
if (!Boolean.TRUE.equals(scenicConfig.getDisableSourceVideo())) {
|
||||
boolean hasPurchasedRecording = orderBiz.checkUserBuyFaceItem(userId, faceId, 1, faceId);
|
||||
if (!hasPurchasedRecording) {
|
||||
allContentsPurchased = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查照片集
|
||||
if (allContentsPurchased && !Boolean.TRUE.equals(scenicConfig.getDisableSourceImage())) {
|
||||
boolean hasPurchasedPhoto = orderBiz.checkUserBuyFaceItem(userId, faceId, 2, faceId);
|
||||
if (!hasPurchasedPhoto) {
|
||||
allContentsPurchased = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果所有内容都已购买,则认为已购买套餐
|
||||
if (allContentsPurchased) {
|
||||
respVO.setBuy(true);
|
||||
}
|
||||
}
|
||||
respVO.setShare(false);
|
||||
if (face == null || !face.getMemberId().equals(userId)) {
|
||||
respVO.setShare(true);
|
||||
}
|
||||
return respVO;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,11 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -29,8 +32,6 @@ public class TemplateBiz {
|
||||
private FaceRepository faceRepository;
|
||||
@Autowired
|
||||
private SourceMapper sourceMapper;
|
||||
@Autowired
|
||||
private SourceRepository sourceRepository;
|
||||
|
||||
public boolean determineTemplateCanGenerate(Long templateId, Long faceId) {
|
||||
return determineTemplateCanGenerate(templateId, faceId, true);
|
||||
@@ -53,6 +54,7 @@ public class TemplateBiz {
|
||||
if (scanSource) {
|
||||
List<SourceEntity> sourceEntities = sourceMapper.listVideoByScenicFaceRelation(face.getScenicId(), faceId);
|
||||
if (sourceEntities == null || sourceEntities.isEmpty()) {
|
||||
log.info("faceId:{} has no source", faceId);
|
||||
return false;
|
||||
}
|
||||
count = sourceEntities.stream()
|
||||
@@ -64,6 +66,7 @@ public class TemplateBiz {
|
||||
} else {
|
||||
List<FaceSampleEntity> faceSampleList = faceRepository.getFaceSampleList(faceId);
|
||||
if (faceSampleList == null || faceSampleList.isEmpty()) {
|
||||
log.info("faceId:{} has no faceSample", faceId);
|
||||
return false;
|
||||
}
|
||||
count = faceSampleList.stream()
|
||||
@@ -91,8 +94,8 @@ public class TemplateBiz {
|
||||
}
|
||||
if (minimalPlaceholderFill == null) {
|
||||
// 未开启
|
||||
log.info("模板:{},未配置最小自动生成功能,默认不生成", templateId);
|
||||
return false;
|
||||
log.info("模板:{},未配置最小自动生成功能,默认生成!", templateId);
|
||||
minimalPlaceholderFill = 1;
|
||||
}
|
||||
if (minimalPlaceholderFill <= 0) {
|
||||
return true;
|
||||
@@ -121,4 +124,97 @@ public class TemplateBiz {
|
||||
return count >= minimalPlaceholderFill;
|
||||
}
|
||||
|
||||
}
|
||||
public Map<String, List<SourceEntity>> filterTaskParams(Long templateId, Map<String, List<SourceEntity>> allTaskParams) {
|
||||
if (allTaskParams == null || allTaskParams.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
List<String> templatePlaceholders = templateRepository.getTemplatePlaceholder(templateId);
|
||||
if (templatePlaceholders == null || templatePlaceholders.isEmpty()) {
|
||||
log.info("filterTaskParams: templateId:{} has no placeholders", templateId);
|
||||
return Map.of();
|
||||
}
|
||||
TemplateConfigEntity templateConfig = templateRepository.getTemplateConfig(templateId);
|
||||
|
||||
// 统计每个 placeholder 在模板中出现的次数
|
||||
Map<String, Long> placeholderCounts = templatePlaceholders.stream()
|
||||
.collect(Collectors.groupingBy(
|
||||
placeholder -> placeholder,
|
||||
Collectors.counting()
|
||||
));
|
||||
|
||||
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()) {
|
||||
String placeholder = entry.getKey();
|
||||
Long requiredCount = entry.getValue();
|
||||
|
||||
if (placeholder.startsWith("P")) {
|
||||
// 图片源:占位符格式为 "P{deviceId}"
|
||||
String imageKey = placeholder;
|
||||
if (allTaskParams.containsKey(imageKey)) {
|
||||
List<SourceEntity> allSources = allTaskParams.get(imageKey);
|
||||
List<SourceEntity> selectedSources = selectSources(allSources, requiredCount.intValue(), allowDuplicate);
|
||||
if (!selectedSources.isEmpty()) {
|
||||
filteredParams.put(imageKey, selectedSources);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 视频源:占位符直接对应设备ID
|
||||
String videoKey = placeholder;
|
||||
if (allTaskParams.containsKey(videoKey)) {
|
||||
List<SourceEntity> allSources = allTaskParams.get(videoKey);
|
||||
List<SourceEntity> selectedSources = selectSources(allSources, requiredCount.intValue(), allowDuplicate);
|
||||
if (!selectedSources.isEmpty()) {
|
||||
filteredParams.put(videoKey, selectedSources);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("filterTaskParams: templateId:{}, original keys:{}, filtered keys:{}, placeholder counts:{}, allowDuplicate:{}",
|
||||
templateId, allTaskParams.keySet().size(), filteredParams.keySet().size(), placeholderCounts, allowDuplicate);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,26 @@
|
||||
package com.ycwl.basic.config;
|
||||
|
||||
import org.springframework.cache.annotation.CachingConfigurerSupport;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
|
||||
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.cache.RedisCacheConfiguration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* @author wenshijia
|
||||
* @date 2021年07月05日 18:34
|
||||
* 修改redis缓存序列化器
|
||||
*/
|
||||
@Configuration
|
||||
@EnableCaching
|
||||
public class CustomRedisCacheManager extends CachingConfigurerSupport {
|
||||
|
||||
@Bean
|
||||
public RedisCacheConfiguration redisCacheConfiguration() {
|
||||
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
|
||||
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig();
|
||||
configuration = configuration.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)).entryTtl(Duration.ofMinutes(1));
|
||||
return configuration;
|
||||
}
|
||||
public class CustomRedisCacheManager {
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* 处理redis连接工具显示redis key值显示乱码问题,value值没处理
|
||||
@@ -45,10 +37,23 @@ public class CustomRedisCacheManager extends CachingConfigurerSupport {
|
||||
|
||||
final StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
|
||||
redisTemplate.setKeySerializer(stringRedisSerializer);
|
||||
redisTemplate.setValueSerializer(stringRedisSerializer);
|
||||
|
||||
redisTemplate.setHashKeySerializer(stringRedisSerializer);
|
||||
|
||||
// Configure Jackson2JsonRedisSerializer with JavaTimeModule for value serialization
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
objectMapper.registerModule(new JavaTimeModule());
|
||||
|
||||
// Configure type handling to prevent ClassCastException
|
||||
PolymorphicTypeValidator typeValidator = BasicPolymorphicTypeValidator.builder()
|
||||
.allowIfBaseType(Object.class)
|
||||
.build();
|
||||
objectMapper.activateDefaultTyping(typeValidator, ObjectMapper.DefaultTyping.NON_FINAL);
|
||||
|
||||
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
|
||||
|
||||
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
|
||||
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
|
||||
|
||||
redisTemplate.afterPropertiesSet();
|
||||
return redisTemplate;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.ycwl.basic.config;
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@@ -11,6 +12,15 @@ import org.springframework.context.annotation.Configuration;
|
||||
* @date 2021年06月04日 9:42
|
||||
*/
|
||||
@Configuration
|
||||
@MapperScan(basePackages = {
|
||||
"com.ycwl.basic.mapper",
|
||||
"com.ycwl.basic.order.mapper",
|
||||
"com.ycwl.basic.pricing.mapper",
|
||||
"com.ycwl.basic.product.mapper",
|
||||
"com.ycwl.basic.profitsharing.mapper",
|
||||
"com.ycwl.basic.puzzle.mapper",
|
||||
"com.ycwl.basic.stats.mapper"
|
||||
})
|
||||
public class MybatisPlusPageConfig {
|
||||
|
||||
/* 旧版本配置
|
||||
|
||||
32
src/main/java/com/ycwl/basic/config/VideoUpdateConfig.java
Normal file
32
src/main/java/com/ycwl/basic/config/VideoUpdateConfig.java
Normal file
@@ -0,0 +1,32 @@
|
||||
package com.ycwl.basic.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 视频更新检查配置
|
||||
* @author Claude
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "video.update")
|
||||
public class VideoUpdateConfig {
|
||||
|
||||
/**
|
||||
* 是否将片段变化检测为新增
|
||||
* true: 任何变化都视为新增
|
||||
* false: 只有数量增加才视为新增
|
||||
*/
|
||||
private boolean detectChangesAsNew = true;
|
||||
|
||||
/**
|
||||
* 最小新增片段数量才认为可更新
|
||||
*/
|
||||
private int minNewSegmentCount = 1;
|
||||
|
||||
/**
|
||||
* 是否启用视频更新检查功能
|
||||
*/
|
||||
private boolean enabled = true;
|
||||
}
|
||||
71
src/main/java/com/ycwl/basic/constant/BuyStatus.java
Normal file
71
src/main/java/com/ycwl/basic/constant/BuyStatus.java
Normal file
@@ -0,0 +1,71 @@
|
||||
package com.ycwl.basic.constant;
|
||||
|
||||
/**
|
||||
* 购买状态枚举
|
||||
* 定义源文件的已购买和未购买两种状态
|
||||
*
|
||||
* @author Claude
|
||||
* @since 2025-10-31
|
||||
*/
|
||||
public enum BuyStatus {
|
||||
/**
|
||||
* 未购买状态
|
||||
*/
|
||||
NOT_BOUGHT(0, "未购买"),
|
||||
|
||||
/**
|
||||
* 已购买状态
|
||||
*/
|
||||
BOUGHT(1, "已购买");
|
||||
|
||||
private final int code;
|
||||
private final String description;
|
||||
|
||||
BuyStatus(int code, String description) {
|
||||
this.code = code;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据代码值获取枚举
|
||||
*
|
||||
* @param code 状态代码
|
||||
* @return 对应的枚举值,如果不存在返回 null
|
||||
*/
|
||||
public static BuyStatus fromCode(int code) {
|
||||
for (BuyStatus status : values()) {
|
||||
if (status.code == code) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断给定的代码是否为已购买状态
|
||||
*
|
||||
* @param code 状态代码
|
||||
* @return true-已购买,false-未购买
|
||||
*/
|
||||
public static boolean isBought(Integer code) {
|
||||
return code != null && code == BOUGHT.code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断给定的代码是否为未购买状态
|
||||
*
|
||||
* @param code 状态代码
|
||||
* @return true-未购买,false-已购买
|
||||
*/
|
||||
public static boolean isNotBought(Integer code) {
|
||||
return code != null && code == NOT_BOUGHT.code;
|
||||
}
|
||||
}
|
||||
@@ -4,4 +4,7 @@ public class FaceConstant {
|
||||
public static final String FACE_DB_NAME_PFX="face:db:";
|
||||
public static final String USER_FACE_DB_NAME="userFace";
|
||||
public static final String FACE_USER_URL_PFX="face:user:url:";
|
||||
public static final String FACE_RECOGNITION_COUNT_PFX="face:recognition:count:";
|
||||
public static final String FACE_CUSTOM_MATCH_COUNT_PFX="face:custom:match:count:";
|
||||
public static final String FACE_LOW_THRESHOLD_PFX="face:low:threshold:";
|
||||
}
|
||||
|
||||
71
src/main/java/com/ycwl/basic/constant/FreeStatus.java
Normal file
71
src/main/java/com/ycwl/basic/constant/FreeStatus.java
Normal file
@@ -0,0 +1,71 @@
|
||||
package com.ycwl.basic.constant;
|
||||
|
||||
/**
|
||||
* 免费状态枚举
|
||||
* 定义源文件的收费和免费两种状态
|
||||
*
|
||||
* @author Claude
|
||||
* @since 2025-10-31
|
||||
*/
|
||||
public enum FreeStatus {
|
||||
/**
|
||||
* 收费状态
|
||||
*/
|
||||
PAID(0, "收费"),
|
||||
|
||||
/**
|
||||
* 免费状态
|
||||
*/
|
||||
FREE(1, "免费");
|
||||
|
||||
private final int code;
|
||||
private final String description;
|
||||
|
||||
FreeStatus(int code, String description) {
|
||||
this.code = code;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据代码值获取枚举
|
||||
*
|
||||
* @param code 状态代码
|
||||
* @return 对应的枚举值,如果不存在返回 null
|
||||
*/
|
||||
public static FreeStatus fromCode(int code) {
|
||||
for (FreeStatus status : values()) {
|
||||
if (status.code == code) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断给定的代码是否为免费状态
|
||||
*
|
||||
* @param code 状态代码
|
||||
* @return true-免费,false-收费
|
||||
*/
|
||||
public static boolean isFree(Integer code) {
|
||||
return code != null && code == FREE.code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断给定的代码是否为收费状态
|
||||
*
|
||||
* @param code 状态代码
|
||||
* @return true-收费,false-免费
|
||||
*/
|
||||
public static boolean isPaid(Integer code) {
|
||||
return code != null && code == PAID.code;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
86
src/main/java/com/ycwl/basic/constant/SourceType.java
Normal file
86
src/main/java/com/ycwl/basic/constant/SourceType.java
Normal file
@@ -0,0 +1,86 @@
|
||||
package com.ycwl.basic.constant;
|
||||
|
||||
/**
|
||||
* 源文件类型枚举
|
||||
* 定义视频和图片两种源文件类型
|
||||
*
|
||||
* @author Claude
|
||||
* @since 2025-10-31
|
||||
*/
|
||||
public enum SourceType {
|
||||
/**
|
||||
* 视频类型
|
||||
*/
|
||||
VIDEO(1, "视频"),
|
||||
|
||||
/**
|
||||
* 图片类型
|
||||
*/
|
||||
IMAGE(2, "图片"),
|
||||
|
||||
/**
|
||||
* AI微单类型
|
||||
*/
|
||||
AI_CAM(3, "AI微单");
|
||||
|
||||
private final int code;
|
||||
private final String description;
|
||||
|
||||
SourceType(int code, String description) {
|
||||
this.code = code;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据代码值获取枚举
|
||||
*
|
||||
* @param code 类型代码
|
||||
* @return 对应的枚举值,如果不存在返回 null
|
||||
*/
|
||||
public static SourceType fromCode(int code) {
|
||||
for (SourceType type : values()) {
|
||||
if (type.code == code) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断给定的代码是否为视频类型
|
||||
*
|
||||
* @param code 类型代码
|
||||
* @return true-是视频,false-不是视频
|
||||
*/
|
||||
public static boolean isVideo(Integer code) {
|
||||
return code != null && code == VIDEO.code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断给定的代码是否为图片类型
|
||||
*
|
||||
* @param code 类型代码
|
||||
* @return true-是图片,false-不是图片
|
||||
*/
|
||||
public static boolean isImage(Integer code) {
|
||||
return code != null && code == IMAGE.code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断给定的代码是否为AI微单类型
|
||||
*
|
||||
* @param code 类型代码
|
||||
* @return true-是AI微单,false-不是AI微单
|
||||
*/
|
||||
public static boolean isAiCam(Integer code) {
|
||||
return code != null && code == AI_CAM.code;
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,7 @@ package com.ycwl.basic.constant;
|
||||
public class StorageConstant {
|
||||
public static final String VLOG_PATH = "vlog";
|
||||
public static final String VIDEO_PIECE_PATH = "source_video";
|
||||
public static final String PHOTO_PATH = "source_photo";
|
||||
public static final String PHOTO_PATH = "viid";
|
||||
public static final String PHOTO_WATERMARKED_PATH = "photo_w";
|
||||
public static final String VIID_FACE = "viid_face";
|
||||
public static final String USER_FACE = "user_face";
|
||||
public static final String USER_FACE = "user_face";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.ycwl.basic.controller;
|
||||
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewAddReqDTO;
|
||||
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewListReqDTO;
|
||||
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewRespDTO;
|
||||
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewStatisticsRespDTO;
|
||||
import com.ycwl.basic.service.VideoReviewService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
|
||||
/**
|
||||
* 视频评价Controller
|
||||
* 管理端使用,通过token角色控制权限
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/video-review/v1")
|
||||
public class VideoReviewController {
|
||||
|
||||
@Autowired
|
||||
private VideoReviewService videoReviewService;
|
||||
|
||||
/**
|
||||
* 新增视频评价
|
||||
*
|
||||
* @param reqDTO 评价信息
|
||||
* @return 评价ID
|
||||
*/
|
||||
@PostMapping("/add")
|
||||
public ApiResponse<Long> addReview(@RequestBody VideoReviewAddReqDTO reqDTO) {
|
||||
log.info("新增视频评价,videoId: {}", reqDTO.getVideoId());
|
||||
Long reviewId = videoReviewService.addReview(reqDTO);
|
||||
return ApiResponse.success(reviewId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询评价列表
|
||||
*
|
||||
* @param reqDTO 查询条件
|
||||
* @return 分页结果
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
public ApiResponse<PageInfo<VideoReviewRespDTO>> getReviewList(VideoReviewListReqDTO reqDTO) {
|
||||
log.info("查询视频评价列表,pageNum: {}, pageSize: {}", reqDTO.getPageNum(), reqDTO.getPageSize());
|
||||
PageInfo<VideoReviewRespDTO> pageInfo = videoReviewService.getReviewList(reqDTO);
|
||||
return ApiResponse.success(pageInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取评价统计数据
|
||||
*
|
||||
* @return 统计结果
|
||||
*/
|
||||
@GetMapping("/statistics")
|
||||
public ApiResponse<VideoReviewStatisticsRespDTO> getStatistics() {
|
||||
log.info("获取视频评价统计数据");
|
||||
VideoReviewStatisticsRespDTO statistics = videoReviewService.getStatistics();
|
||||
return ApiResponse.success(statistics);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出评价数据到Excel
|
||||
*
|
||||
* @param reqDTO 查询条件
|
||||
* @param response HTTP响应
|
||||
*/
|
||||
@GetMapping("/export")
|
||||
public void exportReviews(VideoReviewListReqDTO reqDTO, HttpServletResponse response) {
|
||||
log.info("导出视频评价数据");
|
||||
|
||||
try {
|
||||
// 设置响应头
|
||||
String fileName = "video_reviews_" + System.currentTimeMillis() + ".xlsx";
|
||||
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
response.setCharacterEncoding("UTF-8");
|
||||
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
|
||||
|
||||
// 导出数据
|
||||
videoReviewService.exportReviews(reqDTO, response.getOutputStream());
|
||||
|
||||
response.getOutputStream().flush();
|
||||
} catch (IOException e) {
|
||||
log.error("导出视频评价数据失败", e);
|
||||
throw new RuntimeException("导出失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,14 @@ import cn.hutool.http.HttpUtil;
|
||||
import com.ycwl.basic.annotation.IgnoreToken;
|
||||
import com.ycwl.basic.image.enhancer.adapter.BceImageEnhancer;
|
||||
import com.ycwl.basic.image.enhancer.entity.BceEnhancerConfig;
|
||||
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
||||
import com.ycwl.basic.image.pipeline.stages.DownloadStage;
|
||||
import com.ycwl.basic.image.pipeline.stages.ImageEnhanceStage;
|
||||
import com.ycwl.basic.image.pipeline.stages.ImageSRStage;
|
||||
import com.ycwl.basic.image.pipeline.stages.SourcePhotoUpdateStage;
|
||||
import com.ycwl.basic.image.pipeline.stages.CleanupStage;
|
||||
import com.ycwl.basic.pipeline.core.Pipeline;
|
||||
import com.ycwl.basic.pipeline.core.PipelineBuilder;
|
||||
import com.ycwl.basic.mapper.AioDeviceMapper;
|
||||
import com.ycwl.basic.mapper.MemberMapper;
|
||||
import com.ycwl.basic.model.aio.entity.AioDeviceBannerEntity;
|
||||
@@ -12,29 +20,22 @@ import com.ycwl.basic.model.aio.entity.AioDevicePriceConfigEntity;
|
||||
import com.ycwl.basic.model.aio.req.AioDeviceCreateOrderReq;
|
||||
import com.ycwl.basic.model.aio.resp.AioDeviceCreateOrderResp;
|
||||
import com.ycwl.basic.model.aio.resp.AioDeviceInfoResp;
|
||||
import com.ycwl.basic.model.jwt.JwtInfo;
|
||||
import com.ycwl.basic.model.mobile.face.FaceRecognizeResp;
|
||||
import com.ycwl.basic.model.mobile.goods.GoodsDetailVO;
|
||||
import com.ycwl.basic.model.mobile.goods.GoodsReqQuery;
|
||||
import com.ycwl.basic.model.pc.face.resp.FaceRespVO;
|
||||
import com.ycwl.basic.model.pc.member.entity.MemberEntity;
|
||||
import com.ycwl.basic.model.pc.member.resp.MemberRespVO;
|
||||
import com.ycwl.basic.pay.entity.PayResponse;
|
||||
import com.ycwl.basic.service.aio.AioDeviceService;
|
||||
import com.ycwl.basic.service.mobile.GoodsService;
|
||||
import com.ycwl.basic.service.pc.FaceService;
|
||||
import com.ycwl.basic.service.pc.OrderService;
|
||||
import com.ycwl.basic.service.pc.ScenicService;
|
||||
import com.ycwl.basic.service.pc.SourceService;
|
||||
import com.ycwl.basic.storage.adapters.IStorageAdapter;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import com.ycwl.basic.utils.JwtTokenUtil;
|
||||
import com.ycwl.basic.utils.SnowFlakeUtil;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.Strings;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -48,7 +49,9 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
@@ -119,7 +122,7 @@ public class AioDeviceController {
|
||||
memberEntity.setId(SnowFlakeUtil.getLongId());
|
||||
memberEntity.setNickname("用户");
|
||||
memberMapper.add(memberEntity);
|
||||
FaceRecognizeResp resp = faceService.faceUpload(file, aioDevice.getScenicId(), memberEntity.getId());
|
||||
FaceRecognizeResp resp = faceService.faceUpload(file, aioDevice.getScenicId(), memberEntity.getId(), "");
|
||||
// 尝试超分
|
||||
new Thread(() -> {
|
||||
try {
|
||||
@@ -136,27 +139,38 @@ public class AioDeviceController {
|
||||
redisTemplate.opsForValue().set("aio:faceId:"+resp.getFaceId().toString()+":pass", "1", 1, TimeUnit.DAYS);
|
||||
return;
|
||||
}
|
||||
log.info("超分开始!");
|
||||
log.info("超分开始!共{}张图片待处理", sourcePhotoList.size());
|
||||
|
||||
sourcePhotoList.forEach(photo -> {
|
||||
if (StringUtils.contains(photo.getUrl(), "_q_")) {
|
||||
log.debug("跳过已增强的图片: {}", photo.getUrl());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
File dstFile = new File(photo.getGoodsId()+".jpg");
|
||||
long fileSize = HttpUtil.downloadFile(photo.getUrl(), dstFile);
|
||||
log.info("超分开始:{}", fileSize);
|
||||
BceImageEnhancer enhancer = getEnhancer();
|
||||
MultipartFile enhancedFile = enhancer.enhance(dstFile.getName());
|
||||
log.info("超分结束:{}", photo.getUrl());
|
||||
String url = sourceService.uploadAndUpdateUrl(photo.getGoodsId(), enhancedFile);
|
||||
log.info("上传结束:->{}", url);
|
||||
// 创建超分Pipeline
|
||||
Pipeline<PhotoProcessContext> superResolutionPipeline = createSuperResolutionPipeline(photo.getGoodsId());
|
||||
|
||||
// 使用静态工厂方法创建Context
|
||||
PhotoProcessContext context = PhotoProcessContext.forSuperResolution(
|
||||
photo.getGoodsId(), photo.getUrl(), photo.getScenicId()
|
||||
);
|
||||
|
||||
// 启用图像增强和超分的Stage
|
||||
context.enableStage("image_enhance");
|
||||
context.enableStage("image_sr");
|
||||
|
||||
// 执行Pipeline
|
||||
boolean success = superResolutionPipeline.execute(context);
|
||||
|
||||
if (success) {
|
||||
log.info("超分成功: {} -> {}", photo.getUrl(), context.getResultUrl());
|
||||
} else {
|
||||
log.error("超分失败: {}", photo.getGoodsId());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("超分失败:{}", photo.getGoodsId(), e);
|
||||
} finally {
|
||||
File _file = new File(photo.getGoodsId()+".jpg");
|
||||
if (_file.exists()) {
|
||||
_file.delete();
|
||||
}
|
||||
}
|
||||
});
|
||||
redisTemplate.opsForValue().set("aio:faceId:"+sourcePhotoList.getFirst().getFaceId().toString()+":pass", "1", 1, TimeUnit.DAYS);
|
||||
@@ -213,6 +227,28 @@ public class AioDeviceController {
|
||||
return ApiResponse.success(orderService.queryOrder(orderId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建源图片超分辨率增强Pipeline
|
||||
*
|
||||
* @param sourceId 源图片ID
|
||||
* @return 超分Pipeline
|
||||
*/
|
||||
private Pipeline<PhotoProcessContext> createSuperResolutionPipeline(Long sourceId) {
|
||||
// 创建带有百度云配置的ImageEnhanceStage
|
||||
BceEnhancerConfig config = new BceEnhancerConfig();
|
||||
config.setQps(1);
|
||||
config.setAppId("119554288");
|
||||
config.setApiKey("OX6QoijgKio3eVtA0PiUVf7f");
|
||||
config.setSecretKey("dYatXReVriPeiktTjUblhfubpcmYfuMk");
|
||||
|
||||
return new PipelineBuilder<PhotoProcessContext>("SourcePhotoSuperResolutionPipeline")
|
||||
.addStage(new DownloadStage()) // 1. 下载图片
|
||||
.addStage(new ImageEnhanceStage(config)).addStage(new ImageSRStage(config)) // 2. 图像增强(超分)
|
||||
.addStage(new SourcePhotoUpdateStage(sourceService, sourceId)) // 3. 上传并更新数据库
|
||||
.addStage(new CleanupStage()) // 4. 清理临时文件
|
||||
.build();
|
||||
}
|
||||
|
||||
private BceImageEnhancer getEnhancer() {
|
||||
BceImageEnhancer enhancer = new BceImageEnhancer();
|
||||
BceEnhancerConfig config = new BceEnhancerConfig();
|
||||
|
||||
@@ -18,7 +18,6 @@ import com.ycwl.basic.model.pc.task.entity.TaskEntity;
|
||||
import com.ycwl.basic.model.pc.video.entity.VideoEntity;
|
||||
import com.ycwl.basic.repository.VideoRepository;
|
||||
import com.ycwl.basic.repository.VideoTaskRepository;
|
||||
import com.ycwl.basic.service.mobile.AppScenicService;
|
||||
import com.ycwl.basic.service.mobile.GoodsService;
|
||||
import com.ycwl.basic.service.pc.FaceService;
|
||||
import com.ycwl.basic.service.task.impl.TaskTaskServiceImpl;
|
||||
@@ -114,7 +113,7 @@ public class LyCompatibleController {
|
||||
}
|
||||
FaceRecognizeResp resp;
|
||||
try {
|
||||
resp = faceService.faceUpload(file, scenicId, member.getId());
|
||||
resp = faceService.faceUpload(file, scenicId, member.getId(), "");
|
||||
} catch (Exception e) {
|
||||
return R.error("上传失败!报错:"+e.getMessage());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.ycwl.basic.controller.mobile;
|
||||
|
||||
import com.ycwl.basic.model.jwt.JwtInfo;
|
||||
import com.ycwl.basic.model.mobile.face.FaceRecognizeResp;
|
||||
import com.ycwl.basic.model.mobile.goods.GoodsDetailVO;
|
||||
import com.ycwl.basic.service.mobile.AppAiCamService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import com.ycwl.basic.utils.JwtTokenUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AI相机相关接口
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/mobile/ai_cam/v1")
|
||||
@RequiredArgsConstructor
|
||||
public class AppAiCamController {
|
||||
|
||||
private final AppAiCamService appAiCamService;
|
||||
|
||||
/**
|
||||
* 根据faceId获取AI相机识别到的商品列表
|
||||
* @param faceId 人脸ID
|
||||
* @return 商品详情列表
|
||||
*/
|
||||
@GetMapping("/{faceId}/content")
|
||||
public ApiResponse<List<GoodsDetailVO>> getAiCamGoods(@PathVariable Long faceId) {
|
||||
try {
|
||||
List<GoodsDetailVO> goods = appAiCamService.getAiCamGoodsByFaceId(faceId);
|
||||
return ApiResponse.success(goods);
|
||||
} catch (Exception e) {
|
||||
log.error("获取AI相机商品失败: faceId={}", faceId, e);
|
||||
return ApiResponse.fail("获取商品列表失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量添加会员与source的关联关系
|
||||
* @param faceId 人脸ID
|
||||
* @param sourceIds source ID列表
|
||||
* @return 添加结果
|
||||
*/
|
||||
@PostMapping("/{faceId}/relations")
|
||||
public ApiResponse<String> addMemberSourceRelations(
|
||||
@PathVariable Long faceId,
|
||||
@RequestBody List<Long> sourceIds
|
||||
) {
|
||||
try {
|
||||
int count = appAiCamService.addMemberSourceRelations(faceId, sourceIds);
|
||||
return ApiResponse.success("成功添加" + count + "条关联记录");
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("添加关联失败: faceId={}, error={}", faceId, e.getMessage());
|
||||
return ApiResponse.fail(e.getMessage());
|
||||
} catch (Exception e) {
|
||||
log.error("添加关联失败: faceId={}", faceId, e);
|
||||
return ApiResponse.fail("添加关联失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用人脸样本创建或获取Face记录
|
||||
* @param faceSampleId 人脸样本ID
|
||||
* @return 人脸识别响应
|
||||
*/
|
||||
@GetMapping("/useSample/{faceSampleId}")
|
||||
public ApiResponse<FaceRecognizeResp> useSample(@PathVariable Long faceSampleId) {
|
||||
try {
|
||||
JwtInfo worker = JwtTokenUtil.getWorker();
|
||||
FaceRecognizeResp resp = appAiCamService.useSample(worker.getUserId(), faceSampleId);
|
||||
return ApiResponse.success(resp);
|
||||
} catch (Exception e) {
|
||||
log.error("使用人脸样本失败: faceSampleId={}", faceSampleId, e);
|
||||
return ApiResponse.fail("使用人脸样本失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.ycwl.basic.controller.mobile;
|
||||
|
||||
import com.ycwl.basic.model.jwt.JwtInfo;
|
||||
import com.ycwl.basic.model.mobile.chat.*;
|
||||
import com.ycwl.basic.service.mobile.FaceChatService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import com.ycwl.basic.utils.JwtTokenUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* 小程序人脸智能聊天接口。
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/mobile/chat/v1")
|
||||
@RequiredArgsConstructor
|
||||
public class AppChatController {
|
||||
|
||||
private final FaceChatService faceChatService;
|
||||
|
||||
/**
|
||||
* 获取或创建会话(同一人脸只保留一条)。
|
||||
*/
|
||||
@PostMapping("/faces/{faceId}/conversation")
|
||||
public ApiResponse<ChatConversationVO> createConversation(@PathVariable Long faceId) {
|
||||
JwtInfo worker = JwtTokenUtil.getWorker();
|
||||
ChatConversationVO vo = faceChatService.getOrCreateConversation(faceId, worker.getUserId());
|
||||
return ApiResponse.success(vo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步发送消息,适用于短回复或前端自行轮询。
|
||||
*/
|
||||
@PostMapping("/conversations/{conversationId}/messages")
|
||||
public ApiResponse<ChatSendMessageResp> sendMessage(@PathVariable Long conversationId,
|
||||
@RequestBody ChatSendMessageReq req) {
|
||||
JwtInfo worker = JwtTokenUtil.getWorker();
|
||||
ChatSendMessageResp resp = faceChatService.sendMessage(conversationId, worker.getUserId(),
|
||||
req.getContent(), req.getTraceId());
|
||||
return ApiResponse.success(resp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 流式返回,使用 HTTP chunked。小程序侧用 wx.request 的 onChunkReceived 消费。
|
||||
*/
|
||||
@PostMapping(value = "/conversations/{conversationId}/messages/stream", produces = "text/plain;charset=UTF-8")
|
||||
public ResponseBodyEmitter streamMessage(@PathVariable Long conversationId,
|
||||
@RequestBody ChatSendMessageReq req) {
|
||||
JwtInfo worker = JwtTokenUtil.getWorker();
|
||||
ResponseBodyEmitter emitter = new ResponseBodyEmitter(30_000L);
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
faceChatService.sendMessageStream(
|
||||
conversationId,
|
||||
worker.getUserId(),
|
||||
req.getContent(),
|
||||
req.getTraceId(),
|
||||
chunk -> {
|
||||
try {
|
||||
emitter.send(chunk, new MediaType("text", "plain", java.nio.charset.StandardCharsets.UTF_8));
|
||||
} catch (Exception e) {
|
||||
emitter.completeWithError(e);
|
||||
}
|
||||
});
|
||||
emitter.complete();
|
||||
} catch (Exception e) {
|
||||
log.error("streamMessage error", e);
|
||||
emitter.completeWithError(e);
|
||||
}
|
||||
});
|
||||
return emitter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询历史消息,cursor 为最后一条 seq,limit 为条数。
|
||||
*/
|
||||
@GetMapping("/conversations/{conversationId}/messages")
|
||||
public ApiResponse<ChatMessagePageResp> listMessages(@PathVariable Long conversationId,
|
||||
@RequestParam(value = "cursor", required = false) Integer cursor,
|
||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
||||
JwtInfo worker = JwtTokenUtil.getWorker();
|
||||
ChatMessagePageResp resp = faceChatService.listMessages(conversationId, cursor, limit, worker.getUserId());
|
||||
return ApiResponse.success(resp);
|
||||
}
|
||||
|
||||
@PostMapping("/conversations/{conversationId}/close")
|
||||
public ApiResponse<String> closeConversation(@PathVariable Long conversationId) {
|
||||
JwtInfo worker = JwtTokenUtil.getWorker();
|
||||
faceChatService.closeConversation(conversationId, worker.getUserId());
|
||||
return ApiResponse.success("OK");
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,12 @@ import com.ycwl.basic.pricing.dto.CouponClaimRequest;
|
||||
import com.ycwl.basic.pricing.dto.CouponClaimResult;
|
||||
import com.ycwl.basic.pricing.dto.req.VoucherPrintReq;
|
||||
import com.ycwl.basic.pricing.dto.resp.VoucherPrintResp;
|
||||
import com.ycwl.basic.pricing.enums.CouponType;
|
||||
import com.ycwl.basic.pricing.service.ICouponService;
|
||||
import com.ycwl.basic.pricing.service.VoucherPrintService;
|
||||
import com.ycwl.basic.repository.FaceRepository;
|
||||
import com.ycwl.basic.repository.ScenicRepository;
|
||||
import com.ycwl.basic.util.ScenicConfigManager;
|
||||
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -19,6 +20,9 @@ import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/mobile/claim/v1")
|
||||
@AllArgsConstructor
|
||||
@@ -80,6 +84,24 @@ public class AppClaimController {
|
||||
if (claimResult.isSuccess()) {
|
||||
// 领到了
|
||||
claimResp.setHasCoupon(true);
|
||||
switch (claimResult.getCoupon().getCouponType()) {
|
||||
case CouponType.PERCENTAGE:
|
||||
claimResp.setCouponType("折扣优惠券");
|
||||
claimResp.setCouponDesc("打" + (BigDecimal.valueOf(1).setScale(2, RoundingMode.HALF_UP).subtract(claimResult.getCoupon().getDiscountValue())).multiply(BigDecimal.valueOf(10)) + "折");
|
||||
break;
|
||||
case CouponType.FIXED_AMOUNT:
|
||||
if (claimResult.getCoupon().getMinAmount().compareTo(BigDecimal.ZERO) > 0) {
|
||||
claimResp.setCouponType("满减优惠券");
|
||||
claimResp.setCouponDesc("满" + claimResult.getCoupon().getMinAmount() + "减" + claimResult.getCoupon().getDiscountValue());
|
||||
} else {
|
||||
claimResp.setCouponType("直减优惠券");
|
||||
claimResp.setCouponDesc("直减" + claimResult.getCoupon().getDiscountValue());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
claimResp.setCouponType("普通优惠券");
|
||||
break;
|
||||
}
|
||||
claimResp.setCouponDesc(scenicConfig.getString("coupon_desc_for_type_" + req.getType(), "专属折扣券"));
|
||||
claimResp.setCouponCountdown(scenicConfig.getString("coupon_countdown_for_type_" + req.getType(), "送你优惠,保存美好!"));
|
||||
return ApiResponse.success(claimResp);
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
package com.ycwl.basic.controller.mobile;
|
||||
|
||||
import com.ycwl.basic.exception.BaseException;
|
||||
import com.ycwl.basic.model.jwt.JwtInfo;
|
||||
import com.ycwl.basic.model.mobile.face.FaceRecognizeResp;
|
||||
import com.ycwl.basic.model.mobile.face.FaceStatusResp;
|
||||
import com.ycwl.basic.model.mobile.scenic.content.ContentPageVO;
|
||||
import com.ycwl.basic.model.mobile.face.FaceRecognitionUpdateReq;
|
||||
import com.ycwl.basic.model.mobile.face.FaceRecognitionDetailVO;
|
||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||
import com.ycwl.basic.model.pc.face.resp.FaceRespVO;
|
||||
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
||||
import com.ycwl.basic.repository.FaceRepository;
|
||||
import com.ycwl.basic.service.pc.FaceService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import com.ycwl.basic.utils.JwtTokenUtil;
|
||||
@@ -20,11 +27,12 @@ import java.util.List;
|
||||
@RestController
|
||||
@RequestMapping("/api/mobile/face/v1")
|
||||
// 用户人脸相关接口
|
||||
public class
|
||||
AppFaceController {
|
||||
public class AppFaceController {
|
||||
|
||||
@Autowired
|
||||
private FaceService faceService;
|
||||
@Autowired
|
||||
private FaceRepository faceRepository;
|
||||
|
||||
/**
|
||||
* 1、上传人脸照片
|
||||
@@ -37,11 +45,13 @@ AppFaceController {
|
||||
*/
|
||||
// 人脸照片上传
|
||||
@PostMapping("/faceUPload")
|
||||
public ApiResponse<FaceRecognizeResp> faceUpload(@RequestParam("file")MultipartFile file, @RequestParam("scenicId") Long scenicId) {
|
||||
public ApiResponse<FaceRecognizeResp> faceUpload(@RequestParam("file")MultipartFile file,
|
||||
@RequestParam(value = "scene", defaultValue = "", required = false) String scene,
|
||||
@RequestParam("scenicId") Long scenicId) {
|
||||
//获取用户id
|
||||
JwtInfo worker = JwtTokenUtil.getWorker();
|
||||
Long userId = worker.getUserId();
|
||||
FaceRecognizeResp resp = faceService.faceUpload(file, scenicId, userId);
|
||||
FaceRecognizeResp resp = faceService.faceUpload(file, scenicId, userId, scene);
|
||||
return ApiResponse.success(resp);
|
||||
}
|
||||
|
||||
@@ -49,7 +59,7 @@ AppFaceController {
|
||||
public ApiResponse<List<FaceRespVO>> list(@PathVariable("scenicId") String scenicId) {
|
||||
JwtInfo worker = JwtTokenUtil.getWorker();
|
||||
Long userId = worker.getUserId();
|
||||
List<FaceRespVO> list = faceService.listByUser(userId, scenicId);
|
||||
List<FaceRespVO> list = faceService.listByUser(userId, Long.parseLong(scenicId));
|
||||
return ApiResponse.success(list);
|
||||
}
|
||||
|
||||
@@ -60,6 +70,18 @@ AppFaceController {
|
||||
|
||||
@DeleteMapping("/{faceId}")
|
||||
public ApiResponse<String> deleteFace(@PathVariable("faceId") Long faceId) {
|
||||
// 添加权限检查:验证当前用户是否拥有该 face
|
||||
JwtInfo worker = JwtTokenUtil.getWorker();
|
||||
Long userId = worker.getUserId();
|
||||
|
||||
FaceEntity face = faceRepository.getFace(faceId);
|
||||
if (face == null) {
|
||||
throw new BaseException("人脸数据不存在");
|
||||
}
|
||||
if (!face.getMemberId().equals(userId)) {
|
||||
throw new BaseException("无权删除此人脸");
|
||||
}
|
||||
|
||||
return faceService.deleteFace(faceId);
|
||||
}
|
||||
|
||||
@@ -85,4 +107,40 @@ AppFaceController {
|
||||
faceService.bindFace(faceId, userId);
|
||||
return ApiResponse.success("OK");
|
||||
}
|
||||
|
||||
@GetMapping("/{faceId}/status")
|
||||
public ApiResponse<FaceStatusResp> status(@PathVariable Long faceId) {
|
||||
return ApiResponse.success(faceService.getFaceStatus(faceId));
|
||||
}
|
||||
|
||||
@GetMapping("/{faceId}/extraCheck")
|
||||
public ApiResponse<Boolean> hasExtraCheck(@PathVariable Long faceId) {
|
||||
return ApiResponse.success(faceService.checkHasExtraCheck(faceId));
|
||||
}
|
||||
|
||||
@GetMapping("/{faceId}/queryOtherFace")
|
||||
public ApiResponse<List<FaceSampleEntity>> queryOtherFace(@PathVariable Long faceId) {
|
||||
return ApiResponse.success(faceService.getLowMatchedFaceSamples(faceId));
|
||||
}
|
||||
|
||||
@PostMapping("/{faceId}/queryOtherFace")
|
||||
public ApiResponse<String> queryOtherFace(@PathVariable Long faceId, @RequestBody List<Long> faceIds) {
|
||||
faceService.matchCustomFaceId(faceId, faceIds);
|
||||
return ApiResponse.success("OK");
|
||||
}
|
||||
|
||||
@PutMapping("/{faceId}/recognition")
|
||||
public ApiResponse<?> updateRecognition(@PathVariable Long faceId,
|
||||
@RequestBody FaceRecognitionUpdateReq req) {
|
||||
req.setFaceId(faceId);
|
||||
faceService.updateRecognition(req);
|
||||
return ApiResponse.success("OK");
|
||||
}
|
||||
|
||||
@GetMapping("/{faceId}/recognition/detail")
|
||||
public ApiResponse<FaceRecognitionDetailVO> recognitionDetail(@PathVariable Long faceId) {
|
||||
return ApiResponse.success(faceService.getRecognitionDetail(faceId));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ import com.ycwl.basic.annotation.IgnoreToken;
|
||||
import com.ycwl.basic.exception.CheckTokenException;
|
||||
import com.ycwl.basic.model.jwt.JwtInfo;
|
||||
import com.ycwl.basic.model.mobile.goods.*;
|
||||
import com.ycwl.basic.model.pc.face.resp.FaceRespVO;
|
||||
import com.ycwl.basic.service.mobile.GoodsService;
|
||||
import com.ycwl.basic.service.pc.FaceService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import com.ycwl.basic.utils.JwtTokenUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -24,11 +26,17 @@ public class AppGoodsController {
|
||||
|
||||
@Autowired
|
||||
private GoodsService goodsService;
|
||||
@Autowired
|
||||
private FaceService faceService;
|
||||
|
||||
// 商品列表
|
||||
@PostMapping("/goodsList")
|
||||
public ApiResponse<List<GoodsPageVO>> goodsList(@RequestBody GoodsReqQuery query) {
|
||||
return goodsService.goodsList(query);
|
||||
JwtInfo worker = JwtTokenUtil.getWorker();
|
||||
Long userId = worker.getUserId();
|
||||
List<FaceRespVO> faceRespVOS = faceService.listByUser(userId, query.getScenicId());
|
||||
List<Long> faceIds = faceRespVOS.stream().map(FaceRespVO::getId).toList();
|
||||
return goodsService.listGoodsByFaceIdList(faceIds, query.getIsBuy(), query.getScenicId());
|
||||
}
|
||||
|
||||
// 源素材(原片/照片)商品列表
|
||||
@@ -99,4 +107,16 @@ public class AppGoodsController {
|
||||
JwtInfo worker = JwtTokenUtil.getWorker();
|
||||
return ApiResponse.success(goodsService.getTaskStatusByTemplateId(faceId, templateId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查视频是否可更新
|
||||
*
|
||||
* @param videoId 视频ID
|
||||
* @return 视频更新检查结果
|
||||
*/
|
||||
@GetMapping("/video/{videoId}/updateCheck")
|
||||
public ApiResponse<VideoUpdateCheckVO> checkVideoUpdate(@PathVariable("videoId") Long videoId) {
|
||||
VideoUpdateCheckVO result = goodsService.checkVideoUpdate(videoId);
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.ycwl.basic.controller.mobile;
|
||||
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.ycwl.basic.annotation.IgnoreToken;
|
||||
import com.ycwl.basic.biz.OrderBiz;
|
||||
import com.ycwl.basic.biz.PriceBiz;
|
||||
import com.ycwl.basic.constant.BaseContextHandler;
|
||||
@@ -51,6 +52,7 @@ public class AppOrderController {
|
||||
|
||||
// 用户端订单详情查询
|
||||
@GetMapping("getOrderDetails/{id}")
|
||||
@IgnoreToken
|
||||
public ApiResponse<OrderAppRespVO> getOrderDetails(@PathVariable("id") Long id) {
|
||||
return orderService.appDetail(id);
|
||||
}
|
||||
@@ -91,9 +93,9 @@ public class AppOrderController {
|
||||
}
|
||||
|
||||
@GetMapping("/scenic/{scenicId}/query")
|
||||
public ApiResponse<IsBuyRespVO> isBuy(@PathVariable("scenicId") Long scenicId, @RequestParam("type") Integer type, @RequestParam("goodsId") Long goodsId) {
|
||||
public ApiResponse<IsBuyRespVO> isBuy(@PathVariable("scenicId") Long scenicId, @RequestParam("type") Integer type, @RequestParam("goodsId") Long goodsId, @RequestParam(value = "faceId", required = false) Long faceId) {
|
||||
Long userId = Long.parseLong(BaseContextHandler.getUserId());
|
||||
return ApiResponse.success(orderBiz.isBuy(userId, scenicId, type, goodsId));
|
||||
return ApiResponse.success(orderBiz.isBuy(scenicId, userId, faceId, type, goodsId));
|
||||
}
|
||||
|
||||
@GetMapping("/scenic/{scenicId}/queryBatchPrice")
|
||||
@@ -106,7 +108,7 @@ public class AppOrderController {
|
||||
}
|
||||
faceId = lastFaceByUserId.getId();
|
||||
}
|
||||
IsBuyBatchRespVO buy = priceBiz.isBuy(userId, faceId, scenicId, type, goodsIds);
|
||||
IsBuyBatchRespVO buy = priceBiz.isOnePriceBuy(userId, faceId, scenicId, type, goodsIds);
|
||||
if (buy == null) {
|
||||
return ApiResponse.fail("该套餐暂未开放购买");
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@ import com.github.pagehelper.PageInfo;
|
||||
import com.ycwl.basic.constant.BaseContextHandler;
|
||||
import com.ycwl.basic.mapper.SourceMapper;
|
||||
import com.ycwl.basic.mapper.VideoMapper;
|
||||
import com.ycwl.basic.model.pc.order.req.CreateOrderReqVO;
|
||||
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
||||
import com.ycwl.basic.model.pc.source.req.SourceReqQuery;
|
||||
import com.ycwl.basic.model.pc.task.entity.TaskEntity;
|
||||
import com.ycwl.basic.model.pc.video.entity.MemberVideoEntity;
|
||||
import com.ycwl.basic.model.pc.video.entity.VideoEntity;
|
||||
import com.ycwl.basic.pricing.enums.ProductType;
|
||||
import com.ycwl.basic.repository.TemplateRepository;
|
||||
import com.ycwl.basic.repository.VideoRepository;
|
||||
import com.ycwl.basic.repository.VideoTaskRepository;
|
||||
import com.ycwl.basic.service.pc.OrderService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
@@ -27,17 +27,14 @@ import com.ycwl.basic.order.dto.OrderV2PageRequest;
|
||||
import com.ycwl.basic.order.dto.PaymentParamsRequest;
|
||||
import com.ycwl.basic.order.dto.PaymentParamsResponse;
|
||||
import com.ycwl.basic.order.dto.PaymentCallbackResponse;
|
||||
import com.ycwl.basic.utils.JacksonUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* 移动端订单控制器V2
|
||||
@@ -58,6 +55,8 @@ public class AppOrderV2Controller {
|
||||
private final VideoMapper videoMapper;
|
||||
private final VideoTaskRepository videoTaskRepository;
|
||||
private final TemplateRepository templateRepository;
|
||||
private final VideoRepository videoRepository;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
/**
|
||||
* 移动端价格计算
|
||||
@@ -80,7 +79,17 @@ public class AppOrderV2Controller {
|
||||
// 验证faceId参数
|
||||
if (request.getFaceId() == null) {
|
||||
log.warn("移动端价格计算:faceId参数缺失");
|
||||
return ApiResponse.fail("faceId参数不能为空");
|
||||
// return ApiResponse.fail("faceId参数不能为空");
|
||||
// 兼容:兼容旧版本
|
||||
ProductItem productItem = request.getProducts().getFirst();
|
||||
switch (productItem.getProductType()) {
|
||||
case VLOG_VIDEO -> {
|
||||
VideoEntity video = videoRepository.getVideo(Long.valueOf(productItem.getProductId()));
|
||||
TaskEntity task = videoTaskRepository.getTaskById(video.getTaskId());
|
||||
request.setFaceId(task.getFaceId());
|
||||
}
|
||||
case RECORDING_SET, PHOTO_SET, AI_CAM_PHOTO_SET -> request.setFaceId(Long.valueOf(productItem.getProductId()));
|
||||
}
|
||||
}
|
||||
|
||||
// 查询人脸信息进行权限验证
|
||||
@@ -93,45 +102,14 @@ public class AppOrderV2Controller {
|
||||
FaceRespVO face = faceResponse.getData();
|
||||
Long scenicId = face.getScenicId();
|
||||
|
||||
// 先尝试从Redis缓存获取价格计算结果
|
||||
PriceCalculationResult cachedResult = priceCacheService.getCachedPriceResult(
|
||||
currentUserId, scenicId, request.getProducts());
|
||||
|
||||
if (cachedResult != null) {
|
||||
log.info("命中价格缓存: userId={}, scenicId={}, finalAmount={}",
|
||||
currentUserId, scenicId, cachedResult.getFinalAmount());
|
||||
return ApiResponse.success(cachedResult);
|
||||
}
|
||||
|
||||
request.getProducts().forEach(product -> {
|
||||
switch (product.getProductType()) {
|
||||
case VLOG_VIDEO:
|
||||
AtomicInteger deviceCount = new AtomicInteger();
|
||||
List<MemberVideoEntity> videoEntities = videoMapper.listRelationByFaceAndTemplate(face.getId(), Long.valueOf(product.getProductId()));
|
||||
if (videoEntities != null && !videoEntities.isEmpty()) {
|
||||
TaskEntity task = videoTaskRepository.getTaskById(videoEntities.getFirst().getTaskId());
|
||||
if (task != null) {
|
||||
Map<String, Object> paramJson = JacksonUtil.parseObject(task.getTaskParams(), Map.class);
|
||||
if (paramJson == null) {
|
||||
deviceCount.set(1);
|
||||
} else {
|
||||
List<String> templatePlaceholder = templateRepository.getTemplatePlaceholder(task.getTemplateId());
|
||||
paramJson.entrySet().stream()
|
||||
.filter(entry -> StringUtils.isNumeric(entry.getKey()))
|
||||
.forEach(entry -> {
|
||||
List<Object> jsonArray = JacksonUtil.parseArray(JacksonUtil.toJSONString(entry.getValue()), Object.class);
|
||||
if (jsonArray != null && !jsonArray.isEmpty()) {
|
||||
for (Object ignored : jsonArray) {
|
||||
if (templatePlaceholder.contains(entry.getKey())) {
|
||||
deviceCount.getAndIncrement();
|
||||
templatePlaceholder.remove(entry.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
product.setQuantity(deviceCount.get());
|
||||
}
|
||||
product.setQuantity(videoTaskRepository.getTaskLensNum(videoEntities.getFirst().getTaskId()));
|
||||
} else {
|
||||
product.setQuantity(1);
|
||||
}
|
||||
break;
|
||||
case RECORDING_SET:
|
||||
@@ -143,6 +121,14 @@ public class AppOrderV2Controller {
|
||||
Integer count = sourceMapper.countUser(sourceReqQuery);
|
||||
product.setQuantity(count);
|
||||
break;
|
||||
case AI_CAM_PHOTO_SET:
|
||||
SourceReqQuery aiPhotoSetReqQuery = new SourceReqQuery();
|
||||
aiPhotoSetReqQuery.setMemberId(currentUserId);
|
||||
aiPhotoSetReqQuery.setType(13);
|
||||
aiPhotoSetReqQuery.setFaceId(face.getId());
|
||||
Integer _count = sourceMapper.countUser(aiPhotoSetReqQuery);
|
||||
product.setQuantity(_count);
|
||||
break;
|
||||
default:
|
||||
log.warn("未知的商品类型,跳过重复购买检查: productType={}", product.getProductType());
|
||||
break;
|
||||
@@ -220,7 +206,7 @@ public class AppOrderV2Controller {
|
||||
if (cachedResult.getFinalAmount().compareTo(request.getExpectedFinalAmount()) != 0) {
|
||||
log.warn("移动端下单:价格不匹配, cached={}, expected={}, userId={}, scenicId={}",
|
||||
cachedResult.getFinalAmount(), request.getExpectedFinalAmount(), currentUserId, scenicId);
|
||||
return ApiResponse.fail("请重新下单!");
|
||||
return ApiResponse.fail("价格信息变化,请退出后重新查询价格!");
|
||||
}
|
||||
|
||||
// 验证原价是否匹配(可选)
|
||||
@@ -239,6 +225,7 @@ public class AppOrderV2Controller {
|
||||
Long orderId = oldOrderService.createOrderCompact(currentUserId, request, cachedResult);
|
||||
return ApiResponse.success(String.valueOf(orderId));
|
||||
} catch (Exception e) {
|
||||
log.warn("移动端下单:订单创建失败, userId={}, scenicId={}, error={}", currentUserId, scenicId, e.getMessage(), e);
|
||||
return ApiResponse.fail("订单创建失败,请稍后重试");
|
||||
}
|
||||
|
||||
@@ -285,36 +272,21 @@ public class AppOrderV2Controller {
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户查询自己的订单详情
|
||||
* 查询订单详情
|
||||
*/
|
||||
@GetMapping("/detail/{orderId}")
|
||||
public ApiResponse<OrderV2DetailResponse> getUserOrderDetail(@PathVariable("orderId") Long orderId) {
|
||||
String currentUserIdStr = BaseContextHandler.getUserId();
|
||||
if (currentUserIdStr == null) {
|
||||
log.warn("用户未登录");
|
||||
return ApiResponse.fail("用户未登录");
|
||||
}
|
||||
|
||||
Long currentUserId = Long.valueOf(currentUserIdStr);
|
||||
|
||||
log.info("用户查询订单详情: userId={}, orderId={}", currentUserId, orderId);
|
||||
|
||||
log.info("查询订单详情: orderId={}", orderId);
|
||||
|
||||
try {
|
||||
OrderV2DetailResponse detail = orderService.getOrderDetail(orderId);
|
||||
if (detail == null) {
|
||||
return ApiResponse.fail("订单不存在");
|
||||
}
|
||||
|
||||
// 验证订单是否属于当前用户
|
||||
if (!currentUserId.equals(detail.getMemberId())) {
|
||||
log.warn("用户尝试访问他人订单: userId={}, orderId={}, orderOwner={}",
|
||||
currentUserId, orderId, detail.getMemberId());
|
||||
return ApiResponse.fail("无权访问该订单");
|
||||
}
|
||||
|
||||
|
||||
return ApiResponse.success(detail);
|
||||
} catch (Exception e) {
|
||||
log.error("查询用户订单详情失败: userId={}, orderId={}", currentUserId, orderId, e);
|
||||
log.error("查询订单详情失败: orderId={}", orderId, e);
|
||||
return ApiResponse.fail("查询失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
@@ -379,4 +351,9 @@ public class AppOrderV2Controller {
|
||||
return "FAIL";
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/downloadable/{orderId}")
|
||||
public ApiResponse<Boolean> getDownloadableOrder(@PathVariable("orderId") Long orderId) {
|
||||
return ApiResponse.success(!redisTemplate.hasKey("order_content_not_downloadable_" + orderId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.ycwl.basic.controller.mobile;
|
||||
|
||||
import com.ycwl.basic.annotation.IgnoreToken;
|
||||
import com.ycwl.basic.model.jwt.JwtInfo;
|
||||
import com.ycwl.basic.model.mobile.face.FaceRecognizeResp;
|
||||
import com.ycwl.basic.model.pc.printer.resp.MemberPrintResp;
|
||||
import com.ycwl.basic.model.pc.printer.resp.PrinterResp;
|
||||
import com.ycwl.basic.model.printer.req.FromSourceReq;
|
||||
@@ -16,6 +17,7 @@ import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@@ -35,10 +37,16 @@ public class AppPrinterController {
|
||||
return ApiResponse.success(printerService.listByScenicId(scenicId));
|
||||
}
|
||||
|
||||
@GetMapping("/getListFor/{scenicId}")
|
||||
public ApiResponse<List<MemberPrintResp>> getListFor(@PathVariable("scenicId") Long scenicId) {
|
||||
@GetMapping("/useSample/{sampleId}")
|
||||
public ApiResponse<FaceRecognizeResp> useSample(@PathVariable("sampleId") Long sampleId) throws IOException {
|
||||
JwtInfo worker = JwtTokenUtil.getWorker();
|
||||
return ApiResponse.success(printerService.getUserPhotoList(worker.getUserId(), scenicId));
|
||||
return ApiResponse.success(printerService.useSample(worker.getUserId(), sampleId));
|
||||
}
|
||||
|
||||
@GetMapping("/getListFor/{scenicId}")
|
||||
public ApiResponse<List<MemberPrintResp>> getListFor(@PathVariable("scenicId") Long scenicId, @RequestParam(required = false) String faceId) {
|
||||
JwtInfo worker = JwtTokenUtil.getWorker();
|
||||
return ApiResponse.success(printerService.getUserPhotoList(worker.getUserId(), scenicId, parseFaceId(faceId)));
|
||||
}
|
||||
|
||||
@GetMapping("/getItem/{scenicId}/{id}")
|
||||
@@ -52,31 +60,38 @@ public class AppPrinterController {
|
||||
}
|
||||
|
||||
@PostMapping("/deleteFrom/{scenicId}/{id}")
|
||||
public ApiResponse<?> deleteFrom(@PathVariable("scenicId") Long scenicId, @PathVariable("id") Long id) throws IOException {
|
||||
public ApiResponse<?> deleteFrom(@PathVariable("scenicId") Long scenicId, @PathVariable("id") Long id) {
|
||||
JwtInfo worker = JwtTokenUtil.getWorker();
|
||||
printerService.deleteUserPhoto(worker.getUserId(), scenicId, id);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
@PostMapping("/uploadTo/{scenicId}")
|
||||
public ApiResponse<?> upload(@PathVariable("scenicId") Long scenicId, @RequestParam(value = "file") MultipartFile file) throws IOException {
|
||||
public ApiResponse<?> upload(@PathVariable("scenicId") Long scenicId,
|
||||
@RequestParam(value = "file") MultipartFile file,
|
||||
@RequestParam(value = "faceId", required = false) String faceId) {
|
||||
String[] split = file.getOriginalFilename().split("\\.");
|
||||
String ext = split[split.length - 1];
|
||||
String url = StorageFactory.use().uploadFile(file, "printer", UUID.randomUUID() + "." + ext);
|
||||
printerService.addUserPhoto(JwtTokenUtil.getWorker().getUserId(), scenicId, url);
|
||||
return ApiResponse.success(url);
|
||||
Integer id = printerService.addUserPhoto(JwtTokenUtil.getWorker().getUserId(), scenicId, url, parseFaceId(faceId), null);
|
||||
return ApiResponse.success(id);
|
||||
}
|
||||
@PostMapping("/uploadTo/{scenicId}/cropped/{id}")
|
||||
public ApiResponse<?> uploadReplace(@PathVariable("scenicId") Long scenicId, @PathVariable("id") Long id, @RequestParam(value = "file") MultipartFile file) throws IOException {
|
||||
@PostMapping(value = "/uploadTo/{scenicId}/cropped/{id}", consumes = "multipart/form-data")
|
||||
public ApiResponse<?> uploadReplace(@PathVariable("scenicId") Long scenicId,
|
||||
@PathVariable("id") Long id,
|
||||
@RequestPart(value = "crop", required = false) String crop,
|
||||
@RequestPart(value = "file") MultipartFile file) {
|
||||
String[] split = file.getOriginalFilename().split("\\.");
|
||||
String ext = split[split.length - 1];
|
||||
String url = StorageFactory.use().uploadFile(file, "printer", UUID.randomUUID() + "." + ext);
|
||||
printerService.setPhotoCropped(JwtTokenUtil.getWorker().getUserId(), scenicId, id, url);
|
||||
printerService.setPhotoCropped(JwtTokenUtil.getWorker().getUserId(), scenicId, id, url, crop);
|
||||
return ApiResponse.success(url);
|
||||
}
|
||||
@PostMapping("/uploadTo/{scenicId}/formSource")
|
||||
public ApiResponse<?> uploadFromSource(@PathVariable("scenicId") Long scenicId, @RequestBody FromSourceReq req) throws IOException {
|
||||
printerService.addUserPhotoFromSource(JwtTokenUtil.getWorker().getUserId(), scenicId, req);
|
||||
return ApiResponse.success(null);
|
||||
public ApiResponse<?> uploadFromSource(@PathVariable("scenicId") Long scenicId,
|
||||
@RequestBody FromSourceReq req,
|
||||
@RequestParam(value = "faceId", required = false) String faceId) {
|
||||
List<Integer> list = printerService.addUserPhotoFromSource(JwtTokenUtil.getWorker().getUserId(), scenicId, req, parseFaceId(faceId));
|
||||
return ApiResponse.success(list);
|
||||
}
|
||||
|
||||
@PostMapping("/setQuantity/{scenicId}/{id}")
|
||||
@@ -91,16 +106,35 @@ public class AppPrinterController {
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
@GetMapping("/price/{scenicId}")
|
||||
public ApiResponse<?> queryPrice(@PathVariable("scenicId") Long scenicId) {
|
||||
return ApiResponse.success(printerService.queryPrice(JwtTokenUtil.getWorker().getUserId(), scenicId));
|
||||
public ApiResponse<?> queryPrice(@PathVariable("scenicId") Long scenicId,
|
||||
@RequestParam(value = "faceId", required = false) String faceId) {
|
||||
return ApiResponse.success(printerService.queryPrice(JwtTokenUtil.getWorker().getUserId(), scenicId, parseFaceId(faceId)));
|
||||
}
|
||||
|
||||
@PostMapping("/order/{scenicId}")
|
||||
public ApiResponse<Map<String, Object>> createOrder(@PathVariable("scenicId") Long scenicId) {
|
||||
return ApiResponse.success(printerService.createOrder(JwtTokenUtil.getWorker().getUserId(), scenicId, null));
|
||||
public ApiResponse<Map<String, Object>> createOrder(@PathVariable("scenicId") Long scenicId,
|
||||
@RequestParam(value = "faceId", required = false) String faceId) {
|
||||
return ApiResponse.success(printerService.createOrder(JwtTokenUtil.getWorker().getUserId(), scenicId, null, parseFaceId(faceId)));
|
||||
}
|
||||
@PostMapping("/order/{scenicId}/toPrinter/{printerId}")
|
||||
public ApiResponse<Map<String, Object>> createOrderToPrinter(@PathVariable("scenicId") Long scenicId, @PathVariable("printerId") Integer printerId) {
|
||||
return ApiResponse.success(printerService.createOrder(JwtTokenUtil.getWorker().getUserId(), scenicId, printerId));
|
||||
public ApiResponse<Map<String, Object>> createOrderToPrinter(@PathVariable("scenicId") Long scenicId,
|
||||
@PathVariable("printerId") Integer printerId,
|
||||
@RequestParam(value = "faceId", required = false) String faceId) {
|
||||
return ApiResponse.success(printerService.createOrder(JwtTokenUtil.getWorker().getUserId(), scenicId, printerId, parseFaceId(faceId)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 faceId 字符串为 Long 类型
|
||||
* 如果字符串不是有效数字,则返回 null
|
||||
*/
|
||||
private Long parseFaceId(String faceId) {
|
||||
if (faceId == null || faceId.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(faceId.trim());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
package com.ycwl.basic.controller.mobile;
|
||||
|
||||
import com.ycwl.basic.biz.OrderBiz;
|
||||
import com.ycwl.basic.constant.SourceType;
|
||||
import com.ycwl.basic.model.mobile.order.IsBuyRespVO;
|
||||
import com.ycwl.basic.model.mobile.scenic.content.ContentPageVO;
|
||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||
import com.ycwl.basic.pricing.dto.PriceCalculationRequest;
|
||||
import com.ycwl.basic.pricing.dto.PriceCalculationResult;
|
||||
import com.ycwl.basic.pricing.dto.ProductItem;
|
||||
import com.ycwl.basic.pricing.enums.ProductType;
|
||||
import com.ycwl.basic.pricing.service.IPriceCalculationService;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleGenerationRecordEntity;
|
||||
import com.ycwl.basic.puzzle.mapper.PuzzleGenerationRecordMapper;
|
||||
import com.ycwl.basic.repository.FaceRepository;
|
||||
import com.ycwl.basic.service.printer.PrinterService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/mobile/puzzle/v1")
|
||||
@RequiredArgsConstructor
|
||||
public class AppPuzzleController {
|
||||
|
||||
private final PuzzleGenerationRecordMapper recordMapper;
|
||||
private final FaceRepository faceRepository;
|
||||
private final IPriceCalculationService iPriceCalculationService;
|
||||
private final PrinterService printerService;
|
||||
private final OrderBiz orderBiz;
|
||||
|
||||
/**
|
||||
* 根据faceId查询三拼图数量
|
||||
*/
|
||||
@GetMapping("/count/{faceId}")
|
||||
public ApiResponse<Integer> countByFaceId(@PathVariable("faceId") Long faceId) {
|
||||
if (faceId == null) {
|
||||
return ApiResponse.fail("faceId不能为空");
|
||||
}
|
||||
int count = recordMapper.countByFaceId(faceId);
|
||||
return ApiResponse.success(count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据faceId查询所有三拼图记录
|
||||
*/
|
||||
@GetMapping("/list/{faceId}")
|
||||
public ApiResponse<List<ContentPageVO>> listByFaceId(@PathVariable("faceId") Long faceId) {
|
||||
if (faceId == null) {
|
||||
return ApiResponse.fail("faceId不能为空");
|
||||
}
|
||||
List<PuzzleGenerationRecordEntity> records = recordMapper.listByFaceId(faceId);
|
||||
List<ContentPageVO> result = records.stream()
|
||||
.map(this::convertToContentPageVO)
|
||||
.collect(Collectors.toList());
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据recordId查询单个三拼图记录
|
||||
*/
|
||||
@GetMapping("/detail/{recordId}")
|
||||
public ApiResponse<ContentPageVO> getByRecordId(@PathVariable("recordId") Long recordId) {
|
||||
if (recordId == null) {
|
||||
return ApiResponse.fail("recordId不能为空");
|
||||
}
|
||||
PuzzleGenerationRecordEntity record = recordMapper.getById(recordId);
|
||||
if (record == null) {
|
||||
return ApiResponse.fail("未找到对应的拼图记录");
|
||||
}
|
||||
ContentPageVO result = convertToContentPageVO(record);
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据recordId下载拼图资源
|
||||
*/
|
||||
@GetMapping("/download/{recordId}")
|
||||
public ApiResponse<List<String>> download(@PathVariable("recordId") Long recordId) {
|
||||
if (recordId == null) {
|
||||
return ApiResponse.fail("recordId不能为空");
|
||||
}
|
||||
PuzzleGenerationRecordEntity record = recordMapper.getById(recordId);
|
||||
if (record == null) {
|
||||
return ApiResponse.fail("未找到对应的拼图记录");
|
||||
}
|
||||
String resultImageUrl = record.getResultImageUrl();
|
||||
if (resultImageUrl == null || resultImageUrl.isEmpty()) {
|
||||
return ApiResponse.fail("该拼图记录没有可用的图片URL");
|
||||
}
|
||||
return ApiResponse.success(Collections.singletonList(resultImageUrl));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据recordId查询拼图价格
|
||||
*/
|
||||
@GetMapping("/price/{recordId}")
|
||||
public ApiResponse<PriceCalculationResult> getPriceByRecordId(@PathVariable("recordId") Long recordId) {
|
||||
if (recordId == null) {
|
||||
return ApiResponse.fail("recordId不能为空");
|
||||
}
|
||||
PuzzleGenerationRecordEntity record = recordMapper.getById(recordId);
|
||||
if (record == null) {
|
||||
return ApiResponse.fail("未找到对应的拼图记录");
|
||||
}
|
||||
FaceEntity face = faceRepository.getFace(record.getFaceId());
|
||||
if (face == null) {
|
||||
return ApiResponse.fail("未找到对应的人脸信息");
|
||||
}
|
||||
|
||||
PriceCalculationRequest calculationRequest = new PriceCalculationRequest();
|
||||
ProductItem productItem = new ProductItem();
|
||||
productItem.setProductType(ProductType.PHOTO_LOG);
|
||||
productItem.setProductId(record.getTemplateId().toString());
|
||||
productItem.setPurchaseCount(1);
|
||||
productItem.setScenicId(face.getScenicId().toString());
|
||||
calculationRequest.setProducts(Collections.singletonList(productItem));
|
||||
calculationRequest.setUserId(face.getMemberId());
|
||||
calculationRequest.setFaceId(record.getFaceId());
|
||||
calculationRequest.setPreviewOnly(true); // 仅查询价格,不实际使用优惠
|
||||
PriceCalculationResult calculationResult = iPriceCalculationService.calculatePrice(calculationRequest);
|
||||
|
||||
return ApiResponse.success(calculationResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将拼图导入到打印列表
|
||||
*/
|
||||
@PostMapping("/import-to-print/{recordId}")
|
||||
public ApiResponse<Integer> importToPrint(@PathVariable("recordId") Long recordId) {
|
||||
if (recordId == null) {
|
||||
return ApiResponse.fail("recordId不能为空");
|
||||
}
|
||||
|
||||
// 查询拼图记录
|
||||
PuzzleGenerationRecordEntity record = recordMapper.getById(recordId);
|
||||
if (record == null) {
|
||||
return ApiResponse.fail("未找到对应的拼图记录");
|
||||
}
|
||||
|
||||
// 检查是否有图片URL
|
||||
String resultImageUrl = record.getResultImageUrl();
|
||||
if (resultImageUrl == null || resultImageUrl.isEmpty()) {
|
||||
return ApiResponse.fail("该拼图记录没有可用的图片URL");
|
||||
}
|
||||
|
||||
// 获取人脸信息
|
||||
FaceEntity face = faceRepository.getFace(record.getFaceId());
|
||||
if (face == null) {
|
||||
return ApiResponse.fail("未找到对应的人脸信息");
|
||||
}
|
||||
|
||||
// 调用服务添加到打印列表
|
||||
Integer memberPrintId = printerService.addUserPhotoFromPuzzle(
|
||||
face.getMemberId(),
|
||||
face.getScenicId(),
|
||||
record.getFaceId(),
|
||||
resultImageUrl,
|
||||
0L // 打印特有
|
||||
);
|
||||
|
||||
if (memberPrintId == null) {
|
||||
return ApiResponse.fail("添加到打印列表失败");
|
||||
}
|
||||
|
||||
return ApiResponse.success(memberPrintId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将PuzzleGenerationRecordEntity转换为ContentPageVO
|
||||
*/
|
||||
private ContentPageVO convertToContentPageVO(PuzzleGenerationRecordEntity record) {
|
||||
ContentPageVO vo = new ContentPageVO();
|
||||
|
||||
// 内容类型为3(拼图)
|
||||
vo.setContentType(3);
|
||||
|
||||
// 源素材类型为3(拼图)
|
||||
vo.setSourceType(3);
|
||||
vo.setGroup("拼图");
|
||||
|
||||
// 只要存在记录,lockType不为0(设置为-1表示已生成)
|
||||
vo.setLockType(-1);
|
||||
|
||||
// 通过faceId填充scenicId的信息
|
||||
FaceEntity face = faceRepository.getFace(record.getFaceId());
|
||||
if (record.getFaceId() != null) {
|
||||
vo.setScenicId(face.getScenicId());
|
||||
}
|
||||
|
||||
// contentId为生成记录id
|
||||
vo.setContentId(record.getId());
|
||||
|
||||
// templateCoverUrl和生成的图是一致的
|
||||
vo.setTemplateCoverUrl(record.getResultImageUrl());
|
||||
|
||||
// 设置模板ID
|
||||
vo.setTemplateId(record.getTemplateId());
|
||||
IsBuyRespVO isBuyScenic = orderBiz.isBuy(face.getScenicId(), face.getMemberId(), face.getId(), 5, face.getScenicId());
|
||||
if (isBuyScenic.isBuy()) {
|
||||
vo.setIsBuy(1);
|
||||
} else {
|
||||
IsBuyRespVO isBuyRespVO = orderBiz.isBuy(face.getScenicId(), face.getMemberId(), face.getId(), 5, record.getTemplateId());
|
||||
if (isBuyRespVO.isBuy()) {
|
||||
vo.setIsBuy(1);
|
||||
} else {
|
||||
vo.setIsBuy(0);
|
||||
PriceCalculationRequest calculationRequest = new PriceCalculationRequest();
|
||||
ProductItem productItem = new ProductItem();
|
||||
productItem.setProductType(ProductType.PHOTO_LOG);
|
||||
productItem.setProductId(record.getTemplateId().toString());
|
||||
productItem.setPurchaseCount(1);
|
||||
productItem.setScenicId(face.getScenicId().toString());
|
||||
calculationRequest.setProducts(Collections.singletonList(productItem));
|
||||
calculationRequest.setUserId(face.getMemberId());
|
||||
calculationRequest.setFaceId(record.getFaceId());
|
||||
calculationRequest.setPreviewOnly(true); // 仅查询价格,不实际使用优惠
|
||||
PriceCalculationResult calculationResult = iPriceCalculationService.calculatePrice(calculationRequest);
|
||||
if (calculationResult.getFinalAmount().compareTo(BigDecimal.ZERO) > 0) {
|
||||
vo.setFreeCount(0);
|
||||
} else {
|
||||
vo.setFreeCount(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.ycwl.basic.controller.mobile;
|
||||
|
||||
import com.ycwl.basic.annotation.IgnoreToken;
|
||||
import com.ycwl.basic.constant.BaseContextHandler;
|
||||
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.questionnaire.QuestionnaireResponse;
|
||||
import com.ycwl.basic.integration.questionnaire.service.QuestionnaireIntegrationService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 移动端问卷接口控制器
|
||||
*
|
||||
* @author Claude Code
|
||||
* @date 2025-09-05
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/mobile/questionnaire/v1")
|
||||
@RequiredArgsConstructor
|
||||
public class AppQuestionnaireController {
|
||||
|
||||
private final QuestionnaireIntegrationService questionnaireIntegrationService;
|
||||
|
||||
/**
|
||||
* 获取问卷详情
|
||||
* 包含问卷基本信息和所有题目
|
||||
*/
|
||||
@IgnoreToken
|
||||
@GetMapping("/{id}")
|
||||
public ApiResponse<QuestionnaireResponse> getQuestionnaire(@PathVariable Long id) {
|
||||
log.info("移动端获取问卷详情, id: {}", id);
|
||||
try {
|
||||
QuestionnaireResponse questionnaire = questionnaireIntegrationService.getQuestionnaire(id);
|
||||
|
||||
// 检查问卷状态,只有已发布的问卷才能被移动端访问
|
||||
if (questionnaire.getStatus() != 2) {
|
||||
return ApiResponse.fail("问卷未发布或已停止");
|
||||
}
|
||||
|
||||
return ApiResponse.success(questionnaire);
|
||||
} catch (Exception e) {
|
||||
log.error("移动端获取问卷详情失败, id: {}", id, e);
|
||||
return ApiResponse.fail("获取问卷详情失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交问卷答案
|
||||
*/
|
||||
@PostMapping("/{id}/submit")
|
||||
public ApiResponse<ResponseDetailResponse> submitAnswer(
|
||||
@PathVariable Long id,
|
||||
@Valid @RequestBody SubmitAnswerRequest request) {
|
||||
|
||||
String userId = BaseContextHandler.getUserId();
|
||||
log.info("移动端提交问卷答案, questionnaireId: {}, userId: {}, answers count: {}",
|
||||
id, userId, request.getAnswers() != null ? request.getAnswers().size() : 0);
|
||||
|
||||
try {
|
||||
// 设置问卷ID和用户ID
|
||||
request.setQuestionnaireId(id);
|
||||
request.setUserId(userId);
|
||||
|
||||
ResponseDetailResponse response = questionnaireIntegrationService.submitAnswer(request);
|
||||
return ApiResponse.success(response);
|
||||
} catch (Exception e) {
|
||||
log.error("移动端提交问卷答案失败, questionnaireId: {}, userId: {}", id, userId, e);
|
||||
return ApiResponse.fail("提交问卷答案失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import com.ycwl.basic.model.mobile.scenic.ScenicAppVO;
|
||||
import com.ycwl.basic.model.mobile.scenic.ScenicDeviceCountVO;
|
||||
import com.ycwl.basic.model.mobile.scenic.ScenicIndexVO;
|
||||
import com.ycwl.basic.model.mobile.scenic.content.ContentPageVO;
|
||||
import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity;
|
||||
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
|
||||
import com.ycwl.basic.model.pc.scenic.entity.ScenicEntity;
|
||||
import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery;
|
||||
import com.ycwl.basic.model.pc.scenic.resp.ScenicConfigResp;
|
||||
@@ -48,6 +48,7 @@ public class AppScenicController {
|
||||
add("3932535453961555968");
|
||||
add("3936121342868459520");
|
||||
add("3936940597855784960");
|
||||
add("4049850382325780480");
|
||||
}};
|
||||
|
||||
// 分页查询景区列表
|
||||
@@ -70,25 +71,25 @@ public class AppScenicController {
|
||||
@GetMapping("/{id}/config")
|
||||
@IgnoreToken
|
||||
public ApiResponse<ScenicConfigResp> getConfig(@PathVariable Long id){
|
||||
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(id);
|
||||
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(id);
|
||||
ScenicConfigResp resp = new ScenicConfigResp();
|
||||
resp.setBookRoutine(scenicConfig.getBookRoutine());
|
||||
resp.setForceFinishTime(scenicConfig.getForceFinishTime());
|
||||
resp.setTourTime(scenicConfig.getTourTime());
|
||||
resp.setSampleStoreDay(scenicConfig.getSampleStoreDay());
|
||||
resp.setFaceStoreDay(scenicConfig.getFaceStoreDay());
|
||||
resp.setVideoStoreDay(scenicConfig.getVideoStoreDay());
|
||||
resp.setAllFree(scenicConfig.getAllFree());
|
||||
resp.setDisableSourceVideo(scenicConfig.getDisableSourceVideo());
|
||||
resp.setDisableSourceImage(scenicConfig.getDisableSourceImage());
|
||||
resp.setAntiScreenRecordType(scenicConfig.getAntiScreenRecordType());
|
||||
resp.setVideoSourceStoreDay(scenicConfig.getVideoSourceStoreDay());
|
||||
resp.setImageSourceStoreDay(scenicConfig.getImageSourceStoreDay());
|
||||
resp.setUserSourceExpireDay(scenicConfig.getUserSourceExpireDay());
|
||||
resp.setBrokerDirectRate(scenicConfig.getBrokerDirectRate());
|
||||
resp.setVideoSourcePackHint(scenicConfig.getVideoSourcePackHint());
|
||||
resp.setImageSourcePackHint(scenicConfig.getImageSourcePackHint());
|
||||
resp.setVoucherEnable(scenicConfig.getVoucherEnable());
|
||||
resp.setWatermarkUrl(scenicConfig.getString("watermark_url"));
|
||||
resp.setVideoStoreDay(scenicConfig.getInteger("video_store_day"));
|
||||
resp.setAntiScreenRecordType(scenicConfig.getInteger("anti_screen_record_type"));
|
||||
resp.setGroupingEnable(scenicConfig.getBoolean("grouping_enable", false));
|
||||
resp.setVoucherEnable(scenicConfig.getBoolean("voucher_enable", false));
|
||||
resp.setShowPhotoWhenWaiting(scenicConfig.getBoolean("show_photo_when_waiting", false));
|
||||
resp.setImageSourcePackHint(scenicConfig.getString("image_source_pack_hint"));
|
||||
resp.setVideoSourcePackHint(scenicConfig.getString("video_source_pack_hint"));
|
||||
resp.setShareBeforeBuy(scenicConfig.getBoolean("share_before_buy"));
|
||||
resp.setFaceSelectFirst(scenicConfig.getBoolean("face_select_first", false));
|
||||
resp.setPrintEnableSource(scenicConfig.getBoolean("print_enable_source", true));
|
||||
resp.setPrintForceFaceUpload(scenicConfig.getBoolean("print_force_face_upload", false));
|
||||
resp.setPrintEnableManual(scenicConfig.getBoolean("print_enable_manual", true));
|
||||
resp.setSceneMode(scenicConfig.getInteger("scene_mode", 0));
|
||||
resp.setPrintEnable(scenicConfig.getBoolean("print_enable", false));
|
||||
resp.setShowMyPagePaid(scenicConfig.getBoolean("show_my_page_paid", true));
|
||||
resp.setShowMyPageUnpaid(scenicConfig.getBoolean("show_my_page_unpaid", true));
|
||||
return ApiResponse.success(resp);
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ public class AppTaskController {
|
||||
|
||||
@PostMapping("/submit")
|
||||
public ApiResponse<String> submitVideoTask(@RequestBody VideoTaskReq videoTaskReq) {
|
||||
taskService.createTaskByFaceIdAndTempalteId(videoTaskReq.getFaceId(),videoTaskReq.getTemplateId(),0);
|
||||
taskService.createTaskByFaceIdAndTemplateId(videoTaskReq.getFaceId(),videoTaskReq.getTemplateId(),false);
|
||||
return ApiResponse.success("成功");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,93 @@
|
||||
package com.ycwl.basic.controller.mobile;
|
||||
|
||||
import com.ycwl.basic.constant.BaseContextHandler;
|
||||
import com.ycwl.basic.model.mobile.video.dto.VideoViewPermissionDTO;
|
||||
import com.ycwl.basic.model.task.req.VideoInfoReq;
|
||||
import com.ycwl.basic.repository.VideoRepository;
|
||||
import com.ycwl.basic.service.mobile.VideoViewPermissionService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Deprecated
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/mobile/video/v1")
|
||||
public class AppVideoController {
|
||||
|
||||
@Autowired
|
||||
private VideoRepository videoRepository;
|
||||
|
||||
@Autowired
|
||||
private VideoViewPermissionService videoViewPermissionService;
|
||||
|
||||
@PostMapping("/{videoId}/updateMeta")
|
||||
public void updateMeta(@PathVariable("videoId") Long videoId, @RequestBody VideoInfoReq req) {
|
||||
videoRepository.updateMeta(videoId, req);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录用户查看视频并返回权限信息
|
||||
*
|
||||
* @param videoId 视频ID
|
||||
* @return 查看权限信息
|
||||
*/
|
||||
@PostMapping("/{videoId}/recordView")
|
||||
public ApiResponse<VideoViewPermissionDTO> recordView(@PathVariable("videoId") Long videoId) {
|
||||
try {
|
||||
String userIdStr = BaseContextHandler.getUserId();
|
||||
if (userIdStr == null || userIdStr.isEmpty()) {
|
||||
log.warn("用户未登录,无法记录查看: videoId={}", videoId);
|
||||
return ApiResponse.fail("用户未登录");
|
||||
}
|
||||
|
||||
Long userId = Long.valueOf(userIdStr);
|
||||
log.debug("记录用户查看视频: userId={}, videoId={}", userId, videoId);
|
||||
|
||||
VideoViewPermissionDTO permission = videoViewPermissionService.checkAndRecordView(userId, videoId);
|
||||
return ApiResponse.success(permission);
|
||||
|
||||
} catch (NumberFormatException e) {
|
||||
log.error("用户ID格式错误: userId={}, videoId={}", BaseContextHandler.getUserId(), videoId, e);
|
||||
return ApiResponse.fail("用户信息无效");
|
||||
} catch (Exception e) {
|
||||
log.error("记录用户查看视频失败: videoId={}", videoId, e);
|
||||
return ApiResponse.fail("记录查看失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户查看权限(不记录查看次数)
|
||||
*
|
||||
* @param videoId 视频ID
|
||||
* @return 查看权限信息
|
||||
*/
|
||||
@GetMapping("/{videoId}/checkPermission")
|
||||
public ApiResponse<VideoViewPermissionDTO> checkPermission(@PathVariable("videoId") Long videoId) {
|
||||
try {
|
||||
String userIdStr = BaseContextHandler.getUserId();
|
||||
if (userIdStr == null || userIdStr.isEmpty()) {
|
||||
log.warn("用户未登录,无法查看权限: videoId={}", videoId);
|
||||
return ApiResponse.fail("用户未登录");
|
||||
}
|
||||
|
||||
Long userId = Long.valueOf(userIdStr);
|
||||
log.debug("检查用户查看权限: userId={}, videoId={}", userId, videoId);
|
||||
|
||||
VideoViewPermissionDTO permission = videoViewPermissionService.checkViewPermission(userId, videoId);
|
||||
return ApiResponse.success(permission);
|
||||
|
||||
} catch (NumberFormatException e) {
|
||||
log.error("用户ID格式错误: userId={}, videoId={}", BaseContextHandler.getUserId(), videoId, e);
|
||||
return ApiResponse.fail("用户信息无效");
|
||||
} catch (Exception e) {
|
||||
log.error("检查用户查看权限失败: videoId={}", videoId, e);
|
||||
return ApiResponse.fail("权限检查失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,9 +64,6 @@ public class AppVoucherController {
|
||||
if (face == null) {
|
||||
throw new BaseException("请选择人脸");
|
||||
}
|
||||
if (!face.getMemberId().equals(Long.valueOf(BaseContextHandler.getUserId()))) {
|
||||
throw new BaseException("自动领取失败");
|
||||
}
|
||||
req.setScenicId(face.getScenicId());
|
||||
VoucherCodeResp result = voucherCodeService.claimVoucher(req);
|
||||
return ApiResponse.success(result);
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
package com.ycwl.basic.controller.mobile;
|
||||
|
||||
|
||||
import com.ycwl.basic.annotation.IgnoreToken;
|
||||
import com.ycwl.basic.repository.ScenicRepository;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Author: songmingsong
|
||||
* @CreateTime: 2024-12-06
|
||||
* @Description: 微信消息模板通知
|
||||
* @Version: 1.0
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/mobile/wx/notify/v1")
|
||||
// 微信消息模板通知
|
||||
public class AppWxNotifyController {
|
||||
@Autowired
|
||||
private ScenicRepository scenicRepository;
|
||||
//
|
||||
// @ApiOperation(value = "通知", notes = "通知")
|
||||
// @PostMapping("/pushMessage")
|
||||
// @IgnoreToken
|
||||
// public ApiResponse<?> pushMessage(@RequestBody WechatMessageSubscribeForm req) {
|
||||
// JSONObject resJson = wxNotifyService.pushMessage(req);
|
||||
// return ApiResponse.success(resJson);
|
||||
// }
|
||||
|
||||
@GetMapping({"/getIds", "/"})
|
||||
@IgnoreToken
|
||||
public ApiResponse<List<String>> getIds() {
|
||||
return ApiResponse.success(new ArrayList<>() {{
|
||||
add("5b8vTm7kvwYubqDxb3dxBs0BqxMsgVgGw573aahTEd8");
|
||||
add("vPIzbkA0x4mMj-vdbWx6_45e8juWXzs3FGYnDsIPv3A");
|
||||
add("HB1vp-0BXc2WyYeoYN3a3GuZV9HtPLXUTT7blCBq9eY");
|
||||
}});
|
||||
}
|
||||
|
||||
@GetMapping("/{scenicId}")
|
||||
@IgnoreToken
|
||||
public ApiResponse<List<String>> getIds(@PathVariable("scenicId") Long scenicId) {
|
||||
return ApiResponse.success(new ArrayList<>() {{
|
||||
String videoGeneratedTemplateId = scenicRepository.getVideoGeneratedTemplateId(scenicId);
|
||||
if (StringUtils.isNotBlank(videoGeneratedTemplateId)) {
|
||||
add(videoGeneratedTemplateId);
|
||||
}
|
||||
String videoDownloadTemplateId = scenicRepository.getVideoDownloadTemplateId(scenicId);
|
||||
if (StringUtils.isNotBlank(videoDownloadTemplateId)) {
|
||||
add(videoDownloadTemplateId);
|
||||
}
|
||||
String videoPreExpireTemplateId = scenicRepository.getVideoPreExpireTemplateId(scenicId);
|
||||
if (StringUtils.isNotBlank(videoPreExpireTemplateId)) {
|
||||
add(videoPreExpireTemplateId);
|
||||
}
|
||||
}});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -56,12 +56,4 @@ public class AppStatisticsController {
|
||||
return statisticsService.userConversionFunnel(query);
|
||||
}
|
||||
|
||||
// 统计数据记录
|
||||
@PostMapping("/addStatistics")
|
||||
@IgnoreToken
|
||||
public ApiResponse<String> addStatistics(@RequestBody StatisticsRecordAddReq req) {
|
||||
|
||||
return statisticsService.addStatistics(req);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
package com.ycwl.basic.controller.mobile.notify;
|
||||
|
||||
import com.ycwl.basic.model.mobile.notify.req.NotificationAuthRecordReq;
|
||||
import com.ycwl.basic.model.mobile.notify.resp.NotificationAuthRecordResp;
|
||||
import com.ycwl.basic.model.mobile.notify.resp.ScenicTemplateAuthResp;
|
||||
import com.ycwl.basic.service.UserNotificationAuthorizationService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import com.ycwl.basic.utils.JwtTokenUtil;
|
||||
import com.ycwl.basic.repository.ScenicRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户通知授权记录Controller (移动端API)
|
||||
* 只提供用户主动授权记录功能,支持批量授权,其他检查和消费功能由系统内部调用
|
||||
*
|
||||
* @Author: System
|
||||
* @Date: 2024/12/28
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/mobile/notify/auth")
|
||||
@Slf4j
|
||||
public class UserNotificationAuthController {
|
||||
|
||||
@Autowired
|
||||
private UserNotificationAuthorizationService userNotificationAuthorizationService;
|
||||
|
||||
@Autowired
|
||||
private ScenicRepository scenicRepository;
|
||||
|
||||
/**
|
||||
* 记录用户通知授权 - 支持批量授权
|
||||
* 用户主动同意通知授权时调用
|
||||
*/
|
||||
@PostMapping("/record")
|
||||
public ApiResponse<NotificationAuthRecordResp> recordAuthorization(
|
||||
@RequestBody NotificationAuthRecordReq req) {
|
||||
log.debug("记录用户通知授权: templateIds={}, scenicId={}", req.getTemplateIds(), req.getScenicId());
|
||||
|
||||
try {
|
||||
// 获取当前用户ID
|
||||
Long memberId = JwtTokenUtil.getWorker().getUserId();
|
||||
|
||||
// 调用批量授权记录方法
|
||||
List<UserNotificationAuthorizationService.AuthorizationRecord> records =
|
||||
userNotificationAuthorizationService.batchRecordAuthorization(
|
||||
memberId, req.getTemplateIds(), req.getScenicId());
|
||||
|
||||
NotificationAuthRecordResp resp = new NotificationAuthRecordResp();
|
||||
|
||||
// 转换响应数据
|
||||
List<NotificationAuthRecordResp.AuthorizationRecord> successRecords = new ArrayList<>();
|
||||
List<String> failedTemplateIds = new ArrayList<>();
|
||||
List<String> failureReasons = new ArrayList<>();
|
||||
|
||||
for (UserNotificationAuthorizationService.AuthorizationRecord record : records) {
|
||||
if (record.isSuccess()) {
|
||||
NotificationAuthRecordResp.AuthorizationRecord successRecord =
|
||||
new NotificationAuthRecordResp.AuthorizationRecord();
|
||||
successRecord.setId(record.getId());
|
||||
successRecord.setTemplateId(record.getTemplateId());
|
||||
successRecord.setScenicId(record.getScenicId());
|
||||
successRecord.setAuthorizationCount(record.getAuthorizationCount());
|
||||
successRecord.setConsumedCount(record.getConsumedCount());
|
||||
successRecord.setRemainingCount(record.getRemainingCount());
|
||||
successRecord.setLastAuthorizedTime(record.getLastAuthorizedTime());
|
||||
successRecord.setLastConsumedTime(record.getLastConsumedTime());
|
||||
successRecord.setStatus(record.getStatus());
|
||||
successRecord.setCreateTime(record.getCreateTime());
|
||||
successRecords.add(successRecord);
|
||||
} else {
|
||||
failedTemplateIds.add(record.getTemplateId());
|
||||
failureReasons.add(record.getFailureReason());
|
||||
}
|
||||
}
|
||||
|
||||
resp.setAllSuccess(CollectionUtils.isEmpty(failedTemplateIds));
|
||||
resp.setSuccessRecords(successRecords);
|
||||
resp.setFailedTemplateIds(failedTemplateIds);
|
||||
resp.setFailureReasons(failureReasons);
|
||||
|
||||
return ApiResponse.success(resp);
|
||||
} catch (Exception e) {
|
||||
log.error("记录用户通知授权失败", e);
|
||||
return ApiResponse.fail("记录授权失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取景区通知模板ID及用户授权余额
|
||||
* 复制AppWxNotifyController中的逻辑,并额外返回用户对应的授权余额
|
||||
*/
|
||||
@GetMapping("/{scenicId}/templates")
|
||||
public ApiResponse<ScenicTemplateAuthResp> getScenicTemplatesWithAuth(@PathVariable("scenicId") Long scenicId) {
|
||||
log.debug("获取景区通知模板ID及用户授权余额: scenicId={}", scenicId);
|
||||
|
||||
try {
|
||||
// 获取当前用户ID
|
||||
Long memberId = JwtTokenUtil.getWorker().getUserId();
|
||||
|
||||
// 获取景区的所有模板ID(复制自AppWxNotifyController的逻辑)
|
||||
List<String> templateIds = new ArrayList<>() {{
|
||||
String videoGeneratedTemplateId = scenicRepository.getVideoGeneratedTemplateId(scenicId);
|
||||
if (StringUtils.isNotBlank(videoGeneratedTemplateId)) {
|
||||
add(videoGeneratedTemplateId);
|
||||
}
|
||||
String videoDownloadTemplateId = scenicRepository.getVideoDownloadTemplateId(scenicId);
|
||||
if (StringUtils.isNotBlank(videoDownloadTemplateId)) {
|
||||
add(videoDownloadTemplateId);
|
||||
}
|
||||
String videoPreExpireTemplateId = scenicRepository.getVideoPreExpireTemplateId(scenicId);
|
||||
if (StringUtils.isNotBlank(videoPreExpireTemplateId)) {
|
||||
add(videoPreExpireTemplateId);
|
||||
}
|
||||
}};
|
||||
|
||||
// 构建响应对象
|
||||
ScenicTemplateAuthResp resp = new ScenicTemplateAuthResp();
|
||||
resp.setScenicId(scenicId);
|
||||
|
||||
// 查询每个模板的授权余额信息
|
||||
List<ScenicTemplateAuthResp.TemplateAuthInfo> templateAuthInfos = new ArrayList<>();
|
||||
for (String templateId : templateIds) {
|
||||
ScenicTemplateAuthResp.TemplateAuthInfo templateAuthInfo =
|
||||
new ScenicTemplateAuthResp.TemplateAuthInfo();
|
||||
templateAuthInfo.setTemplateId(templateId);
|
||||
|
||||
if (templateId.equals(scenicRepository.getVideoGeneratedTemplateId(scenicId))) {
|
||||
templateAuthInfo.setTitle("视频生成通知");
|
||||
templateAuthInfo.setDescription("当视频生成完成时,我们将提醒您");
|
||||
} else if (templateId.equals(scenicRepository.getVideoDownloadTemplateId(scenicId))) {
|
||||
templateAuthInfo.setTitle("视频下载通知");
|
||||
templateAuthInfo.setDescription("当您的视频未购买时,我们将提醒您");
|
||||
} else if (templateId.equals(scenicRepository.getVideoPreExpireTemplateId(scenicId))) {
|
||||
templateAuthInfo.setTitle("视频即将过期通知");
|
||||
templateAuthInfo.setDescription("当您的视频即将过期时,我们将提醒您及时下载");
|
||||
} else {
|
||||
templateAuthInfo.setTitle("未知模板类型");
|
||||
templateAuthInfo.setDescription("未知的模板类型");
|
||||
}
|
||||
|
||||
// 获取授权详情
|
||||
try {
|
||||
com.ycwl.basic.model.pc.notify.entity.UserNotificationAuthorizationEntity authEntity =
|
||||
userNotificationAuthorizationService.checkAuthorization(memberId, templateId, scenicId);
|
||||
|
||||
if (authEntity != null) {
|
||||
templateAuthInfo.setAuthorizationCount(authEntity.getAuthorizationCount());
|
||||
templateAuthInfo.setConsumedCount(authEntity.getConsumedCount());
|
||||
templateAuthInfo.setRemainingCount(authEntity.getRemainingCount());
|
||||
templateAuthInfo.setHasAuthorization(authEntity.getRemainingCount() != null && authEntity.getRemainingCount() > 0);
|
||||
} else {
|
||||
// 没有授权记录
|
||||
templateAuthInfo.setAuthorizationCount(0);
|
||||
templateAuthInfo.setConsumedCount(0);
|
||||
templateAuthInfo.setRemainingCount(0);
|
||||
templateAuthInfo.setHasAuthorization(false);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("获取模板授权信息失败: templateId={}, scenicId={}, memberId={}, error={}",
|
||||
templateId, scenicId, memberId, e.getMessage());
|
||||
|
||||
// 获取失败时设置为无授权
|
||||
templateAuthInfo.setAuthorizationCount(0);
|
||||
templateAuthInfo.setConsumedCount(0);
|
||||
templateAuthInfo.setRemainingCount(0);
|
||||
templateAuthInfo.setHasAuthorization(false);
|
||||
}
|
||||
|
||||
templateAuthInfos.add(templateAuthInfo);
|
||||
}
|
||||
|
||||
resp.setTemplates(templateAuthInfos);
|
||||
|
||||
log.debug("成功获取景区通知模板ID及用户授权余额: scenicId={}, templateCount={}, memberId={}",
|
||||
scenicId, templateIds.size(), memberId);
|
||||
|
||||
return ApiResponse.success(resp);
|
||||
} catch (Exception e) {
|
||||
log.error("获取景区通知模板ID及用户授权余额失败: scenicId={}", scenicId, e);
|
||||
return ApiResponse.fail("获取授权信息失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.ycwl.basic.controller.monitor;
|
||||
|
||||
import com.ycwl.basic.integration.kafka.scheduler.AccountFaceSchedulerManager;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 人脸识别监控接口
|
||||
* 提供调度器状态查询功能
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/monitor/face-recognition")
|
||||
@RequiredArgsConstructor
|
||||
public class FaceRecognitionMonitorController {
|
||||
|
||||
private final AccountFaceSchedulerManager schedulerManager;
|
||||
|
||||
/**
|
||||
* 获取所有账号的调度器统计信息
|
||||
* <p>
|
||||
* 示例返回:
|
||||
* {
|
||||
* "LTAI5xxx": {
|
||||
* "accountKey": "LTAI5xxx",
|
||||
* "cloudType": "ALI",
|
||||
* "activeThreads": 3,
|
||||
* "executorQueueSize": 12,
|
||||
* "schedulerQueueSize": 45
|
||||
* },
|
||||
* "245xxx": {
|
||||
* "accountKey": "245xxx",
|
||||
* "cloudType": "BAIDU",
|
||||
* "activeThreads": 8,
|
||||
* "executorQueueSize": 5,
|
||||
* "schedulerQueueSize": 20
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @return 所有账号的调度器状态
|
||||
*/
|
||||
@GetMapping("/schedulers")
|
||||
public ApiResponse<Map<String, AccountFaceSchedulerManager.AccountSchedulerStats>> getAllSchedulerStats() {
|
||||
return ApiResponse.success(schedulerManager.getAllStats());
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ public class CouponController {
|
||||
@GetMapping("/{scenicId}/goodsList")
|
||||
public ApiResponse<List<GoodsListRespVO>> scenicGoodsList(@PathVariable Long scenicId) {
|
||||
List<GoodsListRespVO> data = priceBiz.listGoodsByScenic(scenicId);
|
||||
data.add(new GoodsListRespVO(-1L, "一口价"));
|
||||
data.add(new GoodsListRespVO(-1L, "一口价", -1));
|
||||
return ApiResponse.success(data);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
package com.ycwl.basic.controller.pc;
|
||||
|
||||
import com.ycwl.basic.integration.scenic.dto.config.DefaultConfigDTO;
|
||||
import com.ycwl.basic.integration.scenic.service.DefaultConfigIntegrationService;
|
||||
import com.ycwl.basic.utils.ApiConst;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 默认配置管理控制器
|
||||
* 提供默认配置的增删查改功能
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/default-config")
|
||||
@RequiredArgsConstructor
|
||||
public class DefaultConfigController {
|
||||
|
||||
private final DefaultConfigIntegrationService defaultConfigIntegrationService;
|
||||
|
||||
/**
|
||||
* 获取默认配置列表
|
||||
*/
|
||||
@GetMapping("/")
|
||||
public ApiResponse<List<DefaultConfigDTO>> listDefaultConfigs() {
|
||||
log.info("获取默认配置列表");
|
||||
try {
|
||||
List<DefaultConfigDTO> configs = defaultConfigIntegrationService.listDefaultConfigs();
|
||||
return ApiResponse.success(configs);
|
||||
} catch (Exception e) {
|
||||
log.error("获取默认配置列表失败", e);
|
||||
return ApiResponse.fail("获取默认配置列表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据配置键获取默认配置
|
||||
*/
|
||||
@GetMapping("/{configKey}")
|
||||
public ApiResponse<DefaultConfigDTO> getDefaultConfig(@PathVariable String configKey) {
|
||||
log.info("获取默认配置, configKey: {}", configKey);
|
||||
try {
|
||||
DefaultConfigDTO config = defaultConfigIntegrationService.getDefaultConfig(configKey);
|
||||
return ApiResponse.success(config);
|
||||
} catch (Exception e) {
|
||||
log.error("获取默认配置失败, configKey: {}", configKey, e);
|
||||
return ApiResponse.fail("获取默认配置失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建默认配置
|
||||
*/
|
||||
@PostMapping("/")
|
||||
public ApiResponse<DefaultConfigDTO> createDefaultConfig(@RequestBody DefaultConfigDTO request) {
|
||||
log.info("创建默认配置, configKey: {}", request.getConfigKey());
|
||||
try {
|
||||
DefaultConfigDTO config = defaultConfigIntegrationService.createDefaultConfig(request);
|
||||
return ApiResponse.success(config);
|
||||
} catch (Exception e) {
|
||||
log.error("创建默认配置失败, configKey: {}", request.getConfigKey(), e);
|
||||
return ApiResponse.fail("创建默认配置失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新默认配置
|
||||
*/
|
||||
@PutMapping("/{configKey}")
|
||||
public ApiResponse<DefaultConfigDTO> updateDefaultConfig(@PathVariable String configKey,
|
||||
@RequestBody DefaultConfigDTO request) {
|
||||
log.info("更新默认配置, configKey: {}", configKey);
|
||||
try {
|
||||
DefaultConfigDTO config = defaultConfigIntegrationService.updateDefaultConfig(configKey, request);
|
||||
return ApiResponse.success(config);
|
||||
} catch (Exception e) {
|
||||
log.error("更新默认配置失败, configKey: {}", configKey, e);
|
||||
return ApiResponse.fail("更新默认配置失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除默认配置
|
||||
*/
|
||||
@DeleteMapping("/{configKey}")
|
||||
public ApiResponse<Void> deleteDefaultConfig(@PathVariable String configKey) {
|
||||
log.info("删除默认配置, configKey: {}", configKey);
|
||||
try {
|
||||
defaultConfigIntegrationService.deleteDefaultConfig(configKey);
|
||||
return ApiResponse.buildResponse(ApiConst.Code.CODE_SUCCESS.code(), null, "删除成功");
|
||||
} catch (Exception e) {
|
||||
log.error("删除默认配置失败, configKey: {}", configKey, e);
|
||||
return ApiResponse.fail("删除默认配置失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package com.ycwl.basic.controller.pc;
|
||||
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
|
||||
import com.ycwl.basic.model.pc.device.req.DeviceAddOrUpdateReq;
|
||||
import com.ycwl.basic.model.pc.device.req.DeviceBatchSortRequest;
|
||||
import com.ycwl.basic.model.pc.device.req.DeviceReqQuery;
|
||||
import com.ycwl.basic.model.pc.device.req.DeviceSortRequest;
|
||||
import com.ycwl.basic.model.pc.device.resp.DeviceRespVO;
|
||||
import com.ycwl.basic.model.pc.template.req.TemplateSortRequest;
|
||||
import com.ycwl.basic.service.pc.DeviceService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* @Author:longbinbin
|
||||
* @Date:2024/12/2 16:13
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/device/v1")
|
||||
// 设备管理
|
||||
public class DeviceController {
|
||||
@Autowired
|
||||
private DeviceService deviceService;
|
||||
|
||||
// 设备分页查询
|
||||
@PostMapping("/page")
|
||||
public ApiResponse<PageInfo<DeviceRespVO>> pageQuery(@RequestBody DeviceReqQuery deviceReqQuery) {
|
||||
return deviceService.pageQuery(deviceReqQuery);
|
||||
}
|
||||
// 设备列表查询
|
||||
@PostMapping("/list")
|
||||
public ApiResponse list(@RequestBody DeviceReqQuery deviceReqQuery) {
|
||||
return deviceService.list(deviceReqQuery);
|
||||
}
|
||||
// 设备详情查询
|
||||
@GetMapping("/getDetails/{id}")
|
||||
public ApiResponse<DeviceRespVO> getDetails(@PathVariable("id") Long id) {
|
||||
return deviceService.getById(id);
|
||||
}
|
||||
// 新增或修改设备
|
||||
@PostMapping("/addOrUpdate")
|
||||
public ApiResponse addOrUpdate(@RequestBody DeviceAddOrUpdateReq deviceReqQuery) {
|
||||
return deviceService.addOrUpdate(deviceReqQuery);
|
||||
}
|
||||
// 删除设备
|
||||
@DeleteMapping("/delete/{id}")
|
||||
public ApiResponse delete(@PathVariable("id") Long id) {
|
||||
return deviceService.deleteById(id);
|
||||
}
|
||||
// 修改设备状态
|
||||
@PutMapping("/updateStatus/{id}")
|
||||
public ApiResponse updateStatus(@PathVariable("id") Long id) {
|
||||
return deviceService.updateStatus(id);
|
||||
}
|
||||
|
||||
// 排序设备
|
||||
@PostMapping("/sort")
|
||||
public ApiResponse<Boolean> sortDevice(@RequestBody DeviceSortRequest request) {
|
||||
return deviceService.sortDevice(request.getDeviceId(), request.getAfterDeviceId());
|
||||
}
|
||||
|
||||
@PostMapping("/scenic/{scenicId}/sortBatch")
|
||||
public ApiResponse<Boolean> sortDeviceBatch(@PathVariable("scenicId") Long scenicId, @RequestBody DeviceBatchSortRequest request) {
|
||||
return deviceService.batchSort(scenicId, request);
|
||||
}
|
||||
|
||||
@GetMapping("/config/{id}")
|
||||
public ApiResponse<DeviceConfigEntity> getConfig(@PathVariable("id") Long id) {
|
||||
return ApiResponse.success(deviceService.getConfig(id));
|
||||
}
|
||||
|
||||
@PostMapping("/saveConfig/{configId}")
|
||||
public ApiResponse saveConfig(@PathVariable("configId") Long configId, @RequestBody DeviceConfigEntity deviceConfigEntity) {
|
||||
deviceService.saveConfig(configId, deviceConfigEntity);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
package com.ycwl.basic.controller.pc;
|
||||
|
||||
import com.ycwl.basic.integration.device.dto.config.*;
|
||||
import com.ycwl.basic.integration.common.response.PageResponse;
|
||||
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.DeviceIntegrationService;
|
||||
import com.ycwl.basic.integration.device.service.DeviceStatusIntegrationService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 设备管理 V2 版本控制器 - 基于 zt-device 集成服务
|
||||
*
|
||||
* @author Claude Code
|
||||
* @date 2025-09-01
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/device/v2")
|
||||
@RequiredArgsConstructor
|
||||
public class DeviceV2Controller {
|
||||
|
||||
private final DeviceIntegrationService deviceIntegrationService;
|
||||
private final DeviceConfigIntegrationService deviceConfigIntegrationService;
|
||||
private final DeviceStatusIntegrationService deviceStatusIntegrationService;
|
||||
|
||||
// ========== 设备基础 CRUD 操作 ==========
|
||||
|
||||
/**
|
||||
* 设备V2核心信息分页列表
|
||||
*/
|
||||
@GetMapping("/")
|
||||
public ApiResponse<PageResponse<DeviceV2DTO>> listDevices(@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<DeviceV2DTO> response = deviceIntegrationService.listDevices(page, pageSize, name, no, type, isActive, scenicId, null);
|
||||
return ApiResponse.success(response);
|
||||
} catch (Exception e) {
|
||||
log.error("分页查询设备核心信息列表失败", e);
|
||||
return ApiResponse.fail("分页查询设备列表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取设备信息
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public ApiResponse<DeviceV2DTO> getDevice(@PathVariable Long id) {
|
||||
try {
|
||||
DeviceV2DTO device = deviceIntegrationService.getDevice(id);
|
||||
return ApiResponse.success(device);
|
||||
} catch (Exception e) {
|
||||
log.error("获取设备信息失败, id: {}", id, e);
|
||||
return ApiResponse.fail("获取设备信息失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据设备编号获取设备信息
|
||||
*/
|
||||
@GetMapping("/no/{no}")
|
||||
public ApiResponse<DeviceV2DTO> getDeviceByNo(@PathVariable String no) {
|
||||
try {
|
||||
DeviceV2DTO device = deviceIntegrationService.getDeviceByNo(no);
|
||||
return ApiResponse.success(device);
|
||||
} catch (Exception e) {
|
||||
log.error("根据设备编号获取设备信息失败, no: {}", no, e);
|
||||
return ApiResponse.fail("根据设备编号获取设备信息失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据设备ID获取设备在线状态
|
||||
*/
|
||||
@GetMapping("/{id}/status")
|
||||
public ApiResponse<DeviceStatusDTO> getDeviceOnlineStatus(@PathVariable Long id) {
|
||||
log.info("获取设备在线状态, deviceId: {}", id);
|
||||
try {
|
||||
// 首先获取设备信息以获得设备编号
|
||||
DeviceV2DTO device = deviceIntegrationService.getDevice(id);
|
||||
if (device == null) {
|
||||
return ApiResponse.fail("设备不存在");
|
||||
}
|
||||
|
||||
// 使用设备编号查询在线状态
|
||||
DeviceStatusDTO onlineStatus = deviceStatusIntegrationService.getDeviceStatus(device.getNo());
|
||||
return ApiResponse.success(onlineStatus);
|
||||
} catch (Exception e) {
|
||||
log.error("获取设备在线状态失败, deviceId: {}", id, e);
|
||||
return ApiResponse.fail("获取设备在线状态失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建设备
|
||||
*/
|
||||
@PostMapping("/")
|
||||
public ApiResponse<DeviceV2DTO> createDevice(@Valid @RequestBody CreateDeviceRequest request) {
|
||||
log.info("创建设备, name: {}, no: {}, type: {}, sort: {}",
|
||||
request.getName(), request.getNo(), request.getType(), request.getSort());
|
||||
try {
|
||||
DeviceV2DTO device = deviceIntegrationService.createDevice(request);
|
||||
return ApiResponse.success(device);
|
||||
} catch (Exception e) {
|
||||
log.error("创建设备失败", e);
|
||||
return ApiResponse.fail("创建设备失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建IPC摄像头设备(快捷方法)
|
||||
*/
|
||||
@PostMapping("/ipc")
|
||||
public ApiResponse<DeviceV2DTO> createIpcDevice(@RequestBody Map<String, Object> request) {
|
||||
String name = (String) request.get("name");
|
||||
String deviceNo = (String) request.get("no");
|
||||
Long scenicId = Long.valueOf(request.get("scenicId").toString());
|
||||
Integer sort = request.get("sort") != null ? Integer.valueOf(request.get("sort").toString()) : null;
|
||||
|
||||
log.info("创建IPC摄像头设备, name: {}, no: {}, scenicId: {}, sort: {}", name, deviceNo, scenicId, sort);
|
||||
try {
|
||||
DeviceV2DTO device;
|
||||
if (sort != null) {
|
||||
device = deviceIntegrationService.createIpcDeviceWithSort(name, deviceNo, scenicId, sort);
|
||||
} else {
|
||||
device = deviceIntegrationService.createIpcDevice(name, deviceNo, scenicId);
|
||||
}
|
||||
return ApiResponse.success(device);
|
||||
} catch (Exception e) {
|
||||
log.error("创建IPC摄像头设备失败", e);
|
||||
return ApiResponse.fail("创建IPC摄像头设备失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建自定义设备(快捷方法)
|
||||
*/
|
||||
@PostMapping("/custom")
|
||||
public ApiResponse<DeviceV2DTO> createCustomDevice(@RequestBody Map<String, Object> request) {
|
||||
String name = (String) request.get("name");
|
||||
String deviceNo = (String) request.get("no");
|
||||
Long scenicId = Long.valueOf(request.get("scenicId").toString());
|
||||
Integer sort = request.get("sort") != null ? Integer.valueOf(request.get("sort").toString()) : null;
|
||||
|
||||
log.info("创建自定义设备, name: {}, no: {}, scenicId: {}, sort: {}", name, deviceNo, scenicId, sort);
|
||||
try {
|
||||
DeviceV2DTO device;
|
||||
if (sort != null) {
|
||||
device = deviceIntegrationService.createCustomDeviceWithSort(name, deviceNo, scenicId, sort);
|
||||
} else {
|
||||
device = deviceIntegrationService.createCustomDevice(name, deviceNo, scenicId);
|
||||
}
|
||||
return ApiResponse.success(device);
|
||||
} catch (Exception e) {
|
||||
log.error("创建自定义设备失败", e);
|
||||
return ApiResponse.fail("创建自定义设备失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新设备信息
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
public ApiResponse<String> updateDevice(@PathVariable Long id, @Valid @RequestBody UpdateDeviceRequest request) {
|
||||
log.info("更新设备信息, id: {}", id);
|
||||
try {
|
||||
deviceIntegrationService.updateDevice(id, request);
|
||||
return ApiResponse.success("设备信息更新成功");
|
||||
} catch (Exception e) {
|
||||
log.error("更新设备信息失败, id: {}", id, e);
|
||||
return ApiResponse.fail("更新设备信息失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新设备排序
|
||||
*/
|
||||
@PutMapping("/{id}/sort")
|
||||
public ApiResponse<String> updateDeviceSort(@PathVariable Long id, @RequestBody Map<String, Integer> request) {
|
||||
Integer sort = request.get("sort");
|
||||
log.info("更新设备排序, id: {}, sort: {}", id, sort);
|
||||
try {
|
||||
deviceIntegrationService.updateDeviceSort(id, sort);
|
||||
return ApiResponse.success("设备排序更新成功");
|
||||
} catch (Exception e) {
|
||||
log.error("更新设备排序失败, id: {}, sort: {}", id, sort, e);
|
||||
return ApiResponse.fail("更新设备排序失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用设备
|
||||
*/
|
||||
@PutMapping("/{id}/enable")
|
||||
public ApiResponse<String> enableDevice(@PathVariable Long id) {
|
||||
log.info("启用设备, id: {}", id);
|
||||
try {
|
||||
deviceIntegrationService.enableDevice(id);
|
||||
return ApiResponse.success("设备启用成功");
|
||||
} catch (Exception e) {
|
||||
log.error("启用设备失败, id: {}", id, e);
|
||||
return ApiResponse.fail("启用设备失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用设备
|
||||
*/
|
||||
@PutMapping("/{id}/disable")
|
||||
public ApiResponse<String> disableDevice(@PathVariable Long id) {
|
||||
log.info("禁用设备, id: {}", id);
|
||||
try {
|
||||
deviceIntegrationService.disableDevice(id);
|
||||
return ApiResponse.success("设备禁用成功");
|
||||
} catch (Exception e) {
|
||||
log.error("禁用设备失败, id: {}", id, e);
|
||||
return ApiResponse.fail("禁用设备失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除设备
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public ApiResponse<String> deleteDevice(@PathVariable Long id) {
|
||||
log.info("删除设备, id: {}", id);
|
||||
try {
|
||||
deviceIntegrationService.deleteDevice(id);
|
||||
return ApiResponse.success("设备删除成功");
|
||||
} catch (Exception e) {
|
||||
log.error("删除设备失败, id: {}", id, e);
|
||||
return ApiResponse.fail("删除设备失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 设备配置管理操作 ==========
|
||||
|
||||
/**
|
||||
* 获取设备配置列表
|
||||
*/
|
||||
@GetMapping("/{id}/config")
|
||||
public ApiResponse<List<DeviceConfigV2DTO>> getDeviceConfigs(@PathVariable Long id) {
|
||||
try {
|
||||
List<DeviceConfigV2DTO> configs = deviceConfigIntegrationService.getDeviceConfigs(id);
|
||||
return ApiResponse.success(configs);
|
||||
} catch (Exception e) {
|
||||
log.error("获取设备配置列表失败, deviceId: {}", id, e);
|
||||
return ApiResponse.fail("获取设备配置列表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据配置键获取配置
|
||||
*/
|
||||
@GetMapping("/{id}/config/{configKey}")
|
||||
public ApiResponse<DeviceConfigV2DTO> getDeviceConfigByKey(@PathVariable Long id,
|
||||
@PathVariable String configKey) {
|
||||
try {
|
||||
DeviceConfigV2DTO config = deviceConfigIntegrationService.getDeviceConfigByKey(id, configKey);
|
||||
return ApiResponse.success(config);
|
||||
} catch (Exception e) {
|
||||
log.error("根据键获取设备配置失败, deviceId: {}, configKey: {}", id, configKey, e);
|
||||
return ApiResponse.fail("根据键获取设备配置失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据设备编号获取配置列表
|
||||
*/
|
||||
@GetMapping("/no/{no}/config")
|
||||
public ApiResponse<List<DeviceConfigV2DTO>> getDeviceConfigsByNo(@PathVariable String no) {
|
||||
log.info("根据设备编号获取配置列表, deviceNo: {}", no);
|
||||
try {
|
||||
List<DeviceConfigV2DTO> configs = deviceConfigIntegrationService.getDeviceConfigsByNo(no);
|
||||
return ApiResponse.success(configs);
|
||||
} catch (Exception e) {
|
||||
log.error("根据设备编号获取配置列表失败, deviceNo: {}", no, e);
|
||||
return ApiResponse.fail("根据设备编号获取配置列表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建设备配置
|
||||
*/
|
||||
@PostMapping("/{id}/config")
|
||||
public ApiResponse<DeviceConfigV2DTO> createDeviceConfig(@PathVariable Long id,
|
||||
@Valid @RequestBody CreateDeviceConfigRequest request) {
|
||||
log.info("创建设备配置, deviceId: {}, configKey: {}", id, request.getConfigKey());
|
||||
try {
|
||||
DeviceConfigV2DTO config = deviceConfigIntegrationService.createDeviceConfig(id, request);
|
||||
return ApiResponse.success(config);
|
||||
} catch (Exception e) {
|
||||
log.error("创建设备配置失败, deviceId: {}, configKey: {}", id, request.getConfigKey(), e);
|
||||
return ApiResponse.fail("创建设备配置失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量创建/更新设备配置
|
||||
*/
|
||||
@PostMapping("/{id}/config/batch")
|
||||
public ApiResponse<BatchUpdateResponse> batchUpdateDeviceConfig(@PathVariable Long id,
|
||||
@Valid @RequestBody BatchDeviceConfigRequest request) {
|
||||
log.info("批量更新设备配置, deviceId: {}, configs count: {}", id, request.getConfigs().size());
|
||||
try {
|
||||
BatchUpdateResponse result = deviceConfigIntegrationService.batchUpdateDeviceConfig(id, request);
|
||||
return ApiResponse.success(result);
|
||||
} catch (Exception e) {
|
||||
log.error("批量更新设备配置失败, deviceId: {}", id, e);
|
||||
return ApiResponse.fail("批量更新设备配置失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 更新设备配置
|
||||
*/
|
||||
@PutMapping("/{id}/config/{configId}")
|
||||
public ApiResponse<String> updateDeviceConfig(@PathVariable Long id, @PathVariable Long configId,
|
||||
@Valid @RequestBody UpdateDeviceConfigRequest request) {
|
||||
log.info("更新设备配置, deviceId: {}, configId: {}", id, configId);
|
||||
try {
|
||||
deviceConfigIntegrationService.updateDeviceConfig(id, configId, request);
|
||||
return ApiResponse.success("设备配置更新成功");
|
||||
} catch (Exception e) {
|
||||
log.error("更新设备配置失败, deviceId: {}, configId: {}", id, configId, e);
|
||||
return ApiResponse.fail("更新设备配置失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 删除设备配置
|
||||
*/
|
||||
@DeleteMapping("/{id}/config/{configId}")
|
||||
public ApiResponse<String> deleteDeviceConfig(@PathVariable Long id, @PathVariable Long configId) {
|
||||
log.info("删除设备配置, deviceId: {}, configId: {}", id, configId);
|
||||
try {
|
||||
deviceConfigIntegrationService.deleteDeviceConfig(id, configId);
|
||||
return ApiResponse.success("设备配置删除成功");
|
||||
} catch (Exception e) {
|
||||
log.error("删除设备配置失败, deviceId: {}, configId: {}", id, configId, e);
|
||||
return ApiResponse.fail("删除设备配置失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 景区设备管理操作 ==========
|
||||
|
||||
/**
|
||||
* 获取景区所有设备列表
|
||||
*/
|
||||
@GetMapping("/scenic/{scenicId}")
|
||||
public ApiResponse<PageResponse<DeviceV2DTO>> getScenicAllDevices(@PathVariable Long scenicId,
|
||||
@RequestParam(required = false) String name,
|
||||
@RequestParam(required = false) String type,
|
||||
@RequestParam(required = false) String no,
|
||||
@RequestParam(defaultValue = "1") Integer page,
|
||||
@RequestParam(defaultValue = "10") Integer pageSize) {
|
||||
log.info("获取景区所有设备列表, scenicId: {}, page: {}, pageSize: {}", scenicId, page, pageSize);
|
||||
try {
|
||||
PageResponse<DeviceV2DTO> response = deviceIntegrationService.listDevices(page, pageSize, name, no, type, null, scenicId, null);
|
||||
return ApiResponse.success(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取景区所有设备列表失败, scenicId: {}", scenicId, e);
|
||||
return ApiResponse.fail("获取景区所有设备列表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.ycwl.basic.controller.pc;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ycwl.basic.device.entity.common.DeviceVideoContinuityCache;
|
||||
import com.ycwl.basic.task.DeviceVideoContinuityCheckTask;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 设备视频连续性检查控制器
|
||||
* 提供查询设备视频连续性检查结果的接口
|
||||
*
|
||||
* @author Claude Code
|
||||
* @date 2025-09-01
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/device/video-continuity")
|
||||
@RequiredArgsConstructor
|
||||
public class DeviceVideoContinuityController {
|
||||
|
||||
private static final String REDIS_KEY_PREFIX = "device:video:continuity:";
|
||||
|
||||
private final RedisTemplate<String, String> redisTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final DeviceVideoContinuityCheckTask checkTask;
|
||||
|
||||
/**
|
||||
* 查询设备最近的视频连续性检查结果
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @return 检查结果
|
||||
*/
|
||||
@GetMapping("/{deviceId}")
|
||||
public ApiResponse<DeviceVideoContinuityCache> getDeviceContinuityResult(@PathVariable Long deviceId) {
|
||||
log.info("查询设备 {} 的视频连续性检查结果", deviceId);
|
||||
|
||||
try {
|
||||
String redisKey = REDIS_KEY_PREFIX + deviceId;
|
||||
String cacheJson = redisTemplate.opsForValue().get(redisKey);
|
||||
|
||||
if (cacheJson == null) {
|
||||
log.warn("未找到设备 {} 的视频连续性检查结果", deviceId);
|
||||
return ApiResponse.buildResponse(404, null, "未找到该设备的检查结果,可能设备未配置存储或尚未执行检查");
|
||||
}
|
||||
|
||||
DeviceVideoContinuityCache cache = objectMapper.readValue(cacheJson, DeviceVideoContinuityCache.class);
|
||||
return ApiResponse.success(cache);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("查询设备 {} 视频连续性检查结果失败", deviceId, e);
|
||||
return ApiResponse.buildResponse(500, null, "查询失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动触发设备视频连续性检查
|
||||
* 注意:仅用于测试和紧急情况,正常情况下由定时任务自动执行
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @return 检查结果
|
||||
*/
|
||||
@PostMapping("/{deviceId}/check")
|
||||
public ApiResponse<DeviceVideoContinuityCache> manualCheck(@PathVariable Long deviceId) {
|
||||
log.info("手动触发设备 {} 的视频连续性检查", deviceId);
|
||||
|
||||
try {
|
||||
DeviceVideoContinuityCache result = checkTask.manualCheck(deviceId);
|
||||
return ApiResponse.success(result);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("手动检查设备 {} 视频连续性失败", deviceId, e);
|
||||
return ApiResponse.buildResponse(500, null, "检查失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除设备的视频连续性检查缓存
|
||||
* 用于清理过期或错误的缓存数据
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @return 删除结果
|
||||
*/
|
||||
@DeleteMapping("/{deviceId}")
|
||||
public ApiResponse<String> deleteContinuityCache(@PathVariable Long deviceId) {
|
||||
log.info("删除设备 {} 的视频连续性检查缓存", deviceId);
|
||||
|
||||
try {
|
||||
String redisKey = REDIS_KEY_PREFIX + deviceId;
|
||||
Boolean deleted = redisTemplate.delete(redisKey);
|
||||
|
||||
if (deleted != null && deleted) {
|
||||
return ApiResponse.success("缓存删除成功");
|
||||
} else {
|
||||
return ApiResponse.buildResponse(404, null, "未找到该设备的缓存数据");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("删除设备 {} 视频连续性检查缓存失败", deviceId, e);
|
||||
return ApiResponse.buildResponse(500, null, "删除失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,5 +53,4 @@ public class FaceController {
|
||||
return faceService.deleteByIds(ids);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
package com.ycwl.basic.controller.pc;
|
||||
import com.ycwl.basic.integration.kafka.service.FaceProcessingKafkaService;
|
||||
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
||||
@@ -15,13 +16,14 @@ import java.util.List;
|
||||
* @Author:longbinbin
|
||||
* @Date:2024/12/2 16:33
|
||||
*/
|
||||
@Deprecated
|
||||
@RestController
|
||||
@RequestMapping("/api/faceSample/v1")
|
||||
// 人脸样本管理
|
||||
public class FaceSampleController {
|
||||
@Autowired
|
||||
private FaceSampleService FaceSampleService;
|
||||
@Autowired(required = false)
|
||||
private FaceProcessingKafkaService faceProcessingKafkaService;
|
||||
|
||||
// 分页查询人脸样本
|
||||
@PostMapping("/page")
|
||||
@@ -39,4 +41,25 @@ public class FaceSampleController {
|
||||
return FaceSampleService.getById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重试失败的人脸识别
|
||||
* 用于手动重试状态为-1的人脸样本
|
||||
*
|
||||
* @param id 人脸样本ID
|
||||
* @return 重试结果
|
||||
*/
|
||||
@PostMapping("/retry/{id}")
|
||||
public ApiResponse<String> retryFaceRecognition(@PathVariable("id") Long id) {
|
||||
if (faceProcessingKafkaService == null) {
|
||||
return ApiResponse.fail("Kafka服务未启用,无法重试人脸识别");
|
||||
}
|
||||
|
||||
boolean success = faceProcessingKafkaService.retryFaceRecognition(id);
|
||||
if (success) {
|
||||
return ApiResponse.success("人脸识别重试任务已提交");
|
||||
} else {
|
||||
return ApiResponse.fail("提交重试任务失败,请检查人脸样本状态");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,8 @@
|
||||
package com.ycwl.basic.controller.pc;
|
||||
|
||||
import com.github.pagehelper.PageHelper;
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.ycwl.basic.model.pc.price.entity.PriceConfigEntity;
|
||||
import com.ycwl.basic.model.pc.price.req.PriceConfigListReq;
|
||||
import com.ycwl.basic.biz.PriceBiz;
|
||||
import com.ycwl.basic.model.pc.price.resp.GoodsListRespVO;
|
||||
import com.ycwl.basic.model.pc.price.resp.PriceConfigRespVO;
|
||||
import com.ycwl.basic.repository.PriceRepository;
|
||||
import com.ycwl.basic.service.pc.PriceConfigService;
|
||||
import com.ycwl.basic.model.pc.price.resp.SimpleGoodsRespVO;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@@ -19,61 +14,13 @@ import java.util.List;
|
||||
public class PriceConfigController {
|
||||
|
||||
@Autowired
|
||||
private PriceConfigService priceConfigService;
|
||||
@Autowired
|
||||
private PriceRepository priceRepository;
|
||||
private PriceBiz priceBiz;
|
||||
|
||||
@GetMapping("/goodsList")
|
||||
public ApiResponse<List<GoodsListRespVO>> goodsList(@RequestParam Long scenicId) {
|
||||
return ApiResponse.success(priceConfigService.listGoodsByScenic(scenicId));
|
||||
public ApiResponse<List<SimpleGoodsRespVO>> goodsList(
|
||||
@RequestParam Long scenicId,
|
||||
@RequestParam(required = false) String productType) {
|
||||
return ApiResponse.success(priceBiz.listSimpleGoodsByScenic(scenicId, productType));
|
||||
}
|
||||
|
||||
@PostMapping("/add")
|
||||
public ApiResponse<PriceConfigEntity> addPriceConfig(@RequestBody PriceConfigEntity priceConfig) {
|
||||
priceConfig.setId(null);
|
||||
priceConfigService.save(priceConfig);
|
||||
return ApiResponse.success(priceConfig);
|
||||
}
|
||||
|
||||
@PostMapping("/update")
|
||||
public ApiResponse<PriceConfigEntity> updatePriceConfig(@RequestBody PriceConfigEntity priceConfig) {
|
||||
priceRepository.clearPriceCache(priceConfig.getId());
|
||||
priceConfigService.updateById(priceConfig);
|
||||
priceRepository.clearPriceCache(priceConfig.getId());
|
||||
return ApiResponse.success(priceConfig);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete/{id}")
|
||||
public ApiResponse<Boolean> deletePriceConfig(@PathVariable Integer id) {
|
||||
priceRepository.clearPriceCache(id);
|
||||
priceConfigService.removeById(id);
|
||||
priceRepository.clearPriceCache(id);
|
||||
return ApiResponse.success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/status")
|
||||
public ApiResponse<Boolean> updateStatus(@PathVariable Integer id) {
|
||||
priceRepository.clearPriceCache(id);
|
||||
priceConfigService.updateStatus(id);
|
||||
priceRepository.clearPriceCache(id);
|
||||
return ApiResponse.success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ApiResponse<PriceConfigRespVO> getPriceConfigById(@PathVariable Integer id) {
|
||||
PriceConfigRespVO config = priceConfigService.findById(id);
|
||||
priceConfigService.fillGoodsName(config);
|
||||
return ApiResponse.success(config);
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
public ApiResponse<PageInfo<PriceConfigRespVO>> list(@RequestParam(defaultValue = "1") int pageNum,
|
||||
@RequestParam(defaultValue = "10") int pageSize,
|
||||
@ModelAttribute PriceConfigListReq req) {
|
||||
PageHelper.startPage(pageNum, pageSize);
|
||||
List<PriceConfigRespVO> result = priceConfigService.listByCondition(req);
|
||||
priceConfigService.fillGoodsName(result);
|
||||
PageInfo<PriceConfigRespVO> pageInfo = new PageInfo<>(result);
|
||||
return ApiResponse.success(pageInfo);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,12 @@
|
||||
package com.ycwl.basic.controller.pc;
|
||||
|
||||
import com.github.pagehelper.PageHelper;
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.ycwl.basic.mapper.PrintTaskMapper;
|
||||
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.req.PrintTaskReqQuery;
|
||||
import com.ycwl.basic.model.pc.printer.req.ReprintRequest;
|
||||
import com.ycwl.basic.service.printer.PrinterService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -20,6 +26,9 @@ public class PrinterController {
|
||||
@Autowired
|
||||
private PrinterService printerService;
|
||||
|
||||
@Autowired
|
||||
private PrintTaskMapper printTaskMapper;
|
||||
|
||||
// 查询列表
|
||||
@PostMapping("/list")
|
||||
public ApiResponse<List<PrinterEntity>> list(@RequestBody PrinterEntity condition) {
|
||||
@@ -49,4 +58,65 @@ public class PrinterController {
|
||||
public ApiResponse<Integer> delete(@PathVariable("id") Integer id) {
|
||||
return printerService.delete(id);
|
||||
}
|
||||
|
||||
// 分页查询打印任务
|
||||
@PostMapping("/task/page")
|
||||
public ApiResponse<PageInfo<PrintTaskEntity>> taskPage(@RequestBody PrintTaskReqQuery req) {
|
||||
PageHelper.startPage(req.getPageNum(), req.getPageSize());
|
||||
List<PrintTaskEntity> list = printTaskMapper.queryByCondition(req.getPrinterId(), req.getStatus());
|
||||
PageInfo<PrintTaskEntity> pageInfo = new PageInfo<>(list);
|
||||
return ApiResponse.success(pageInfo);
|
||||
}
|
||||
|
||||
// 重新打印(将状态设置为0-未开始,并更新打印机名称)
|
||||
@PostMapping("/task/reprint/{id}")
|
||||
public ApiResponse<Integer> reprint(@PathVariable("id") Integer id, @RequestBody ReprintRequest request) {
|
||||
int result = printerService.handleReprint(id, request);
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询待审核的打印任务
|
||||
* @param printerId 打印机ID(可选)
|
||||
* @return 待审核任务列表
|
||||
*/
|
||||
@GetMapping("/task/pending-review")
|
||||
public ApiResponse<List<PrintTaskEntity>> getPendingReviewTasks(Integer printerId) {
|
||||
List<PrintTaskEntity> tasks = printerService.getPendingReviewTasks(printerId);
|
||||
return ApiResponse.success(tasks);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新待审核任务的URL(重新处理水印等)
|
||||
* @param taskId 任务ID
|
||||
* @param url 新的打印URL
|
||||
* @return 操作结果
|
||||
*/
|
||||
@PostMapping("/task/{taskId}/url")
|
||||
public ApiResponse<Boolean> updateTaskUrl(@PathVariable("taskId") Integer taskId, @RequestBody String url) {
|
||||
boolean success = printerService.updatePendingReviewTaskUrl(taskId, url);
|
||||
return ApiResponse.success(success);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批准待审核任务,下发到打印队列
|
||||
* @param taskIds 任务ID列表
|
||||
* @return 成功数量
|
||||
*/
|
||||
@PostMapping("/task/approve")
|
||||
public ApiResponse<Integer> approveTasks(@RequestBody List<Integer> taskIds) {
|
||||
int count = printerService.approvePrintTasks(taskIds);
|
||||
return ApiResponse.success(count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拒绝待审核任务
|
||||
* @param taskIds 任务ID列表
|
||||
* @return 成功数量
|
||||
*/
|
||||
@PostMapping("/task/reject")
|
||||
public ApiResponse<Integer> rejectTasks(@RequestBody List<Integer> taskIds) {
|
||||
int count = printerService.rejectPrintTasks(taskIds);
|
||||
return ApiResponse.success(count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package com.ycwl.basic.controller.pc;
|
||||
|
||||
import com.ycwl.basic.model.pc.printer.entity.PrinterEntity;
|
||||
import com.ycwl.basic.model.pc.printer.req.PrinterPreferredSizeUpdateReq;
|
||||
import com.ycwl.basic.model.pc.printer.req.PrinterStatusUpdateReq;
|
||||
import com.ycwl.basic.model.pc.printer.req.PrinterUsePrinterUpdateReq;
|
||||
import com.ycwl.basic.service.printer.PrinterService;
|
||||
import com.ycwl.basic.utils.ApiConst;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 打印机管理接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/pc/printers/v1")
|
||||
@RequiredArgsConstructor
|
||||
public class PrinterManageController {
|
||||
|
||||
private final PrinterService printerService;
|
||||
|
||||
/**
|
||||
* 打印机列表查询
|
||||
*/
|
||||
@GetMapping
|
||||
public ApiResponse<List<PrinterEntity>> list(@RequestParam(value = "scenicId", required = false) Long scenicId,
|
||||
@RequestParam(value = "status", required = false) Integer status,
|
||||
@RequestParam(value = "name", required = false) String name) {
|
||||
PrinterEntity condition = new PrinterEntity();
|
||||
condition.setScenicId(scenicId);
|
||||
condition.setStatus(status);
|
||||
condition.setName(name);
|
||||
return printerService.list(condition);
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印机详情
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public ApiResponse<PrinterEntity> detail(@PathVariable("id") Integer id) {
|
||||
ApiResponse<PrinterEntity> response = printerService.get(id);
|
||||
if (response.getData() == null) {
|
||||
return ApiResponse.buildResponse(ApiConst.Code.CODE_NOT_EXIST, "打印机不存在");
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增打印机
|
||||
*/
|
||||
@PostMapping
|
||||
public ApiResponse<Integer> create(@RequestBody PrinterEntity request) {
|
||||
request.setId(null);
|
||||
return printerService.add(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新打印机信息
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
public ApiResponse<Integer> update(@PathVariable("id") Integer id, @RequestBody PrinterEntity request) {
|
||||
request.setId(id);
|
||||
return printerService.update(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新打印机状态
|
||||
*/
|
||||
@PatchMapping("/{id}/status")
|
||||
public ApiResponse<Integer> updateStatus(@PathVariable("id") Integer id,
|
||||
@RequestBody PrinterStatusUpdateReq req) {
|
||||
if (req == null || req.getStatus() == null) {
|
||||
return ApiResponse.buildResponse(ApiConst.Code.CODE_PARAM_ERROR, "状态不能为空");
|
||||
}
|
||||
PrinterEntity entity = new PrinterEntity();
|
||||
entity.setId(id);
|
||||
entity.setStatus(req.getStatus());
|
||||
return printerService.update(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新打印机首选尺寸
|
||||
*/
|
||||
@PatchMapping("/{id}/preferred-size")
|
||||
public ApiResponse<Integer> updatePreferredSize(@PathVariable("id") Integer id,
|
||||
@RequestBody PrinterPreferredSizeUpdateReq req) {
|
||||
if (req == null || (req.getPreferW() == null && req.getPreferH() == null)) {
|
||||
return ApiResponse.buildResponse(ApiConst.Code.CODE_PARAM_ERROR, "首选尺寸不能为空");
|
||||
}
|
||||
PrinterEntity entity = new PrinterEntity();
|
||||
entity.setId(id);
|
||||
entity.setPreferW(req.getPreferW());
|
||||
entity.setPreferH(req.getPreferH());
|
||||
return printerService.update(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新当前使用的打印机
|
||||
*/
|
||||
@PatchMapping("/{id}/use-printer")
|
||||
public ApiResponse<Integer> updateUsePrinter(@PathVariable("id") Integer id,
|
||||
@RequestBody PrinterUsePrinterUpdateReq req) {
|
||||
if (req == null) {
|
||||
return ApiResponse.buildResponse(ApiConst.Code.CODE_PARAM_ERROR, "请求参数不能为空");
|
||||
}
|
||||
PrinterEntity entity = new PrinterEntity();
|
||||
entity.setId(id);
|
||||
entity.setUsePrinter(req.getUsePrinter());
|
||||
return printerService.update(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除打印机
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public ApiResponse<Integer> delete(@PathVariable("id") Integer id) {
|
||||
return printerService.delete(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.ycwl.basic.controller.pc;
|
||||
|
||||
import com.ycwl.basic.model.pc.project.entity.ProjectEntity;
|
||||
import com.ycwl.basic.model.pc.project.req.ProjectReqQuery;
|
||||
import com.ycwl.basic.model.pc.project.resp.ProjectRespVO;
|
||||
import com.ycwl.basic.service.pc.ProjectService;
|
||||
import com.ycwl.basic.storage.enums.StorageAcl;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import com.ycwl.basic.utils.WxMpUtil;
|
||||
import com.ycwl.basic.repository.ScenicRepository;
|
||||
import com.ycwl.basic.storage.StorageFactory;
|
||||
import com.ycwl.basic.storage.adapters.IStorageAdapter;
|
||||
import com.ycwl.basic.model.pc.mp.MpConfigEntity;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* 景区项目管理控制器
|
||||
*
|
||||
* @Author: Claude
|
||||
* @Date: 2025-01-15
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/project/v1")
|
||||
public class ProjectController {
|
||||
|
||||
@Autowired
|
||||
private ProjectService projectService;
|
||||
|
||||
@Autowired
|
||||
private ScenicRepository scenicRepository;
|
||||
|
||||
// 分页查询
|
||||
@PostMapping("/page")
|
||||
public ApiResponse page(@RequestBody ProjectReqQuery projectReqQuery) {
|
||||
return ApiResponse.success(projectService.pageQuery(projectReqQuery));
|
||||
}
|
||||
|
||||
// 列表查询
|
||||
@PostMapping("/list")
|
||||
public ApiResponse list(@RequestBody ProjectReqQuery projectReqQuery) {
|
||||
return ApiResponse.success(projectService.list(projectReqQuery));
|
||||
}
|
||||
|
||||
// 详情查询
|
||||
@GetMapping("/getDetails/{id}")
|
||||
public ApiResponse getDetails(@PathVariable("id") Long id) {
|
||||
return ApiResponse.success(projectService.getById(id));
|
||||
}
|
||||
|
||||
// 新增或修改
|
||||
@PostMapping("/addOrUpdate")
|
||||
public ApiResponse addOrUpdate(@RequestBody ProjectEntity project) {
|
||||
return ApiResponse.success(projectService.addOrUpdate(project));
|
||||
}
|
||||
|
||||
// 删除
|
||||
@DeleteMapping("/delete/{id}")
|
||||
public ApiResponse delete(@PathVariable("id") Long id) {
|
||||
return ApiResponse.success(projectService.delete(id));
|
||||
}
|
||||
|
||||
// 修改状态
|
||||
@PutMapping("/updateStatus/{id}")
|
||||
public ApiResponse updateStatus(@PathVariable("id") Long id) {
|
||||
return ApiResponse.success(projectService.updateStatus(id));
|
||||
}
|
||||
|
||||
// 根据项目ID下载小程序二维码
|
||||
@GetMapping("/{id}/QRCode")
|
||||
public ApiResponse<String> downloadQrCode(@PathVariable Long id) {
|
||||
ProjectRespVO project = projectService.getById(id);
|
||||
if (project == null) {
|
||||
return ApiResponse.fail("项目不存在");
|
||||
}
|
||||
MpConfigEntity mpConfig = scenicRepository.getScenicMpConfig(project.getScenicId());
|
||||
if (mpConfig == null) {
|
||||
return ApiResponse.fail("小程序配置不存在");
|
||||
}
|
||||
String appId = mpConfig.getAppId();
|
||||
String appSecret = mpConfig.getAppSecret();
|
||||
String appState = mpConfig.getState();
|
||||
String path = "pages/home/index?scenicId=" + project.getScenicId() + "&projectId=" + id;
|
||||
String filePath = "qr_code_project_" + id + ".jpg";
|
||||
IStorageAdapter adapter = StorageFactory.use();
|
||||
if (adapter.isExists(filePath)) {
|
||||
return ApiResponse.success(adapter.getUrl(filePath));
|
||||
}
|
||||
try {
|
||||
WxMpUtil.generateWXAQRCode(appId, appSecret, appState, path, filePath);
|
||||
File file = new File(filePath);
|
||||
String s = adapter.uploadFile(null, file, filePath);
|
||||
file.delete();
|
||||
adapter.setAcl(StorageAcl.PUBLIC_READ, filePath);
|
||||
return ApiResponse.success(s);
|
||||
} catch (Exception e) {
|
||||
return ApiResponse.fail("生成二维码失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
package com.ycwl.basic.controller.pc;
|
||||
|
||||
import com.ycwl.basic.integration.questionnaire.dto.answer.ResponseDetailResponse;
|
||||
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 com.ycwl.basic.integration.common.response.PageResponse;
|
||||
import com.ycwl.basic.model.pc.mp.MpConfigEntity;
|
||||
import com.ycwl.basic.repository.ScenicRepository;
|
||||
import com.ycwl.basic.storage.StorageFactory;
|
||||
import com.ycwl.basic.storage.adapters.IStorageAdapter;
|
||||
import com.ycwl.basic.storage.enums.StorageAcl;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import com.ycwl.basic.utils.WxMpUtil;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 问卷管理 V2 版本控制器 - 基于 zt-questionnaire 集成服务
|
||||
*
|
||||
* @author Claude Code
|
||||
* @date 2025-09-05
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/questionnaire/v2")
|
||||
@RequiredArgsConstructor
|
||||
public class QuestionnaireV2Controller {
|
||||
|
||||
private final QuestionnaireIntegrationService questionnaireIntegrationService;
|
||||
private final ScenicRepository scenicRepository;
|
||||
|
||||
// ========== 问卷管理 CRUD 操作 ==========
|
||||
|
||||
/**
|
||||
* 分页查询问卷列表
|
||||
*/
|
||||
@GetMapping("/")
|
||||
public ApiResponse<PageResponse<QuestionnaireResponse>> listQuestionnaires(
|
||||
@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<QuestionnaireResponse> response =
|
||||
questionnaireIntegrationService.getQuestionnaireList(page, pageSize, name, status, null);
|
||||
return ApiResponse.success(response);
|
||||
} catch (Exception e) {
|
||||
log.error("分页查询问卷列表失败", e);
|
||||
return ApiResponse.fail("分页查询问卷列表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取问卷详情
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public ApiResponse<QuestionnaireResponse> getQuestionnaire(@PathVariable Long id) {
|
||||
log.info("获取问卷详情, id: {}", id);
|
||||
try {
|
||||
QuestionnaireResponse questionnaire = questionnaireIntegrationService.getQuestionnaire(id);
|
||||
return ApiResponse.success(questionnaire);
|
||||
} catch (Exception e) {
|
||||
log.error("获取问卷详情失败, id: {}", id, e);
|
||||
return ApiResponse.fail("获取问卷详情失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建问卷
|
||||
*/
|
||||
@PostMapping("/")
|
||||
public ApiResponse<QuestionnaireResponse> createQuestionnaire(@Valid @RequestBody CreateQuestionnaireRequest request) {
|
||||
log.info("创建问卷, name: {}, questions count: {}",
|
||||
request.getName(), request.getQuestions() != null ? request.getQuestions().size() : 0);
|
||||
try {
|
||||
QuestionnaireResponse questionnaire = questionnaireIntegrationService.createQuestionnaire(request, "admin");
|
||||
return ApiResponse.success(questionnaire);
|
||||
} catch (Exception e) {
|
||||
log.error("创建问卷失败", e);
|
||||
return ApiResponse.fail("创建问卷失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新问卷
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
public ApiResponse<QuestionnaireResponse> updateQuestionnaire(
|
||||
@PathVariable Long id,
|
||||
@Valid @RequestBody CreateQuestionnaireRequest request) {
|
||||
log.info("更新问卷, id: {}", id);
|
||||
try {
|
||||
QuestionnaireResponse questionnaire = questionnaireIntegrationService.updateQuestionnaire(id, request, "admin");
|
||||
return ApiResponse.success(questionnaire);
|
||||
} catch (Exception e) {
|
||||
log.error("更新问卷失败, id: {}", id, e);
|
||||
return ApiResponse.fail("更新问卷失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新问卷状态
|
||||
*/
|
||||
@PutMapping("/{id}/status")
|
||||
public ApiResponse<String> updateQuestionnaireStatus(@PathVariable Long id, @RequestBody Map<String, Integer> request) {
|
||||
Integer status = request.get("status");
|
||||
log.info("更新问卷状态, id: {}, status: {}", id, status);
|
||||
try {
|
||||
// 根据状态调用不同的方法
|
||||
if (status == 2) {
|
||||
questionnaireIntegrationService.publishQuestionnaire(id, "admin");
|
||||
} else if (status == 3) {
|
||||
questionnaireIntegrationService.stopQuestionnaire(id, "admin");
|
||||
}
|
||||
return ApiResponse.success("问卷状态更新成功");
|
||||
} catch (Exception e) {
|
||||
log.error("更新问卷状态失败, id: {}, status: {}", id, status, e);
|
||||
return ApiResponse.fail("更新问卷状态失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布问卷
|
||||
*/
|
||||
@PutMapping("/{id}/publish")
|
||||
public ApiResponse<String> publishQuestionnaire(@PathVariable Long id) {
|
||||
log.info("发布问卷, id: {}", id);
|
||||
try {
|
||||
questionnaireIntegrationService.publishQuestionnaire(id, "admin");
|
||||
return ApiResponse.success("问卷发布成功");
|
||||
} catch (Exception e) {
|
||||
log.error("发布问卷失败, id: {}", id, e);
|
||||
return ApiResponse.fail("发布问卷失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止问卷
|
||||
*/
|
||||
@PutMapping("/{id}/stop")
|
||||
public ApiResponse<String> stopQuestionnaire(@PathVariable Long id) {
|
||||
log.info("停止问卷, id: {}", id);
|
||||
try {
|
||||
questionnaireIntegrationService.stopQuestionnaire(id, "admin");
|
||||
return ApiResponse.success("问卷停止成功");
|
||||
} catch (Exception e) {
|
||||
log.error("停止问卷失败, id: {}", id, e);
|
||||
return ApiResponse.fail("停止问卷失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除问卷
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public ApiResponse<String> deleteQuestionnaire(@PathVariable Long id) {
|
||||
log.info("删除问卷, id: {}", id);
|
||||
try {
|
||||
questionnaireIntegrationService.deleteQuestionnaire(id, "admin");
|
||||
return ApiResponse.success("问卷删除成功");
|
||||
} catch (Exception e) {
|
||||
log.error("删除问卷失败, id: {}", id, e);
|
||||
return ApiResponse.fail("删除问卷失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 问卷答案查看操作 ==========
|
||||
|
||||
/**
|
||||
* 分页查询问卷答案
|
||||
*/
|
||||
@GetMapping("/{id}/answers")
|
||||
public ApiResponse<PageResponse<ResponseDetailResponse>> getQuestionnaireAnswers(
|
||||
@PathVariable Long id,
|
||||
@RequestParam(defaultValue = "1") Integer page,
|
||||
@RequestParam(defaultValue = "10") Integer pageSize,
|
||||
@RequestParam(required = false) String userId,
|
||||
@RequestParam(required = false) String startTime,
|
||||
@RequestParam(required = false) String endTime) {
|
||||
log.info("分页查询问卷答案, questionnaireId: {}, page: {}, pageSize: {}, userId: {}",
|
||||
id, page, pageSize, userId);
|
||||
|
||||
// 参数验证:限制pageSize最大值为100
|
||||
if (pageSize > 100) {
|
||||
pageSize = 100;
|
||||
}
|
||||
|
||||
try {
|
||||
PageResponse<ResponseDetailResponse> response =
|
||||
questionnaireIntegrationService.getResponseList(page, pageSize, id, userId, startTime, endTime);
|
||||
return ApiResponse.success(response);
|
||||
} catch (Exception e) {
|
||||
log.error("分页查询问卷答案失败, questionnaireId: {}", id, e);
|
||||
return ApiResponse.fail("分页查询问卷答案失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取特定答案详情
|
||||
*/
|
||||
@GetMapping("/{id}/answers/{answerId}")
|
||||
public ApiResponse<ResponseDetailResponse> getQuestionnaireAnswer(@PathVariable Long id, @PathVariable Long answerId) {
|
||||
log.info("获取问卷答案详情, questionnaireId: {}, answerId: {}", id, answerId);
|
||||
try {
|
||||
ResponseDetailResponse answer = questionnaireIntegrationService.getResponseDetail(answerId);
|
||||
return ApiResponse.success(answer);
|
||||
} catch (Exception e) {
|
||||
log.error("获取问卷答案详情失败, questionnaireId: {}, answerId: {}", id, answerId, e);
|
||||
return ApiResponse.fail("获取问卷答案详情失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户答题记录
|
||||
*/
|
||||
@GetMapping("/answers/user/{userId}")
|
||||
public ApiResponse<PageResponse<ResponseDetailResponse>> getUserAnswers(
|
||||
@PathVariable String userId,
|
||||
@RequestParam(defaultValue = "1") Integer page,
|
||||
@RequestParam(defaultValue = "10") Integer pageSize,
|
||||
@RequestParam(required = false) Long questionnaireId) {
|
||||
log.info("查询用户答题记录, userId: {}, page: {}, pageSize: {}, questionnaireId: {}",
|
||||
userId, page, pageSize, questionnaireId);
|
||||
|
||||
// 参数验证:限制pageSize最大值为100
|
||||
if (pageSize > 100) {
|
||||
pageSize = 100;
|
||||
}
|
||||
|
||||
try {
|
||||
PageResponse<ResponseDetailResponse> response =
|
||||
questionnaireIntegrationService.getResponseList(page, pageSize, questionnaireId, userId, null, null);
|
||||
return ApiResponse.success(response);
|
||||
} catch (Exception e) {
|
||||
log.error("查询用户答题记录失败, userId: {}", userId, e);
|
||||
return ApiResponse.fail("查询用户答题记录失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 统计功能 ==========
|
||||
|
||||
/**
|
||||
* 获取问卷统计信息
|
||||
*/
|
||||
@GetMapping("/{id}/statistics")
|
||||
public ApiResponse<QuestionnaireStatistics> getQuestionnaireStatistics(@PathVariable Long id) {
|
||||
log.info("获取问卷统计信息, id: {}", id);
|
||||
try {
|
||||
QuestionnaireStatistics statistics = questionnaireIntegrationService.getStatistics(id);
|
||||
return ApiResponse.success(statistics);
|
||||
} catch (Exception e) {
|
||||
log.error("获取问卷统计信息失败, id: {}", id, e);
|
||||
return ApiResponse.fail("获取问卷统计信息失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载问卷小程序二维码
|
||||
*/
|
||||
@GetMapping("/{id}/QRCode")
|
||||
public ApiResponse<String> downloadQrCode(@PathVariable Long id) {
|
||||
log.info("下载问卷小程序二维码, id: {}", id);
|
||||
try {
|
||||
// 获取问卷详情
|
||||
QuestionnaireResponse questionnaire = questionnaireIntegrationService.getQuestionnaire(id);
|
||||
if (questionnaire == null) {
|
||||
return ApiResponse.fail("问卷不存在");
|
||||
}
|
||||
|
||||
MpConfigEntity mpConfig = scenicRepository.getScenicMpConfig(3930324797233434624L);
|
||||
if (mpConfig == null) {
|
||||
return ApiResponse.fail("小程序配置不存在");
|
||||
}
|
||||
|
||||
String appId = mpConfig.getAppId();
|
||||
String appSecret = mpConfig.getAppSecret();
|
||||
String appState = mpConfig.getState();
|
||||
String path = "pages/questionnaire/index?id=" + id;
|
||||
String filePath = "qr_code_questionnaire_" + id + ".jpg";
|
||||
|
||||
IStorageAdapter adapter = StorageFactory.use();
|
||||
if (adapter.isExists(filePath)) {
|
||||
return ApiResponse.success(adapter.getUrl(filePath));
|
||||
}
|
||||
|
||||
WxMpUtil.generateWXAQRCode(appId, appSecret, appState, path, filePath);
|
||||
File file = new File(filePath);
|
||||
String s = adapter.uploadFile(null, file, filePath);
|
||||
file.delete();
|
||||
adapter.setAcl(StorageAcl.PUBLIC_READ, filePath);
|
||||
return ApiResponse.success(s);
|
||||
} catch (Exception e) {
|
||||
log.error("生成问卷二维码失败, id: {}", id, e);
|
||||
return ApiResponse.fail("生成二维码失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package com.ycwl.basic.controller.pc;
|
||||
|
||||
import com.ycwl.basic.integration.render.dto.config.BatchRenderWorkerConfigRequest;
|
||||
import com.ycwl.basic.integration.render.dto.config.RenderWorkerConfigV2DTO;
|
||||
import com.ycwl.basic.integration.render.service.RenderWorkerConfigIntegrationService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 渲染工作器配置管理 V2 版本控制器
|
||||
* 基于 zt-render-worker 微服务标准接口实现
|
||||
*
|
||||
* @author Claude Code
|
||||
* @date 2025-09-06
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/render/worker/config/v2")
|
||||
@RequiredArgsConstructor
|
||||
public class RenderWorkerConfigV2Controller {
|
||||
|
||||
private final RenderWorkerConfigIntegrationService configIntegrationService;
|
||||
|
||||
/**
|
||||
* 获取工作器所有配置
|
||||
*
|
||||
* @param workerId 工作器ID
|
||||
* @return 工作器配置列表
|
||||
*/
|
||||
@GetMapping("/{workerId}")
|
||||
public ApiResponse<List<RenderWorkerConfigV2DTO>> getWorkerConfigs(@PathVariable Long workerId) {
|
||||
log.info("获取渲染工作器配置列表, workerId: {}", workerId);
|
||||
|
||||
try {
|
||||
List<RenderWorkerConfigV2DTO> configs = configIntegrationService.getWorkerConfigs(workerId);
|
||||
return ApiResponse.success(configs);
|
||||
} catch (Exception e) {
|
||||
log.error("获取渲染工作器配置列表失败, workerId: {}", workerId, e);
|
||||
return ApiResponse.fail("获取渲染工作器配置列表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工作器平铺配置
|
||||
*
|
||||
* @param workerId 工作器ID
|
||||
* @return 平铺配置Map
|
||||
*/
|
||||
@GetMapping("/{workerId}/flat")
|
||||
public ApiResponse<Map<String, Object>> getWorkerFlatConfig(@PathVariable Long workerId) {
|
||||
log.info("获取渲染工作器平铺配置, workerId: {}", workerId);
|
||||
|
||||
try {
|
||||
Map<String, Object> flatConfig = configIntegrationService.getWorkerFlatConfig(workerId);
|
||||
return ApiResponse.success(flatConfig);
|
||||
} catch (Exception e) {
|
||||
log.error("获取渲染工作器平铺配置失败, workerId: {}", workerId, e);
|
||||
return ApiResponse.fail("获取渲染工作器平铺配置失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据配置键获取特定配置
|
||||
*
|
||||
* @param workerId 工作器ID
|
||||
* @param configKey 配置键
|
||||
* @return 配置信息
|
||||
*/
|
||||
@GetMapping("/{workerId}/key/{configKey}")
|
||||
public ApiResponse<RenderWorkerConfigV2DTO> getWorkerConfigByKey(@PathVariable Long workerId,
|
||||
@PathVariable String configKey) {
|
||||
log.info("根据配置键获取渲染工作器配置, workerId: {}, configKey: {}", workerId, configKey);
|
||||
|
||||
try {
|
||||
RenderWorkerConfigV2DTO config = configIntegrationService.getWorkerConfigByKey(workerId, configKey);
|
||||
return ApiResponse.success(config);
|
||||
} catch (Exception e) {
|
||||
log.error("根据配置键获取渲染工作器配置失败, workerId: {}, configKey: {}", workerId, configKey, e);
|
||||
return ApiResponse.fail("根据配置键获取渲染工作器配置失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建工作器配置
|
||||
*
|
||||
* @param workerId 工作器ID
|
||||
* @param config 配置信息
|
||||
* @return 创建的配置信息
|
||||
*/
|
||||
@PostMapping("/{workerId}")
|
||||
public ApiResponse<RenderWorkerConfigV2DTO> createWorkerConfig(@PathVariable Long workerId,
|
||||
@Valid @RequestBody RenderWorkerConfigV2DTO config) {
|
||||
log.info("创建渲染工作器配置, workerId: {}, configKey: {}", workerId, config.getConfigKey());
|
||||
|
||||
try {
|
||||
RenderWorkerConfigV2DTO result = configIntegrationService.createWorkerConfig(workerId, config);
|
||||
return ApiResponse.success(result);
|
||||
} catch (Exception e) {
|
||||
log.error("创建渲染工作器配置失败, workerId: {}", workerId, e);
|
||||
return ApiResponse.fail("创建渲染工作器配置失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新工作器配置
|
||||
*
|
||||
* @param workerId 工作器ID
|
||||
* @param configId 配置ID
|
||||
* @param updates 更新内容
|
||||
* @return 操作结果
|
||||
*/
|
||||
@PutMapping("/{workerId}/{configId}")
|
||||
public ApiResponse<Void> updateWorkerConfig(@PathVariable Long workerId,
|
||||
@PathVariable Long configId,
|
||||
@Valid @RequestBody Map<String, Object> updates) {
|
||||
log.info("更新渲染工作器配置, workerId: {}, configId: {}", workerId, configId);
|
||||
|
||||
try {
|
||||
configIntegrationService.updateWorkerConfig(workerId, configId, updates);
|
||||
return ApiResponse.success(null);
|
||||
} catch (Exception e) {
|
||||
log.error("更新渲染工作器配置失败, workerId: {}, configId: {}", workerId, configId, e);
|
||||
return ApiResponse.fail("更新渲染工作器配置失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除工作器配置
|
||||
*
|
||||
* @param workerId 工作器ID
|
||||
* @param configId 配置ID
|
||||
* @return 操作结果
|
||||
*/
|
||||
@DeleteMapping("/{workerId}/{configId}")
|
||||
public ApiResponse<Void> deleteWorkerConfig(@PathVariable Long workerId,
|
||||
@PathVariable Long configId) {
|
||||
log.info("删除渲染工作器配置, workerId: {}, configId: {}", workerId, configId);
|
||||
|
||||
try {
|
||||
configIntegrationService.deleteWorkerConfig(workerId, configId);
|
||||
return ApiResponse.success(null);
|
||||
} catch (Exception e) {
|
||||
log.error("删除渲染工作器配置失败, workerId: {}, configId: {}", workerId, configId, e);
|
||||
return ApiResponse.fail("删除渲染工作器配置失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新工作器配置
|
||||
*
|
||||
* @param workerId 工作器ID
|
||||
* @param request 批量配置请求
|
||||
* @return 操作结果
|
||||
*/
|
||||
@PostMapping("/{workerId}/batch")
|
||||
public ApiResponse<Void> batchUpdateWorkerConfigs(@PathVariable Long workerId,
|
||||
@Valid @RequestBody BatchRenderWorkerConfigRequest request) {
|
||||
log.info("批量更新渲染工作器配置, workerId: {}, configCount: {}",
|
||||
workerId, request.getConfigs() != null ? request.getConfigs().size() : 0);
|
||||
|
||||
try {
|
||||
configIntegrationService.batchUpdateWorkerConfigs(workerId, request);
|
||||
return ApiResponse.success(null);
|
||||
} catch (Exception e) {
|
||||
log.error("批量更新渲染工作器配置失败, workerId: {}", workerId, e);
|
||||
return ApiResponse.fail("批量更新渲染工作器配置失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量平铺更新工作器配置
|
||||
*
|
||||
* @param workerId 工作器ID
|
||||
* @param flatConfigs 平铺配置Map
|
||||
* @return 操作结果
|
||||
*/
|
||||
@PostMapping("/{workerId}/flat-batch")
|
||||
public ApiResponse<Void> batchFlatUpdateWorkerConfigs(@PathVariable Long workerId,
|
||||
@Valid @RequestBody Map<String, Object> flatConfigs) {
|
||||
log.info("批量平铺更新渲染工作器配置, workerId: {}, configCount: {}", workerId, flatConfigs.size());
|
||||
|
||||
try {
|
||||
configIntegrationService.batchFlatUpdateWorkerConfigs(workerId, flatConfigs);
|
||||
return ApiResponse.success(null);
|
||||
} catch (Exception e) {
|
||||
log.error("批量平铺更新渲染工作器配置失败, workerId: {}", workerId, e);
|
||||
return ApiResponse.fail("批量平铺更新渲染工作器配置失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package com.ycwl.basic.controller.pc;
|
||||
|
||||
import com.ycwl.basic.model.pc.renderWorker.entity.RenderWorkerEntity;
|
||||
import com.ycwl.basic.model.pc.renderWorker.req.RenderWorkerReqQuery;
|
||||
import com.ycwl.basic.service.pc.RenderWorkerService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* @Author:longbinbin
|
||||
* @Date:2024/12/3 14:59
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/renderWorker/v1")
|
||||
// 渲染机管理
|
||||
public class RenderWorkerController {
|
||||
|
||||
@Autowired
|
||||
private RenderWorkerService renderWorkerService;
|
||||
|
||||
// 分页查询渲染机
|
||||
@PostMapping("/page")
|
||||
public ApiResponse pageQuery(@RequestBody RenderWorkerReqQuery renderWorkerReqQuery){
|
||||
return renderWorkerService.pageQuery(renderWorkerReqQuery);
|
||||
}
|
||||
// 渲染机列表查询
|
||||
@PostMapping("/list")
|
||||
public ApiResponse list(@RequestBody RenderWorkerReqQuery renderWorkerReqQuery){
|
||||
return renderWorkerService.list(renderWorkerReqQuery);
|
||||
}
|
||||
// 渲染机详情查询
|
||||
@GetMapping("/detail/{id}")
|
||||
public ApiResponse detail(@PathVariable Long id){
|
||||
return renderWorkerService.detail(id);
|
||||
}
|
||||
|
||||
// 渲染机新增
|
||||
@PostMapping("/add")
|
||||
public ApiResponse add(@RequestBody RenderWorkerEntity renderWorker){
|
||||
return renderWorkerService.add(renderWorker);
|
||||
}
|
||||
|
||||
// 渲染机删除
|
||||
@DeleteMapping("/delete/{id}")
|
||||
public ApiResponse deleteById(@PathVariable Long id){
|
||||
return renderWorkerService.deleteById(id);
|
||||
}
|
||||
|
||||
// 渲染机修改
|
||||
@PostMapping("/update")
|
||||
public ApiResponse update(@RequestBody RenderWorkerEntity renderWorker){
|
||||
return renderWorkerService.update(renderWorker);
|
||||
}
|
||||
|
||||
// 渲染机修改状态
|
||||
@PutMapping("/updateStatus/{id}")
|
||||
public ApiResponse updateStatus(@PathVariable Long id) {
|
||||
return renderWorkerService.updateStatus(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package com.ycwl.basic.controller.pc;
|
||||
|
||||
import com.ycwl.basic.dto.RenderWorkerWithStatusDTO;
|
||||
import com.ycwl.basic.integration.common.response.PageResponse;
|
||||
import com.ycwl.basic.integration.render.dto.worker.CreateRenderWorkerRequest;
|
||||
import com.ycwl.basic.integration.render.dto.worker.RenderWorkerV2DTO;
|
||||
import com.ycwl.basic.integration.render.dto.worker.UpdateRenderWorkerRequest;
|
||||
import com.ycwl.basic.integration.render.service.RenderWorkerIntegrationService;
|
||||
import com.ycwl.basic.model.task.req.ClientStatusReqVo;
|
||||
import com.ycwl.basic.repository.RenderWorkerRepository;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 渲染工作器管理 V2 版本控制器
|
||||
* 基于 zt-render-worker 微服务标准接口实现
|
||||
*
|
||||
* @author Claude Code
|
||||
* @date 2025-09-06
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/render/worker/v2")
|
||||
@RequiredArgsConstructor
|
||||
public class RenderWorkerV2Controller {
|
||||
|
||||
private final RenderWorkerIntegrationService renderWorkerIntegrationService;
|
||||
private final RenderWorkerRepository renderWorkerRepository;
|
||||
|
||||
/**
|
||||
* 分页查询渲染工作器列表(带保活信息)
|
||||
*
|
||||
* @param page 页码,从1开始
|
||||
* @param pageSize 每页大小,默认10,最大100
|
||||
* @param isEnabled 是否启用(0-禁用,1-启用)
|
||||
* @param name 工作器名称(模糊搜索)
|
||||
* @return 分页查询结果(包含保活信息)
|
||||
*/
|
||||
@GetMapping
|
||||
public ApiResponse<PageResponse<RenderWorkerWithStatusDTO>> listWorkers(
|
||||
@RequestParam(defaultValue = "1") Integer page,
|
||||
@RequestParam(defaultValue = "10") Integer pageSize,
|
||||
@RequestParam(required = false) Integer isEnabled,
|
||||
@RequestParam(required = false) String name) {
|
||||
|
||||
log.debug("分页查询渲染工作器列表, page: {}, pageSize: {}, isEnabled: {}, name: {}",
|
||||
page, pageSize, isEnabled, name);
|
||||
|
||||
// 参数验证:限制pageSize最大值为100
|
||||
if (pageSize > 100) {
|
||||
pageSize = 100;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取基础工作器列表
|
||||
PageResponse<RenderWorkerV2DTO> basicResult = renderWorkerIntegrationService.listWorkers(
|
||||
page, pageSize, isEnabled, name);
|
||||
|
||||
// 转换为带保活信息的DTO列表
|
||||
List<RenderWorkerWithStatusDTO> workersWithStatus = new ArrayList<>();
|
||||
for (RenderWorkerV2DTO worker : basicResult.getList()) {
|
||||
RenderWorkerWithStatusDTO workerWithStatus = new RenderWorkerWithStatusDTO();
|
||||
|
||||
// 复制基础信息
|
||||
BeanUtils.copyProperties(worker, workerWithStatus);
|
||||
|
||||
// 查询保活信息
|
||||
ClientStatusReqVo hostStatus = renderWorkerRepository.getWorkerHostStatus(worker.getId());
|
||||
workerWithStatus.setHostStatus(hostStatus);
|
||||
workerWithStatus.setIsOnline(hostStatus != null);
|
||||
|
||||
workersWithStatus.add(workerWithStatus);
|
||||
}
|
||||
|
||||
// 构建带保活信息的分页响应
|
||||
PageResponse<RenderWorkerWithStatusDTO> result = new PageResponse<>();
|
||||
result.setList(workersWithStatus);
|
||||
result.setTotal(basicResult.getTotal());
|
||||
result.setPage(basicResult.getPage());
|
||||
result.setPageSize(basicResult.getPageSize());
|
||||
|
||||
return ApiResponse.success(result);
|
||||
} catch (Exception e) {
|
||||
log.error("分页查询渲染工作器列表失败", e);
|
||||
return ApiResponse.fail("分页查询渲染工作器列表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取渲染工作器详情
|
||||
*
|
||||
* @param id 工作器ID
|
||||
* @return 工作器详情
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public ApiResponse<RenderWorkerV2DTO> getWorker(@PathVariable Long id) {
|
||||
log.debug("获取渲染工作器详情, id: {}", id);
|
||||
|
||||
try {
|
||||
RenderWorkerV2DTO worker = renderWorkerIntegrationService.getWorker(id);
|
||||
return ApiResponse.success(worker);
|
||||
} catch (Exception e) {
|
||||
log.error("获取渲染工作器详情失败, id: {}", id, e);
|
||||
return ApiResponse.fail("获取渲染工作器详情失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建渲染工作器
|
||||
*
|
||||
* @param request 创建请求
|
||||
* @return 创建的工作器信息
|
||||
*/
|
||||
@PostMapping
|
||||
public ApiResponse<RenderWorkerV2DTO> createWorker(@Valid @RequestBody CreateRenderWorkerRequest request) {
|
||||
log.debug("创建渲染工作器, name: {}, key: {}, isActive: {}",
|
||||
request.getName(), request.getKey(), request.getIsActive());
|
||||
|
||||
try {
|
||||
RenderWorkerV2DTO worker = renderWorkerIntegrationService.createWorker(request);
|
||||
return ApiResponse.success(worker);
|
||||
} catch (Exception e) {
|
||||
log.error("创建渲染工作器失败", e);
|
||||
return ApiResponse.fail("创建渲染工作器失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新渲染工作器
|
||||
*
|
||||
* @param id 工作器ID
|
||||
* @param request 更新请求
|
||||
* @return 操作结果
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
public ApiResponse<Void> updateWorker(@PathVariable Long id,
|
||||
@Valid @RequestBody UpdateRenderWorkerRequest request) {
|
||||
log.debug("更新渲染工作器, id: {}, name: {}, isActive: {}",
|
||||
id, request.getName(), request.getIsActive());
|
||||
|
||||
try {
|
||||
renderWorkerIntegrationService.updateWorker(id, request);
|
||||
return ApiResponse.success(null);
|
||||
} catch (Exception e) {
|
||||
log.error("更新渲染工作器失败, id: {}", id, e);
|
||||
return ApiResponse.fail("更新渲染工作器失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除渲染工作器
|
||||
*
|
||||
* @param id 工作器ID
|
||||
* @return 操作结果
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public ApiResponse<Void> deleteWorker(@PathVariable Long id) {
|
||||
log.debug("删除渲染工作器, id: {}", id);
|
||||
|
||||
try {
|
||||
renderWorkerIntegrationService.deleteWorker(id);
|
||||
return ApiResponse.success(null);
|
||||
} catch (Exception e) {
|
||||
log.error("删除渲染工作器失败, id: {}", id, e);
|
||||
return ApiResponse.fail("删除渲染工作器失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,7 @@ 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.scenic.CreateScenicRequest;
|
||||
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
|
||||
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2ListResponse;
|
||||
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2WithConfigDTO;
|
||||
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2WithConfigListResponse;
|
||||
import com.ycwl.basic.integration.common.response.PageResponse;
|
||||
import com.ycwl.basic.integration.scenic.dto.scenic.UpdateScenicRequest;
|
||||
import com.ycwl.basic.integration.scenic.service.ScenicConfigIntegrationService;
|
||||
import com.ycwl.basic.integration.scenic.service.ScenicIntegrationService;
|
||||
@@ -52,7 +50,7 @@ public class ScenicV2Controller {
|
||||
* 景区V2核心信息分页列表
|
||||
*/
|
||||
@GetMapping("/")
|
||||
public ApiResponse<ScenicV2ListResponse> listScenics(@RequestParam(defaultValue = "1") Integer page,
|
||||
public ApiResponse<PageResponse<ScenicV2DTO>> listScenics(@RequestParam(defaultValue = "1") Integer page,
|
||||
@RequestParam(defaultValue = "10") Integer pageSize,
|
||||
@RequestParam(required = false) Integer status,
|
||||
@RequestParam(required = false) String name) {
|
||||
@@ -62,9 +60,9 @@ public class ScenicV2Controller {
|
||||
if (pageSize > 100) {
|
||||
pageSize = 100;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
ScenicV2ListResponse response = scenicIntegrationService.listScenics(page, pageSize, status, name);
|
||||
PageResponse<ScenicV2DTO> response = scenicIntegrationService.listScenics(page, pageSize, status, name, null);
|
||||
return ApiResponse.success(response);
|
||||
} catch (Exception e) {
|
||||
log.error("分页查询景区核心信息列表失败", e);
|
||||
@@ -72,30 +70,6 @@ public class ScenicV2Controller {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 景区V2带配置信息分页列表
|
||||
*/
|
||||
@GetMapping("/with-config")
|
||||
public ApiResponse<ScenicV2WithConfigListResponse> 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 {
|
||||
ScenicV2WithConfigListResponse response = scenicIntegrationService.listScenicsWithConfig(page, pageSize, status, name);
|
||||
return ApiResponse.success(response);
|
||||
} catch (Exception e) {
|
||||
log.error("分页查询景区带配置信息列表失败", e);
|
||||
return ApiResponse.fail("分页查询景区带配置信息列表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询单个景区详情
|
||||
*/
|
||||
@@ -178,11 +152,11 @@ public class ScenicV2Controller {
|
||||
* 只支持根据状态筛选
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
public ApiResponse<ScenicV2ListResponse> listScenicsByStatus(@RequestParam(required = false) Integer status) {
|
||||
public ApiResponse<PageResponse<ScenicV2DTO>> listScenicsByStatus(@RequestParam(required = false) Integer status) {
|
||||
log.info("查询景区列表, status: {}", status);
|
||||
try {
|
||||
// 默认查询1000条数据,第1页
|
||||
ScenicV2ListResponse scenics = scenicIntegrationService.listScenics(1, 1000, status, null);
|
||||
PageResponse<ScenicV2DTO> scenics = scenicIntegrationService.listScenics(1, 1000, status, null, null);
|
||||
return ApiResponse.success(scenics);
|
||||
} catch (Exception e) {
|
||||
log.error("查询景区列表失败, status: {}", status, e);
|
||||
@@ -193,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());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取景区配置列表
|
||||
*/
|
||||
@@ -317,20 +261,4 @@ public class ScenicV2Controller {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,6 @@ public class SourceController {
|
||||
@Autowired
|
||||
private SourceService sourceService;
|
||||
|
||||
@Deprecated
|
||||
// 分页查询视频源
|
||||
@PostMapping("/page")
|
||||
public ApiResponse pageQuery(@RequestBody SourceReqQuery sourceReqQuery) {
|
||||
|
||||
@@ -13,7 +13,6 @@ import org.springframework.web.bind.annotation.*;
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/task/v1")
|
||||
@Deprecated
|
||||
// 任务列表管理
|
||||
public class TaskController {
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/video/v1")
|
||||
@Deprecated
|
||||
// 视频成片管理
|
||||
public class VideoController {
|
||||
|
||||
@@ -41,4 +40,16 @@ public class VideoController {
|
||||
return videoService.getById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询视频是否被购买
|
||||
*
|
||||
* @param videoId 视频ID
|
||||
* @return 是否已购买
|
||||
*/
|
||||
@GetMapping("/checkBuyStatus")
|
||||
public ApiResponse<Boolean> checkBuyStatus(@RequestParam("videoId") Long videoId) {
|
||||
Boolean isBuy = videoService.checkVideoBuyStatus(videoId);
|
||||
return ApiResponse.success(isBuy);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
package com.ycwl.basic.controller.printer;
|
||||
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
|
||||
import com.ycwl.basic.mapper.FaceMapper;
|
||||
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
|
||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
||||
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
|
||||
import com.ycwl.basic.repository.MemberRelationRepository;
|
||||
import com.ycwl.basic.repository.SourceRepository;
|
||||
import com.ycwl.basic.service.printer.PrinterService;
|
||||
import com.ycwl.basic.service.task.TaskFaceService;
|
||||
import com.ycwl.basic.storage.adapters.IStorageAdapter;
|
||||
import com.ycwl.basic.annotation.IgnoreToken;
|
||||
import com.ycwl.basic.storage.StorageFactory;
|
||||
import com.ycwl.basic.integration.device.dto.device.DeviceV2DTO;
|
||||
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
|
||||
import com.ycwl.basic.mapper.SourceMapper;
|
||||
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
||||
import com.ycwl.basic.model.pc.mp.MpConfigEntity;
|
||||
import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery;
|
||||
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
|
||||
import com.ycwl.basic.model.printer.FaceRecognizeWithSourcesResp;
|
||||
import com.ycwl.basic.repository.DeviceRepository;
|
||||
import com.ycwl.basic.repository.FaceRepository;
|
||||
import com.ycwl.basic.repository.ScenicRepository;
|
||||
import com.ycwl.basic.service.pc.FaceService;
|
||||
import com.ycwl.basic.service.pc.ScenicService;
|
||||
import com.ycwl.basic.storage.utils.StorageUtil;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import com.ycwl.basic.utils.SnowFlakeUtil;
|
||||
import com.ycwl.basic.utils.WxMpUtil;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static com.ycwl.basic.constant.StorageConstant.USER_FACE;
|
||||
|
||||
@IgnoreToken
|
||||
// 打印机大屏对接接口
|
||||
@RestController
|
||||
@RequestMapping("/printer/v1/tv")
|
||||
@RequiredArgsConstructor
|
||||
public class PrinterTvController {
|
||||
|
||||
private final DeviceRepository deviceRepository;
|
||||
private final ScenicRepository scenicRepository;
|
||||
private final FaceRepository faceRepository;
|
||||
private final TaskFaceService faceService;
|
||||
private final FaceService pcFaceService;
|
||||
private final ScenicService scenicService;
|
||||
private final SourceMapper sourceMapper;
|
||||
private final FaceMapper faceMapper;
|
||||
private final MemberRelationRepository memberRelationRepository;
|
||||
private final SourceRepository sourceRepository;
|
||||
private final PrinterService printerService;
|
||||
|
||||
/**
|
||||
* 获取景区列表
|
||||
*
|
||||
* @return 景区列表
|
||||
*/
|
||||
@GetMapping("/scenic/list")
|
||||
public ApiResponse<List<ScenicV2DTO>> getScenicList() {
|
||||
ScenicReqQuery query = new ScenicReqQuery();
|
||||
query.setStatus("1"); // 只查询启用状态的景区
|
||||
query.setPageNum(1);
|
||||
query.setPageSize(1000);
|
||||
return ApiResponse.success(scenicRepository.list(query));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据景区ID查询设备列表
|
||||
*
|
||||
* @param scenicId 景区ID
|
||||
* @return 设备列表
|
||||
*/
|
||||
@GetMapping("/device/list")
|
||||
public ApiResponse<List<DeviceV2DTO>> getDeviceListByScenicId(@RequestParam Long scenicId) {
|
||||
List<DeviceV2DTO> result = deviceRepository.getAllDeviceByScenicId(scenicId);
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
|
||||
@GetMapping("/{sampleId}/qrcode")
|
||||
public void getQrcode(@PathVariable("sampleId") Long sampleId, HttpServletResponse response) throws Exception {
|
||||
File qrcode = new File("qrcode_"+sampleId+".jpg");
|
||||
FaceSampleEntity faceSample = faceRepository.getFaceSample(sampleId);
|
||||
if (faceSample == null) {
|
||||
response.setStatus(404);
|
||||
return;
|
||||
}
|
||||
String targetPath = "pages/printer/from_sample";
|
||||
DeviceV2DTO device = deviceRepository.getDeviceBasic(faceSample.getDeviceId());
|
||||
if (device.getType().equals("AI_CAM")) {
|
||||
// AI_CAM,需要修改path
|
||||
targetPath = "pages/ai-cam/from_sample";
|
||||
}
|
||||
try {
|
||||
MpConfigEntity scenicMpConfig = scenicRepository.getScenicMpConfig(faceSample.getScenicId());
|
||||
WxMpUtil.generateUnlimitedWXAQRCode(scenicMpConfig.getAppId(), scenicMpConfig.getAppSecret(), targetPath, sampleId.toString(), qrcode);
|
||||
|
||||
// 设置响应头
|
||||
response.setContentType("image/jpeg");
|
||||
response.setHeader("Content-Disposition", "inline; filename=\"" + qrcode.getName() + "\"");
|
||||
|
||||
// 将二维码文件写入响应输出流
|
||||
try (FileInputStream fis = new FileInputStream(qrcode);
|
||||
OutputStream os = response.getOutputStream()) {
|
||||
byte[] buffer = new byte[1024];
|
||||
int bytesRead;
|
||||
while ((bytesRead = fis.read(buffer)) != -1) {
|
||||
os.write(buffer, 0, bytesRead);
|
||||
}
|
||||
os.flush();
|
||||
}
|
||||
} finally {
|
||||
// 删除临时文件
|
||||
if (qrcode.exists()) {
|
||||
qrcode.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取人脸绑定二维码
|
||||
* 生成小程序二维码,用于绑定人脸到用户账号
|
||||
*
|
||||
* @param faceId 人脸ID
|
||||
* @param response HTTP响应
|
||||
*/
|
||||
@GetMapping("/face/{faceId}/qrcode")
|
||||
public void getFaceQrcode(@PathVariable("faceId") Long faceId, HttpServletResponse response) throws Exception {
|
||||
File qrcode = new File("qrcode_face_" + faceId + ".jpg");
|
||||
try {
|
||||
FaceEntity face = faceRepository.getFace(faceId);
|
||||
if (face == null) {
|
||||
response.setStatus(404);
|
||||
return;
|
||||
}
|
||||
MpConfigEntity scenicMpConfig = scenicRepository.getScenicMpConfig(face.getScenicId());
|
||||
if (scenicMpConfig == null) {
|
||||
response.setStatus(500);
|
||||
return;
|
||||
}
|
||||
WxMpUtil.generateUnlimitedWXAQRCode(
|
||||
scenicMpConfig.getAppId(),
|
||||
scenicMpConfig.getAppSecret(),
|
||||
"pages/videoSynthesis/bind_face",
|
||||
faceId.toString(),
|
||||
qrcode
|
||||
);
|
||||
|
||||
// 设置响应头
|
||||
response.setContentType("image/jpeg");
|
||||
response.setHeader("Content-Disposition", "inline; filename=\"" + qrcode.getName() + "\"");
|
||||
|
||||
// 将二维码文件写入响应输出流
|
||||
try (FileInputStream fis = new FileInputStream(qrcode);
|
||||
OutputStream os = response.getOutputStream()) {
|
||||
byte[] buffer = new byte[1024];
|
||||
int bytesRead;
|
||||
while ((bytesRead = fis.read(buffer)) != -1) {
|
||||
os.write(buffer, 0, bytesRead);
|
||||
}
|
||||
os.flush();
|
||||
}
|
||||
} finally {
|
||||
// 删除临时文件
|
||||
if (qrcode.exists()) {
|
||||
qrcode.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据人脸样本ID查询图像素材
|
||||
*
|
||||
* @param faceSampleId 人脸样本ID
|
||||
* @return type=2且face_sample_id匹配的source记录
|
||||
*/
|
||||
@GetMapping("/{faceSampleId}/source")
|
||||
public ApiResponse<SourceEntity> getSourceByFaceSampleId(@PathVariable Long faceSampleId) {
|
||||
SourceEntity source = sourceMapper.getBySampleIdAndType(faceSampleId, 2);
|
||||
if (source == null) {
|
||||
return ApiResponse.fail("未找到对应的图像素材");
|
||||
}
|
||||
return ApiResponse.success(source);
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印机大屏人脸识别
|
||||
* 上传照片,在景区人脸库中搜索匹配的人脸样本,返回识别结果和匹配到的图像素材
|
||||
*
|
||||
* @param file 人脸照片文件
|
||||
* @param scenicId 景区ID
|
||||
* @return 人脸识别结果和匹配的source列表
|
||||
*/
|
||||
@PostMapping("/{scenicId}/faceRecognize")
|
||||
public ApiResponse<FaceRecognizeWithSourcesResp> faceRecognize(
|
||||
@RequestParam("file") MultipartFile file,
|
||||
@PathVariable Long scenicId) throws Exception {
|
||||
|
||||
// 1. 上传人脸照片到存储
|
||||
IStorageAdapter adapter = StorageFactory.use("faces");
|
||||
String filePath = StorageUtil.joinPath(USER_FACE, DateUtil.format(new Date(), "yyyy-MM-dd"));
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
String suffix = originalFilename.split("\\.", 2)[1];
|
||||
String fileName = UUID.randomUUID() + "." + suffix;
|
||||
String faceUrl = adapter.uploadFile(file, filePath, fileName);
|
||||
|
||||
// 2. 保存人脸数据到数据库
|
||||
Long faceId = SnowFlakeUtil.getLongId();
|
||||
FaceEntity faceEntity = new FaceEntity();
|
||||
faceEntity.setId(faceId);
|
||||
faceEntity.setScenicId(scenicId);
|
||||
faceEntity.setFaceUrl(faceUrl);
|
||||
faceEntity.setCreateAt(new Date());
|
||||
faceEntity.setMemberId(0L); // 打印机大屏端没有用户ID
|
||||
faceMapper.add(faceEntity);
|
||||
|
||||
// 3. 在景区人脸库中搜索(注意:这里使用scenicId作为数据库名,搜索的是景区内的人脸样本)
|
||||
pcFaceService.matchFaceId(faceId);
|
||||
|
||||
// 4. 自动添加照片到打印列表,并获取添加成功的照片列表
|
||||
List<SourceEntity> addedSources = printerService.autoAddPhotosToPreferPrint(faceId);
|
||||
|
||||
// 5. 根据自动添加结果决定返回的sources
|
||||
List<SourceEntity> sources;
|
||||
if (addedSources != null && !addedSources.isEmpty()) {
|
||||
// 如果自动添加成功,返回添加的照片列表
|
||||
sources = addedSources;
|
||||
} else {
|
||||
// 如果自动添加为空,按原逻辑查询匹配到的图像素材(type=2)
|
||||
sources = new ArrayList<>();
|
||||
List<MemberSourceEntity> memberSourceEntities = memberRelationRepository.listSourceByFaceRelation(faceId, 2);
|
||||
for (MemberSourceEntity memberSourceEntity : memberSourceEntities) {
|
||||
SourceEntity source = sourceRepository.getSource(memberSourceEntity.getSourceId());
|
||||
if (source != null) {
|
||||
sources.add(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 构造响应
|
||||
FaceRecognizeWithSourcesResp resp = new FaceRecognizeWithSourcesResp();
|
||||
resp.setUrl(faceUrl);
|
||||
resp.setFaceId(faceId);
|
||||
resp.setScenicId(scenicId);
|
||||
resp.setSources(sources);
|
||||
// 只有当添加了照片时才返回二维码URL
|
||||
if (addedSources != null && !addedSources.isEmpty()) {
|
||||
resp.setQrcodeUrl("https://zhentuai.com/printer/v1/tv/face/" + faceId + "/qrcode");
|
||||
} else {
|
||||
resp.setQrcodeUrl(null);
|
||||
}
|
||||
|
||||
return ApiResponse.success(resp);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,453 +0,0 @@
|
||||
package com.ycwl.basic.controller.viid;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.thread.ThreadFactoryBuilder;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.ycwl.basic.utils.JacksonUtil;
|
||||
import com.ycwl.basic.annotation.IgnoreLogReq;
|
||||
import com.ycwl.basic.annotation.IgnoreToken;
|
||||
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
|
||||
import com.ycwl.basic.facebody.entity.AddFaceResp;
|
||||
import com.ycwl.basic.mapper.DeviceMapper;
|
||||
import com.ycwl.basic.mapper.FaceSampleMapper;
|
||||
import com.ycwl.basic.mapper.SourceMapper;
|
||||
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
|
||||
import com.ycwl.basic.model.pc.device.entity.DeviceCropConfig;
|
||||
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
|
||||
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
||||
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
|
||||
import com.ycwl.basic.model.viid.entity.DeviceIdObject;
|
||||
import com.ycwl.basic.model.viid.entity.FaceListObject;
|
||||
import com.ycwl.basic.model.viid.entity.FaceObject;
|
||||
import com.ycwl.basic.model.viid.entity.FacePositionObject;
|
||||
import com.ycwl.basic.model.viid.entity.ResponseStatusObject;
|
||||
import com.ycwl.basic.model.viid.entity.SubImageInfoObject;
|
||||
import com.ycwl.basic.model.viid.entity.SubImageList;
|
||||
import com.ycwl.basic.model.viid.entity.SystemTimeObject;
|
||||
import com.ycwl.basic.model.viid.req.FaceUploadReq;
|
||||
import com.ycwl.basic.model.viid.req.ImageUploadReq;
|
||||
import com.ycwl.basic.model.viid.req.KeepaliveReq;
|
||||
import com.ycwl.basic.model.viid.req.RegisterReq;
|
||||
import com.ycwl.basic.model.viid.req.UnRegisterReq;
|
||||
import com.ycwl.basic.model.viid.resp.SystemTimeResp;
|
||||
import com.ycwl.basic.model.viid.resp.VIIDBaseResp;
|
||||
import com.ycwl.basic.repository.DeviceRepository;
|
||||
import com.ycwl.basic.repository.ScenicRepository;
|
||||
import com.ycwl.basic.service.pc.ScenicService;
|
||||
import com.ycwl.basic.service.task.TaskFaceService;
|
||||
import com.ycwl.basic.storage.StorageFactory;
|
||||
import com.ycwl.basic.storage.adapters.IStorageAdapter;
|
||||
import com.ycwl.basic.storage.enums.StorageAcl;
|
||||
import com.ycwl.basic.storage.utils.StorageUtil;
|
||||
import com.ycwl.basic.task.DynamicTaskGenerator;
|
||||
import com.ycwl.basic.utils.ImageUtils;
|
||||
import com.ycwl.basic.utils.IpUtils;
|
||||
import com.ycwl.basic.utils.SnowFlakeUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import java.awt.image.RasterFormatException;
|
||||
import java.io.IOException;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static com.ycwl.basic.constant.StorageConstant.PHOTO_PATH;
|
||||
import static com.ycwl.basic.constant.StorageConstant.VIID_FACE;
|
||||
|
||||
@IgnoreToken
|
||||
@RestController
|
||||
// 摄像头对接接口
|
||||
@RequestMapping("/VIID")
|
||||
@Slf4j
|
||||
public class ViidController {
|
||||
@Autowired
|
||||
private DeviceMapper deviceMapper;
|
||||
private static final String serverId = "00000000000000000001";
|
||||
@Autowired
|
||||
private SourceMapper sourceMapper;
|
||||
@Autowired
|
||||
private DeviceRepository deviceRepository;
|
||||
@Autowired
|
||||
private ScenicRepository scenicRepository;
|
||||
@Autowired
|
||||
private TaskFaceService taskFaceService;
|
||||
private final Map<Long, ThreadPoolExecutor> executors = new ConcurrentHashMap<>();
|
||||
@Autowired
|
||||
private ScenicService scenicService;
|
||||
|
||||
private ThreadPoolExecutor getExecutor(Long scenicId) {
|
||||
return executors.computeIfAbsent(scenicId, k -> {
|
||||
ThreadFactory threadFactory = new ThreadFactoryBuilder()
|
||||
.setNamePrefix("VIID-" + scenicId + "-t")
|
||||
.build();
|
||||
return new ThreadPoolExecutor(
|
||||
4, 1024, 0L, TimeUnit.MILLISECONDS,
|
||||
new ArrayBlockingQueue<>(1024),
|
||||
threadFactory);
|
||||
});
|
||||
}
|
||||
|
||||
// region 注册注销基础接口
|
||||
/**
|
||||
* 注册接口
|
||||
*
|
||||
* @param req 注册的信息
|
||||
* @param request 请求
|
||||
* @return 返回
|
||||
*/
|
||||
@RequestMapping(value = "/System/Register", method = RequestMethod.POST)
|
||||
public VIIDBaseResp register(@RequestBody RegisterReq req, HttpServletRequest request) {
|
||||
DeviceIdObject deviceIdObject = req.getRegisterObject();
|
||||
log.info("注册的设备信息:{}", deviceIdObject);
|
||||
// 保存设备注册时间
|
||||
String deviceId = deviceIdObject.getDeviceId();
|
||||
DeviceEntity device = deviceRepository.getDeviceByDeviceNo(deviceId);
|
||||
if (device == null) {
|
||||
device = new DeviceEntity();
|
||||
device.setName("未配置设备");
|
||||
device.setNo(deviceId);
|
||||
device.setOnline(1);
|
||||
}
|
||||
device.setKeepaliveAt(new Date());
|
||||
device.setIpAddr(IpUtils.getIpAddr(request));
|
||||
if (device.getId() != null) {
|
||||
deviceMapper.updateEntity(device);
|
||||
} else {
|
||||
device.setId(SnowFlakeUtil.getLongId());
|
||||
deviceMapper.addEntity(device);
|
||||
deviceRepository.clearDeviceCache(deviceId);
|
||||
}
|
||||
return new VIIDBaseResp(
|
||||
new ResponseStatusObject(serverId, "/VIID/System/Register", "0", "注册成功", sdfTime.format(new Date()))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保活接口
|
||||
*
|
||||
* @param req 保活的设备信息
|
||||
* @param request 请求
|
||||
* @return 返回
|
||||
*/
|
||||
@IgnoreLogReq
|
||||
@RequestMapping(value = "/System/Keepalive", method = RequestMethod.POST)
|
||||
public VIIDBaseResp keepalive(@RequestBody KeepaliveReq req, HttpServletRequest request) {
|
||||
DeviceIdObject keepaliveObject = req.getKeepaliveObject();
|
||||
// log.info("对方发送的心跳的信息:{}", keepaliveObject);
|
||||
|
||||
String deviceId = keepaliveObject.getDeviceId();
|
||||
DeviceEntity device = deviceRepository.getDeviceByDeviceNo(deviceId);
|
||||
|
||||
// 判断设备状态
|
||||
if (device == null) {
|
||||
// 不存在设备就注册
|
||||
device = new DeviceEntity();
|
||||
device.setName("未配置设备");
|
||||
device.setNo(deviceId);
|
||||
device.setOnline(1);
|
||||
device.setKeepaliveAt(new Date());
|
||||
device.setIpAddr(IpUtils.getIpAddr(request));
|
||||
device.setId(SnowFlakeUtil.getLongId());
|
||||
deviceMapper.addEntity(device);
|
||||
deviceRepository.clearDeviceCache(deviceId);
|
||||
} else {
|
||||
deviceRepository.updateOnlineStatus(device.getId(), IpUtils.getIpAddr(request), 1, new Date());
|
||||
}
|
||||
// log.info("已经解析过的心跳信息:{}", keepaliveObject);
|
||||
|
||||
return new VIIDBaseResp(
|
||||
new ResponseStatusObject(deviceId, "/VIID/System/Keepalive", "0", "保活", sdfTime.format(new Date()))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销设备
|
||||
*
|
||||
* @param req 参数
|
||||
* @return 返回
|
||||
*/
|
||||
@RequestMapping(value = "/System/UnRegister", method = RequestMethod.POST)
|
||||
public VIIDBaseResp unRegister(@RequestBody UnRegisterReq req, HttpServletRequest request) {
|
||||
// 获取设备id
|
||||
DeviceIdObject unRegisterObject = req.getUnRegisterObject();
|
||||
String deviceId = unRegisterObject.getDeviceId();
|
||||
log.info("获取的注销的请求参数:{}", unRegisterObject);
|
||||
|
||||
// 首先查询该设备是否存在
|
||||
DeviceEntity device = deviceRepository.getDeviceByDeviceNo(deviceId);
|
||||
// 判断
|
||||
if (device != null) {
|
||||
deviceRepository.updateOnlineStatus(device.getId(), IpUtils.getIpAddr(request), 0, new Date());
|
||||
}
|
||||
return new VIIDBaseResp(
|
||||
new ResponseStatusObject(deviceId, "/VIID/System/UnRegister", "0", "注销成功", sdfTime.format(new Date()))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校时接口
|
||||
*
|
||||
* @return 返回
|
||||
*/
|
||||
@RequestMapping(value = "/System/Time", method = RequestMethod.GET)
|
||||
public SystemTimeResp time() {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
|
||||
return new SystemTimeResp(
|
||||
new SystemTimeObject(serverId, "2", sdf.format(new Date()), TimeZone.getTimeZone("Asia/Shanghai").toString())
|
||||
);
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
@Autowired
|
||||
private FaceSampleMapper faceSampleMapper;
|
||||
|
||||
private final SimpleDateFormat sdfTime = new SimpleDateFormat("yyyyMMddHHmmss");
|
||||
|
||||
|
||||
/**
|
||||
* 批量新增人脸
|
||||
*/
|
||||
@RequestMapping(value = "/Faces", method = RequestMethod.POST)
|
||||
@IgnoreLogReq
|
||||
public VIIDBaseResp faces(@RequestBody FaceUploadReq req) {
|
||||
FaceListObject faceListObject = req.getFaceListObject();
|
||||
List<FaceObject> faceObject = faceListObject.getFaceObject();
|
||||
String faceId = null;
|
||||
// 遍历人脸列表
|
||||
for (FaceObject face : faceObject) {
|
||||
// 设置FaceId
|
||||
faceId = face.getFaceID();
|
||||
// 获取图片信息
|
||||
SubImageList subImageList = face.getSubImageList();
|
||||
// 判断人脸对象中的列表是否为空
|
||||
String deviceID = face.getDeviceID();
|
||||
DeviceEntity device = deviceRepository.getDeviceByDeviceNo(deviceID);
|
||||
if (device == null) {
|
||||
continue;
|
||||
}
|
||||
DeviceConfigEntity deviceConfig = deviceRepository.getDeviceConfig(device.getId());
|
||||
if (deviceConfig == null) {
|
||||
log.warn("设备配置不存在:" + deviceID);
|
||||
return new VIIDBaseResp(
|
||||
new ResponseStatusObject(faceId, "/VIID/Faces", "0", "OK", sdfTime.format(new Date()))
|
||||
);
|
||||
}
|
||||
int viidMode = 0;
|
||||
if (deviceConfig.getViidType() != null) {
|
||||
viidMode = deviceConfig.getViidType();
|
||||
}
|
||||
Date shotTime = null;
|
||||
if (StringUtils.isNotBlank(face.getShotTime())) {
|
||||
try {
|
||||
shotTime = sdfTime.parse(face.getShotTime());
|
||||
} catch (ParseException e) {
|
||||
log.warn("拍摄时间时间转换失败,使用当前时间。错误entity:{}", face);
|
||||
}
|
||||
}
|
||||
if (shotTime == null) {
|
||||
if (StringUtils.isNotBlank(face.getFaceAppearTime())) {
|
||||
try {
|
||||
shotTime = sdfTime.parse(face.getFaceAppearTime());
|
||||
} catch (ParseException e) {
|
||||
log.warn("拍摄时间时间转换失败,使用当前时间。错误entity:{}", face);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shotTime == null) {
|
||||
shotTime = new Date();
|
||||
} else if (!DateUtil.isSameDay(shotTime, new Date())) {
|
||||
log.warn("时间不是今天,使用当前时间。错误entity:{}", face);
|
||||
shotTime = new Date();
|
||||
}
|
||||
if (Math.abs(shotTime.getTime() - System.currentTimeMillis()) > 3600 * 1000) {
|
||||
String jsonString = JacksonUtil.toJSONStringCompat(req);
|
||||
log.warn("时间差超过1小时。device:{},错误entity:{}", device, jsonString);
|
||||
}
|
||||
Long scenicId = device.getScenicId();
|
||||
if (scenicId == null) {
|
||||
continue;
|
||||
}
|
||||
IStorageAdapter scenicStorageAdapter = scenicService.getScenicStorageAdapter(scenicId);
|
||||
IFaceBodyAdapter faceBodyAdapter = scenicService.getScenicFaceBodyAdapter(scenicId);
|
||||
FacePositionObject facePosition = new FacePositionObject();
|
||||
facePosition.setLtY(face.getLeftTopY());
|
||||
facePosition.setLtX(face.getLeftTopX());
|
||||
facePosition.setRbY(face.getRightBtmY());
|
||||
facePosition.setRbX(face.getRightBtmX());
|
||||
if (ObjectUtil.isNotEmpty(subImageList) && CollUtil.isNotEmpty(subImageList.getSubImageInfoObject())) {
|
||||
if (viidMode == 0) {
|
||||
// 遍历每个图片对象
|
||||
// 先找到type14的图片
|
||||
List<SubImageInfoObject> type14ImageList = subImageList.getSubImageInfoObject().stream().filter(subImage -> "14".equals(subImage.getType())).toList();
|
||||
for (SubImageInfoObject subImage : subImageList.getSubImageInfoObject()) {
|
||||
// base64转换成MultipartFIle
|
||||
MultipartFile file = ImageUtils.base64ToMultipartFile(subImage.getData());
|
||||
String ext;
|
||||
if (subImage.getFileFormat().equalsIgnoreCase("jpeg")) {
|
||||
ext = "jpg";
|
||||
} else {
|
||||
ext = subImage.getFileFormat();
|
||||
}
|
||||
IStorageAdapter adapter = StorageFactory.use("faces");
|
||||
// Type=11 人脸
|
||||
if (subImage.getType().equals("11")) {
|
||||
// 上传oss
|
||||
Long newFaceSampleId = SnowFlakeUtil.getLongId();
|
||||
if (Integer.valueOf(1).equals(device.getStatus())) {
|
||||
FaceSampleEntity faceSample = new FaceSampleEntity();
|
||||
faceSample.setId(newFaceSampleId);
|
||||
faceSample.setScenicId(scenicId);
|
||||
faceSample.setDeviceId(device.getId());
|
||||
faceSample.setStatus(0);
|
||||
faceSample.setCreateAt(shotTime);
|
||||
String url = adapter.uploadFile(file, VIID_FACE, UUID.randomUUID() + "." + ext);
|
||||
faceSample.setFaceUrl(url);
|
||||
faceSampleMapper.add(faceSample);
|
||||
ThreadPoolExecutor executor = getExecutor(scenicId);
|
||||
executor.execute(() -> {
|
||||
if (faceBodyAdapter != null) {
|
||||
taskFaceService.assureFaceDb(faceBodyAdapter, scenicId.toString());
|
||||
AddFaceResp addFaceResp = faceBodyAdapter.addFace(scenicId.toString(), faceSample.getId().toString(), url, newFaceSampleId.toString());
|
||||
if (addFaceResp != null) {
|
||||
faceSample.setScore(addFaceResp.getScore());
|
||||
faceSampleMapper.updateScore(faceSample.getId(), addFaceResp.getScore());
|
||||
}
|
||||
}
|
||||
if (Integer.valueOf(1).equals(deviceConfig.getEnablePreBook())) {
|
||||
DynamicTaskGenerator.addTask(faceSample.getId());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (SubImageInfoObject _subImage : type14ImageList) {
|
||||
facePosition.setImgHeight(_subImage.getHeight());
|
||||
facePosition.setImgWidth(_subImage.getWidth());
|
||||
SourceEntity source = new SourceEntity();
|
||||
source.setDeviceId(device.getId());
|
||||
source.setScenicId(device.getScenicId());
|
||||
source.setFaceSampleId(newFaceSampleId);
|
||||
source.setCreateTime(shotTime);
|
||||
source.setType(2);
|
||||
// 上传oss
|
||||
MultipartFile _file = ImageUtils.base64ToMultipartFile(_subImage.getData());
|
||||
ThreadPoolExecutor executor = getExecutor(scenicId);
|
||||
executor.execute(() -> {
|
||||
List<DeviceCropConfig> cropConfigs = deviceConfig._getCropConfig();
|
||||
for (DeviceCropConfig cropConfig : cropConfigs) {
|
||||
source.setId(SnowFlakeUtil.getLongId());
|
||||
String filename = StorageUtil.joinPath(PHOTO_PATH, UUID.randomUUID() + "." + ext);
|
||||
MultipartFile _finalFile = _file;
|
||||
if (cropConfig.getCropType() == 1) {
|
||||
// 按固定位置截图
|
||||
try {
|
||||
_finalFile = ImageUtils.cropImage(_file, cropConfig.getTargetX(), cropConfig.getTargetY(), cropConfig.getTargetWidth(), cropConfig.getTargetHeight());
|
||||
} catch (IOException e) {
|
||||
log.error("裁切图片失败!", e);
|
||||
} catch (RasterFormatException e) {
|
||||
log.error("裁切图片出错!", e);
|
||||
}
|
||||
} else if (cropConfig.getCropType() == 2) {
|
||||
// 按人脸位置
|
||||
try {
|
||||
int targetX = facePosition.getLtX() - (cropConfig.getTargetWidth() - facePosition.getWidth())/2;
|
||||
int targetY = facePosition.getLtY() - (cropConfig.getTargetHeight() - facePosition.getHeight())/2;
|
||||
_finalFile = ImageUtils.cropImage(_file, targetX, targetY, cropConfig.getTargetWidth(), cropConfig.getTargetHeight());
|
||||
} catch (IOException e) {
|
||||
log.error("裁切图片失败!", e);
|
||||
} catch (RasterFormatException e) {
|
||||
log.error("裁切图片出错!", e);
|
||||
}
|
||||
facePosition.setImgHeight(cropConfig.getTargetHeight());
|
||||
facePosition.setImgWidth(cropConfig.getTargetWidth());
|
||||
}
|
||||
String _sourceUrl = scenicStorageAdapter.uploadFile(_finalFile, filename);
|
||||
scenicStorageAdapter.setAcl(StorageAcl.PUBLIC_READ, filename);
|
||||
source.setUrl(_sourceUrl);
|
||||
source.setPosJson(JacksonUtil.toJSONString(facePosition));
|
||||
sourceMapper.add(source);
|
||||
}
|
||||
});
|
||||
}
|
||||
log.info("人脸信息及原图{}张入库成功!设备ID:{}", type14ImageList.size(), deviceID);
|
||||
}
|
||||
}
|
||||
} else if (viidMode == 1) {
|
||||
for (SubImageInfoObject subImage : subImageList.getSubImageInfoObject()) {
|
||||
// base64转换成MultipartFIle
|
||||
MultipartFile file = ImageUtils.base64ToMultipartFile(subImage.getData());
|
||||
String ext = subImage.getFileFormat();
|
||||
if (ext.equalsIgnoreCase("jpeg")) {
|
||||
ext = "jpg";
|
||||
}
|
||||
IStorageAdapter adapter = StorageFactory.use("faces");
|
||||
// Type=14 人脸,传™的,有这么传的嘛
|
||||
if (subImage.getType().equals("14")) {
|
||||
// 上传oss
|
||||
if (Integer.valueOf(1).equals(device.getStatus())) {
|
||||
FaceSampleEntity faceSample = new FaceSampleEntity();
|
||||
Long newFaceSampleId = SnowFlakeUtil.getLongId();
|
||||
faceSample.setId(newFaceSampleId);
|
||||
faceSample.setScenicId(scenicId);
|
||||
faceSample.setDeviceId(device.getId());
|
||||
faceSample.setStatus(0);
|
||||
faceSample.setCreateAt(shotTime);
|
||||
String url = adapter.uploadFile(file, VIID_FACE, UUID.randomUUID() + "." + ext);
|
||||
faceSample.setFaceUrl(url);
|
||||
faceSampleMapper.add(faceSample);
|
||||
DynamicTaskGenerator.addTask(faceSample.getId());
|
||||
ThreadPoolExecutor executor = getExecutor(scenicId);
|
||||
executor.execute(() -> {
|
||||
if (faceBodyAdapter != null) {
|
||||
taskFaceService.assureFaceDb(faceBodyAdapter, scenicId.toString());
|
||||
AddFaceResp addFaceResp = faceBodyAdapter.addFace(scenicId.toString(), faceSample.getId().toString(), url, newFaceSampleId.toString());
|
||||
if (addFaceResp != null) {
|
||||
faceSample.setScore(addFaceResp.getScore());
|
||||
faceSampleMapper.updateScore(faceSample.getId(), addFaceResp.getScore());
|
||||
}
|
||||
}
|
||||
if (Integer.valueOf(1).equals(deviceConfig.getEnablePreBook())) {
|
||||
DynamicTaskGenerator.addTask(faceSample.getId());
|
||||
}
|
||||
});
|
||||
log.info("模式1人脸信息入库成功!设备ID:{}", deviceID);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new VIIDBaseResp(
|
||||
new ResponseStatusObject(faceId, "/VIID/Faces", "0", "OK", sdfTime.format(new Date()))
|
||||
);
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/Images", method = RequestMethod.POST)
|
||||
@IgnoreLogReq
|
||||
public VIIDBaseResp images(HttpServletRequest request, @RequestBody ImageUploadReq req) throws IOException {
|
||||
return new VIIDBaseResp(
|
||||
new ResponseStatusObject("1", "/VIID/Images", "0", "OK", sdfTime.format(new Date()))
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import com.ycwl.basic.constant.StorageConstant;
|
||||
import com.ycwl.basic.device.entity.common.FileObject;
|
||||
import com.ycwl.basic.device.operator.WvpPassiveStorageOperator;
|
||||
import com.ycwl.basic.model.wvp.WvpSyncReqVo;
|
||||
import com.ycwl.basic.service.pc.DeviceService;
|
||||
import com.ycwl.basic.service.pc.ScenicService;
|
||||
import com.ycwl.basic.storage.adapters.IStorageAdapter;
|
||||
import com.ycwl.basic.storage.enums.StorageAcl;
|
||||
@@ -30,15 +29,12 @@ import java.util.List;
|
||||
@RequestMapping("/wvp/v1/")
|
||||
public class WvpController {
|
||||
|
||||
@Autowired
|
||||
private DeviceService deviceService;
|
||||
@Autowired
|
||||
private ScenicService scenicService;
|
||||
|
||||
@IgnoreLogReq
|
||||
@PostMapping("/scenic/{scenicId}/sync")
|
||||
public ApiResponse<List<WvpPassiveStorageOperator.Task>> sync(@PathVariable("scenicId") Long scenicId, @RequestBody WvpSyncReqVo reqVo) {
|
||||
deviceService.updateDevices(scenicId, reqVo);
|
||||
return ApiResponse.success(WvpPassiveStorageOperator.getTaskListByScenicId(scenicId));
|
||||
}
|
||||
|
||||
|
||||
@@ -11,10 +11,13 @@ import com.ycwl.basic.device.operator.VptPassiveStorageOperator;
|
||||
import com.ycwl.basic.device.operator.WvpActiveStorageOperator;
|
||||
import com.ycwl.basic.device.operator.WvpPassiveStorageOperator;
|
||||
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
|
||||
import com.ycwl.basic.integration.device.dto.device.DeviceV2DTO;
|
||||
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Date;
|
||||
|
||||
public class DeviceFactory {
|
||||
public static IDeviceStorageOperator getDeviceStorageOperator(DeviceEntity device, DeviceConfigEntity config) {
|
||||
public static IDeviceStorageOperator getDeviceStorageOperator(DeviceV2DTO device, DeviceConfigEntity config) {
|
||||
IDeviceStorageOperator operator = null;
|
||||
if (config == null) {
|
||||
return null;
|
||||
@@ -35,11 +38,33 @@ public class DeviceFactory {
|
||||
if (operator == null) {
|
||||
return null;
|
||||
}
|
||||
operator.setDevice(device);
|
||||
operator.setDevice(convertToEntity(device));
|
||||
operator.setDeviceConfig(config);
|
||||
return operator;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将DeviceV2DTO转换为DeviceEntity
|
||||
*/
|
||||
private static DeviceEntity convertToEntity(DeviceV2DTO dto) {
|
||||
if (dto == null) {
|
||||
return null;
|
||||
}
|
||||
DeviceEntity entity = new DeviceEntity();
|
||||
entity.setId(dto.getId());
|
||||
entity.setName(dto.getName());
|
||||
entity.setNo(dto.getNo());
|
||||
entity.setScenicId(dto.getScenicId());
|
||||
entity.setStatus(dto.getIsActive());
|
||||
if (dto.getCreateTime() != null) {
|
||||
entity.setCreateAt(dto.getCreateTime());
|
||||
}
|
||||
if (dto.getUpdateTime() != null) {
|
||||
entity.setUpdateAt(dto.getUpdateTime());
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
public static IDeviceStatusChecker getDeviceStatusChecker(DeviceEntity device, DeviceConfigEntity config) {
|
||||
IDeviceStatusChecker checker = null;
|
||||
if (config.getOnlineCheck() <= 0) {
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
package com.ycwl.basic.device.entity.common;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 设备视频连续性检查缓存实体
|
||||
* 用于存储在Redis中的检查结果
|
||||
*
|
||||
* @author Claude Code
|
||||
* @date 2025-09-01
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DeviceVideoContinuityCache implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 设备ID
|
||||
*/
|
||||
private Long deviceId;
|
||||
|
||||
/**
|
||||
* 检查时间
|
||||
*/
|
||||
private Date checkTime;
|
||||
|
||||
/**
|
||||
* 检查的开始时间
|
||||
*/
|
||||
private Date startTime;
|
||||
|
||||
/**
|
||||
* 检查的结束时间
|
||||
*/
|
||||
private Date endTime;
|
||||
|
||||
/**
|
||||
* 是否支持连续性检查
|
||||
*/
|
||||
private Boolean support;
|
||||
|
||||
/**
|
||||
* 视频是否连续
|
||||
*/
|
||||
private Boolean continuous;
|
||||
|
||||
/**
|
||||
* 视频总数
|
||||
*/
|
||||
private Integer totalVideos;
|
||||
|
||||
/**
|
||||
* 总时长(毫秒)
|
||||
*/
|
||||
private Long totalDurationMs;
|
||||
|
||||
/**
|
||||
* 允许的最大间隙(毫秒)
|
||||
*/
|
||||
private Long maxAllowedGapMs;
|
||||
|
||||
/**
|
||||
* 间隙数量
|
||||
*/
|
||||
private Integer gapCount;
|
||||
|
||||
/**
|
||||
* 间隙列表(简化版,只包含关键信息)
|
||||
*/
|
||||
private List<GapInfo> gaps;
|
||||
|
||||
/**
|
||||
* 间隙信息简化类
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class GapInfo implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 前一个文件名
|
||||
*/
|
||||
private String beforeFileName;
|
||||
|
||||
/**
|
||||
* 后一个文件名
|
||||
*/
|
||||
private String afterFileName;
|
||||
|
||||
/**
|
||||
* 间隙时长(毫秒)
|
||||
*/
|
||||
private Long gapMs;
|
||||
|
||||
/**
|
||||
* 间隙开始时间
|
||||
*/
|
||||
private Date gapStartTime;
|
||||
|
||||
/**
|
||||
* 间隙结束时间
|
||||
*/
|
||||
private Date gapEndTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从VideoContinuityResult创建缓存对象
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @param result 检查结果
|
||||
* @param startTime 检查开始时间
|
||||
* @param endTime 检查结束时间
|
||||
* @return 缓存对象
|
||||
*/
|
||||
public static DeviceVideoContinuityCache fromResult(Long deviceId, VideoContinuityResult result,
|
||||
Date startTime, Date endTime) {
|
||||
DeviceVideoContinuityCache cache = new DeviceVideoContinuityCache();
|
||||
cache.setDeviceId(deviceId);
|
||||
cache.setCheckTime(new Date());
|
||||
cache.setStartTime(startTime);
|
||||
cache.setEndTime(endTime);
|
||||
cache.setSupport(result.isSupport());
|
||||
cache.setContinuous(result.isContinuous());
|
||||
cache.setTotalVideos(result.getTotalVideos());
|
||||
cache.setTotalDurationMs(result.getTotalDurationMs());
|
||||
cache.setMaxAllowedGapMs(result.getMaxAllowedGapMs());
|
||||
cache.setGapCount(result.getGapCount());
|
||||
|
||||
// 转换间隙列表
|
||||
if (result.getGaps() != null && !result.getGaps().isEmpty()) {
|
||||
List<GapInfo> gapInfos = result.getGaps().stream()
|
||||
.map(gap -> new GapInfo(
|
||||
gap.getBeforeFile() != null ? gap.getBeforeFile().getName() : null,
|
||||
gap.getAfterFile() != null ? gap.getAfterFile().getName() : null,
|
||||
gap.getGapMs(),
|
||||
gap.getGapStartTime(),
|
||||
gap.getGapEndTime()
|
||||
))
|
||||
.toList();
|
||||
cache.setGaps(gapInfos);
|
||||
}
|
||||
|
||||
return cache;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.ycwl.basic.device.entity.common;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 视频连续性检查中的间隙信息
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class VideoContinuityGap {
|
||||
/**
|
||||
* 间隙前的视频文件
|
||||
*/
|
||||
private FileObject beforeFile;
|
||||
|
||||
/**
|
||||
* 间隙后的视频文件
|
||||
*/
|
||||
private FileObject afterFile;
|
||||
|
||||
/**
|
||||
* 间隙时长(毫秒)
|
||||
*/
|
||||
private long gapMs;
|
||||
|
||||
/**
|
||||
* 间隙开始时间(前一个视频的endTime)
|
||||
*/
|
||||
private Date gapStartTime;
|
||||
|
||||
/**
|
||||
* 间隙结束时间(后一个视频的createTime)
|
||||
*/
|
||||
private Date gapEndTime;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.ycwl.basic.device.entity.common;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 视频连续性检查结果
|
||||
*/
|
||||
@Data
|
||||
public class VideoContinuityResult {
|
||||
/**
|
||||
* 是否支持连续性检查功能
|
||||
*/
|
||||
private boolean support;
|
||||
|
||||
/**
|
||||
* 视频是否连续(所有间隙都在允许范围内)
|
||||
*/
|
||||
private boolean continuous;
|
||||
|
||||
/**
|
||||
* 检测到的间隙列表
|
||||
*/
|
||||
private List<VideoContinuityGap> gaps = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 视频文件总数
|
||||
*/
|
||||
private int totalVideos;
|
||||
|
||||
/**
|
||||
* 总时长(毫秒)
|
||||
*/
|
||||
private long totalDurationMs;
|
||||
|
||||
/**
|
||||
* 允许的最大间隙(毫秒)
|
||||
*/
|
||||
private long maxAllowedGapMs;
|
||||
|
||||
/**
|
||||
* 添加一个间隙
|
||||
*/
|
||||
public void addGap(VideoContinuityGap gap) {
|
||||
this.gaps.add(gap);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取间隙数量
|
||||
*/
|
||||
public int getGapCount() {
|
||||
return gaps.size();
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,50 @@
|
||||
package com.ycwl.basic.device.operator;
|
||||
|
||||
import com.ycwl.basic.device.entity.common.VideoContinuityResult;
|
||||
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
|
||||
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
|
||||
public abstract class ADeviceStorageOperator implements IDeviceStorageOperator {
|
||||
@Setter
|
||||
protected DeviceEntity device;
|
||||
@Setter
|
||||
protected DeviceConfigEntity deviceConfig;
|
||||
|
||||
/**
|
||||
* 默认实现:不支持视频连续性检查
|
||||
*
|
||||
* @param startDate 开始时间
|
||||
* @param endDate 结束时间
|
||||
* @param maxGapMs 允许的最大间隔时间(毫秒)
|
||||
* @return support=false的结果
|
||||
*/
|
||||
@Override
|
||||
public VideoContinuityResult checkVideoContinuity(Date startDate, Date endDate, long maxGapMs) {
|
||||
VideoContinuityResult result = new VideoContinuityResult();
|
||||
result.setSupport(false);
|
||||
result.setContinuous(false);
|
||||
result.setTotalVideos(0);
|
||||
result.setTotalDurationMs(0);
|
||||
result.setMaxAllowedGapMs(maxGapMs);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认实现:不支持视频连续性检查
|
||||
*
|
||||
* @return support=false的结果
|
||||
*/
|
||||
@Override
|
||||
public VideoContinuityResult checkRecentVideoContinuity() {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.add(Calendar.MINUTE, -2);
|
||||
Date endDate = calendar.getTime();
|
||||
calendar.add(Calendar.MINUTE, -5);
|
||||
Date startDate = calendar.getTime();
|
||||
return checkVideoContinuity(startDate, endDate, 2000L);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.ycwl.basic.device.operator;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import com.ycwl.basic.utils.JacksonUtil;
|
||||
import com.ycwl.basic.device.entity.common.FileObject;
|
||||
import com.ycwl.basic.device.entity.common.VideoContinuityGap;
|
||||
import com.ycwl.basic.device.entity.common.VideoContinuityResult;
|
||||
import com.ycwl.basic.storage.StorageFactory;
|
||||
import com.ycwl.basic.storage.adapters.IStorageAdapter;
|
||||
import com.ycwl.basic.storage.entity.AliOssStorageConfig;
|
||||
@@ -69,22 +71,14 @@ public class AliOssStorageOperator extends ADeviceStorageOperator {
|
||||
if (startDate == null || endDate == null) {
|
||||
return null;
|
||||
}
|
||||
List<FileObject> fileList = new ArrayList<>();
|
||||
if (startDate.after(endDate)) {
|
||||
return fileList;
|
||||
}
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTime(startDate);
|
||||
calendar.set(Calendar.SECOND, 0);
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
|
||||
while (calendar.getTime().before(endDate)) {
|
||||
String prefix = dateFormat.format(calendar.getTime());
|
||||
List<FileObject> fileListByPrefix = getOssFileListByPrefix(prefix);
|
||||
if (fileListByPrefix == null) {
|
||||
return null;
|
||||
}
|
||||
fileList.addAll(fileListByPrefix);
|
||||
calendar.add(Calendar.MINUTE, 1);
|
||||
String prefix = dateFormat.format(calendar.getTime());
|
||||
List<FileObject> fileList = getOssFileListByPrefix(prefix);
|
||||
if (fileList == null) {
|
||||
return null;
|
||||
}
|
||||
calendar.clear();
|
||||
return fileList.stream()
|
||||
@@ -106,4 +100,104 @@ public class AliOssStorageOperator extends ADeviceStorageOperator {
|
||||
String prefix = dateFormat.format(calendar.getTime());
|
||||
return removeFilesByPrefix(prefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查视频片段的连续性
|
||||
*
|
||||
* @param startDate 开始时间
|
||||
* @param endDate 结束时间
|
||||
* @param maxGapMs 允许的最大间隔时间(毫秒)
|
||||
* @return 包含缺口信息的验证结果
|
||||
*/
|
||||
@Override
|
||||
public VideoContinuityResult checkVideoContinuity(Date startDate, Date endDate, long maxGapMs) {
|
||||
VideoContinuityResult result = new VideoContinuityResult();
|
||||
result.setSupport(true);
|
||||
result.setMaxAllowedGapMs(maxGapMs);
|
||||
|
||||
// 获取时间范围内的视频列表
|
||||
List<FileObject> fileList = getFileListByDtRange(startDate, endDate);
|
||||
|
||||
if (fileList == null || fileList.isEmpty()) {
|
||||
result.setContinuous(false);
|
||||
result.setTotalVideos(0);
|
||||
result.setTotalDurationMs(0);
|
||||
log.warn("未找到指定时间范围内的视频文件: {} - {}", startDate, endDate);
|
||||
return result;
|
||||
}
|
||||
|
||||
result.setTotalVideos(fileList.size());
|
||||
|
||||
// 只有一个视频文件时,认为是连续的
|
||||
if (fileList.size() == 1) {
|
||||
FileObject file = fileList.get(0);
|
||||
long duration = file.getEndTime().getTime() - file.getCreateTime().getTime();
|
||||
result.setContinuous(true);
|
||||
result.setTotalDurationMs(duration);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 检查相邻视频之间的间隙
|
||||
long totalDuration = 0;
|
||||
for (int i = 0; i < fileList.size() - 1; i++) {
|
||||
FileObject currentFile = fileList.get(i);
|
||||
FileObject nextFile = fileList.get(i + 1);
|
||||
|
||||
// 计算当前视频的时长
|
||||
totalDuration += currentFile.getEndTime().getTime() - currentFile.getCreateTime().getTime();
|
||||
|
||||
// 计算间隙: 后一个视频的开始时间 - 前一个视频的结束时间
|
||||
long gapMs = nextFile.getCreateTime().getTime() - currentFile.getEndTime().getTime();
|
||||
|
||||
// 如果间隙超过允许值,记录该间隙
|
||||
if (gapMs > maxGapMs) {
|
||||
VideoContinuityGap gap = new VideoContinuityGap();
|
||||
gap.setBeforeFile(currentFile);
|
||||
gap.setAfterFile(nextFile);
|
||||
gap.setGapMs(gapMs);
|
||||
gap.setGapStartTime(currentFile.getEndTime());
|
||||
gap.setGapEndTime(nextFile.getCreateTime());
|
||||
result.addGap(gap);
|
||||
log.debug("检测到视频间隙: {} -> {}, 间隙时长: {}ms",
|
||||
currentFile.getName(), nextFile.getName(), gapMs);
|
||||
}
|
||||
}
|
||||
|
||||
// 加上最后一个视频的时长
|
||||
FileObject lastFile = fileList.get(fileList.size() - 1);
|
||||
totalDuration += lastFile.getEndTime().getTime() - lastFile.getCreateTime().getTime();
|
||||
|
||||
result.setTotalDurationMs(totalDuration);
|
||||
result.setContinuous(result.getGapCount() == 0);
|
||||
|
||||
log.info("视频连续性检查完成: 总视频数={}, 总时长={}ms, 间隙数={}, 连续={}",
|
||||
result.getTotalVideos(), result.getTotalDurationMs(), result.getGapCount(), result.isContinuous());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查近期视频的连续性(测试用)
|
||||
* 时间范围: 当前时间向前2分钟后,再向前10分钟(即前12分钟到前2分钟)
|
||||
* 允许的最大间隙: 2秒
|
||||
*
|
||||
* @return 包含缺口信息的验证结果
|
||||
*/
|
||||
@Override
|
||||
public VideoContinuityResult checkRecentVideoContinuity() {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
|
||||
// 结束时间: 当前时间 - 2分钟
|
||||
calendar.add(Calendar.MINUTE, -2);
|
||||
Date endDate = calendar.getTime();
|
||||
|
||||
// 开始时间: 当前时间 - 12分钟 (再向前10分钟)
|
||||
calendar.add(Calendar.MINUTE, -10);
|
||||
Date startDate = calendar.getTime();
|
||||
|
||||
log.info("检查近期视频连续性: {} - {}", startDate, endDate);
|
||||
|
||||
// 允许的最大间隙为2秒(2000毫秒)
|
||||
return checkVideoContinuity(startDate, endDate, 2000L);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.ycwl.basic.device.operator;
|
||||
|
||||
import com.ycwl.basic.device.IDeviceCommon;
|
||||
import com.ycwl.basic.device.entity.common.FileObject;
|
||||
import com.ycwl.basic.device.entity.common.VideoContinuityResult;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
@@ -19,10 +20,29 @@ public interface IDeviceStorageOperator extends IDeviceCommon {
|
||||
List<FileObject> getFileListByDtRange(Date startDate, Date endDate);
|
||||
|
||||
/**
|
||||
* 删除指定日期之前的文件,不包含指定的日期当天
|
||||
* 删除指定日期之前的文件,不包含指定的日期当天
|
||||
*
|
||||
* @param date 指定日期,不包含指定日期当天
|
||||
* @param date 指定日期,不包含指定日期当天
|
||||
* @return
|
||||
*/
|
||||
boolean removeFilesBeforeDate(Date date);
|
||||
|
||||
/**
|
||||
* 检查视频片段的连续性
|
||||
*
|
||||
* @param startDate 开始时间
|
||||
* @param endDate 结束时间
|
||||
* @param maxGapMs 允许的最大间隔时间(毫秒)
|
||||
* @return 包含缺口信息的验证结果
|
||||
*/
|
||||
VideoContinuityResult checkVideoContinuity(Date startDate, Date endDate, long maxGapMs);
|
||||
|
||||
/**
|
||||
* 检查近期视频的连续性(便捷方法)
|
||||
* 时间范围: 当前时间向前2分钟后,再向前5分钟(即前7分钟到前2分钟)
|
||||
* 允许的最大间隙: 2秒
|
||||
*
|
||||
* @return 包含缺口信息的验证结果
|
||||
*/
|
||||
VideoContinuityResult checkRecentVideoContinuity();
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package com.ycwl.basic.device.operator;
|
||||
|
||||
import com.ycwl.basic.device.entity.common.FileObject;
|
||||
import com.ycwl.basic.device.entity.common.VideoContinuityResult;
|
||||
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
|
||||
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
@@ -34,4 +36,24 @@ public class LocalStorageOperator implements IDeviceStorageOperator {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VideoContinuityResult checkVideoContinuity(Date startDate, Date endDate, long maxGapMs) {
|
||||
VideoContinuityResult result = new VideoContinuityResult();
|
||||
result.setSupport(false);
|
||||
result.setContinuous(false);
|
||||
result.setTotalVideos(0);
|
||||
result.setTotalDurationMs(0);
|
||||
result.setMaxAllowedGapMs(maxGapMs);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VideoContinuityResult checkRecentVideoContinuity() {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.add(Calendar.MINUTE, -2);
|
||||
Date endDate = calendar.getTime();
|
||||
calendar.add(Calendar.MINUTE, -5);
|
||||
Date startDate = calendar.getTime();
|
||||
return checkVideoContinuity(startDate, endDate, 2000L);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,8 @@ public class VptPassiveStorageOperator extends ADeviceStorageOperator {
|
||||
if (StringUtils.isNotBlank(config.getDeviceNo())) {
|
||||
task.deviceNo = config.getDeviceNo();
|
||||
} else {
|
||||
task.deviceNo = device.getNo2();
|
||||
log.warn("设备未配置deviceNo:{}", device);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
task.startTime = startDate;
|
||||
task.endTime = endDate;
|
||||
|
||||
@@ -81,7 +81,8 @@ public class WvpPassiveStorageOperator extends ADeviceStorageOperator {
|
||||
if (StringUtils.isNotBlank(config.getDeviceNo())) {
|
||||
task.deviceNo = config.getDeviceNo();
|
||||
} else {
|
||||
task.deviceNo = device.getNo2();
|
||||
log.warn("设备未配置deviceNo:{}", device);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
task.startTime = startDate;
|
||||
task.endTime = endDate;
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.ycwl.basic.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.ycwl.basic.model.task.req.ClientStatusReqVo;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 带保活信息的渲染工作器DTO
|
||||
*/
|
||||
@Data
|
||||
public class RenderWorkerWithStatusDTO {
|
||||
|
||||
/**
|
||||
* 工作器ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 工作器名称
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 工作器标识
|
||||
*/
|
||||
private String key;
|
||||
|
||||
/**
|
||||
* 是否启用 (0-禁用,1-启用)
|
||||
*/
|
||||
@JsonProperty("isActive")
|
||||
private Integer isActive;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@JsonProperty("createTime")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@JsonProperty("updateTime")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updateTime;
|
||||
|
||||
/**
|
||||
* 主机保活状态信息
|
||||
*/
|
||||
private ClientStatusReqVo hostStatus;
|
||||
|
||||
/**
|
||||
* 是否在线(基于保活信息判断)
|
||||
*/
|
||||
private Boolean isOnline;
|
||||
}
|
||||
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 || sourceType == 3);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
package com.ycwl.basic.face.pipeline.core;
|
||||
|
||||
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
|
||||
import com.ycwl.basic.face.pipeline.enums.FaceMatchingScene;
|
||||
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
|
||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
||||
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
||||
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
|
||||
import com.ycwl.basic.pipeline.core.PipelineContext;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 人脸匹配管线上下文
|
||||
* 在各个Stage之间传递状态和数据
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class FaceMatchingContext implements PipelineContext {
|
||||
|
||||
// ==================== 核心字段(构造时必填)====================
|
||||
|
||||
/**
|
||||
* 人脸ID(必填)
|
||||
*/
|
||||
private final Long faceId;
|
||||
|
||||
/**
|
||||
* 是否新用户
|
||||
*/
|
||||
private final boolean isNew;
|
||||
|
||||
// ==================== 场景标识 ====================
|
||||
|
||||
/**
|
||||
* 场景标识
|
||||
*/
|
||||
private FaceMatchingScene scene;
|
||||
|
||||
/**
|
||||
* 手动选择的样本ID(自定义匹配场景)
|
||||
*/
|
||||
private List<Long> faceSampleIds;
|
||||
|
||||
// ==================== 中间状态 ====================
|
||||
|
||||
/**
|
||||
* 人脸实体
|
||||
*/
|
||||
private FaceEntity face;
|
||||
|
||||
/**
|
||||
* 景区配置管理器
|
||||
*/
|
||||
private ScenicConfigManager scenicConfig;
|
||||
|
||||
/**
|
||||
* 人脸识别适配器
|
||||
*/
|
||||
private IFaceBodyAdapter faceBodyAdapter;
|
||||
|
||||
/**
|
||||
* 人脸搜索结果
|
||||
*/
|
||||
private SearchFaceRespVo searchResult;
|
||||
|
||||
/**
|
||||
* 人脸样本列表(自定义匹配场景)
|
||||
*/
|
||||
private List<FaceSampleEntity> faceSamples;
|
||||
|
||||
/**
|
||||
* 匹配到的样本ID列表
|
||||
*/
|
||||
private List<Long> sampleListIds;
|
||||
|
||||
/**
|
||||
* 源文件关联列表
|
||||
*/
|
||||
private List<MemberSourceEntity> memberSourceList;
|
||||
|
||||
/**
|
||||
* 免费源文件ID列表
|
||||
*/
|
||||
private List<Long> freeSourceIds;
|
||||
|
||||
/**
|
||||
* 人脸选择后置模式配置(自定义匹配场景)
|
||||
* 0: 并集, 1: 交集, 2: 直接使用
|
||||
*/
|
||||
private Integer faceSelectPostMode;
|
||||
|
||||
// ==================== 输出结果 ====================
|
||||
|
||||
/**
|
||||
* 最终结果
|
||||
*/
|
||||
private SearchFaceRespVo finalResult;
|
||||
|
||||
// ==================== Stage配置 ====================
|
||||
|
||||
/**
|
||||
* Stage开关配置表
|
||||
* Key: stageId, Value: 是否启用
|
||||
*/
|
||||
private Map<String, Boolean> stageEnabledMap = new HashMap<>();
|
||||
|
||||
// ==================== 构造函数(私有)====================
|
||||
|
||||
private FaceMatchingContext(Builder builder) {
|
||||
this.faceId = builder.faceId;
|
||||
this.isNew = builder.isNew;
|
||||
this.scene = builder.scene;
|
||||
this.faceSampleIds = builder.faceSampleIds;
|
||||
}
|
||||
|
||||
// ==================== 静态工厂方法 ====================
|
||||
|
||||
/**
|
||||
* 获取 Builder
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速创建自动匹配场景Context
|
||||
*/
|
||||
public static FaceMatchingContext forAutoMatching(Long faceId, boolean isNew) {
|
||||
return FaceMatchingContext.builder()
|
||||
.faceId(faceId)
|
||||
.isNew(isNew)
|
||||
.scene(FaceMatchingScene.AUTO_MATCHING)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速创建自定义匹配场景Context
|
||||
*/
|
||||
public static FaceMatchingContext forCustomMatching(Long faceId, List<Long> faceSampleIds) {
|
||||
return FaceMatchingContext.builder()
|
||||
.faceId(faceId)
|
||||
.isNew(false)
|
||||
.faceSampleIds(faceSampleIds)
|
||||
.scene(FaceMatchingScene.CUSTOM_MATCHING)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速创建仅识别场景Context
|
||||
*/
|
||||
public static FaceMatchingContext forRecognitionOnly(Long faceId) {
|
||||
return FaceMatchingContext.builder()
|
||||
.faceId(faceId)
|
||||
.isNew(false)
|
||||
.scene(FaceMatchingScene.RECOGNITION_ONLY)
|
||||
.build();
|
||||
}
|
||||
|
||||
// ==================== 业务方法 ====================
|
||||
|
||||
/**
|
||||
* 判断指定Stage是否启用
|
||||
*
|
||||
* @param stageId Stage唯一标识
|
||||
* @param defaultEnabled 默认值(当配置未指定时使用)
|
||||
* @return true-启用, false-禁用
|
||||
*/
|
||||
@Override
|
||||
public boolean isStageEnabled(String stageId, boolean defaultEnabled) {
|
||||
return stageEnabledMap.getOrDefault(stageId, defaultEnabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断指定Stage是否启用(默认为false)
|
||||
*
|
||||
* @param stageId Stage唯一标识
|
||||
* @return true-启用, false-禁用
|
||||
*/
|
||||
@Override
|
||||
public boolean isStageEnabled(String stageId) {
|
||||
return stageEnabledMap.getOrDefault(stageId, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置指定Stage的启用状态
|
||||
*
|
||||
* @param stageId Stage唯一标识
|
||||
* @param enabled 是否启用
|
||||
* @return this(支持链式调用)
|
||||
*/
|
||||
public FaceMatchingContext setStageState(String stageId, boolean enabled) {
|
||||
stageEnabledMap.put(stageId, enabled);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用指定Stage
|
||||
*
|
||||
* @param stageId Stage唯一标识
|
||||
* @return this(支持链式调用)
|
||||
*/
|
||||
public FaceMatchingContext enableStage(String stageId) {
|
||||
stageEnabledMap.put(stageId, true);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用指定Stage
|
||||
*
|
||||
* @param stageId Stage唯一标识
|
||||
* @return this(支持链式调用)
|
||||
*/
|
||||
public FaceMatchingContext disableStage(String stageId) {
|
||||
stageEnabledMap.put(stageId, false);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置Stage启用状态
|
||||
*
|
||||
* @param stages Stage配置Map(stageId -> enabled)
|
||||
* @return this(支持链式调用)
|
||||
*/
|
||||
public FaceMatchingContext setStages(Map<String, Boolean> stages) {
|
||||
if (stages != null) {
|
||||
stageEnabledMap.putAll(stages);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有Stage配置
|
||||
*
|
||||
* @return this(支持链式调用)
|
||||
*/
|
||||
public FaceMatchingContext clearStages() {
|
||||
stageEnabledMap.clear();
|
||||
return this;
|
||||
}
|
||||
|
||||
// ==================== Builder ====================
|
||||
|
||||
public static class Builder {
|
||||
private Long faceId;
|
||||
private boolean isNew = false;
|
||||
private FaceMatchingScene scene;
|
||||
private List<Long> faceSampleIds;
|
||||
|
||||
public Builder faceId(Long faceId) {
|
||||
this.faceId = faceId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder isNew(boolean isNew) {
|
||||
this.isNew = isNew;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder scene(FaceMatchingScene scene) {
|
||||
this.scene = scene;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder faceSampleIds(List<Long> faceSampleIds) {
|
||||
this.faceSampleIds = faceSampleIds;
|
||||
return this;
|
||||
}
|
||||
|
||||
public FaceMatchingContext build() {
|
||||
// 参数校验
|
||||
if (faceId == null) {
|
||||
throw new IllegalArgumentException("faceId is required");
|
||||
}
|
||||
if (scene == null) {
|
||||
throw new IllegalArgumentException("scene is required");
|
||||
}
|
||||
// 自定义匹配场景必须提供faceSampleIds
|
||||
if (scene == FaceMatchingScene.CUSTOM_MATCHING && (faceSampleIds == null || faceSampleIds.isEmpty())) {
|
||||
throw new IllegalArgumentException("faceSampleIds is required for CUSTOM_MATCHING scene");
|
||||
}
|
||||
return new FaceMatchingContext(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.ycwl.basic.face.pipeline.enums;
|
||||
|
||||
/**
|
||||
* 人脸匹配场景枚举
|
||||
*/
|
||||
public enum FaceMatchingScene {
|
||||
|
||||
/**
|
||||
* 自动人脸匹配
|
||||
* 新用户上传人脸后自动执行匹配,或老用户重新匹配
|
||||
*/
|
||||
AUTO_MATCHING,
|
||||
|
||||
/**
|
||||
* 自定义人脸匹配
|
||||
* 用户手动选择人脸样本进行匹配
|
||||
*/
|
||||
CUSTOM_MATCHING,
|
||||
|
||||
/**
|
||||
* 仅识别
|
||||
* 只执行人脸识别,不处理后续业务逻辑(源文件关联、任务创建等)
|
||||
*/
|
||||
RECOGNITION_ONLY
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.ycwl.basic.face.pipeline.exception;
|
||||
|
||||
/**
|
||||
* Stage执行异常
|
||||
*/
|
||||
public class StageExecutionException extends RuntimeException {
|
||||
|
||||
private final String stageName;
|
||||
|
||||
public StageExecutionException(String stageName, String message) {
|
||||
super(String.format("[%s] %s", stageName, message));
|
||||
this.stageName = stageName;
|
||||
}
|
||||
|
||||
public StageExecutionException(String stageName, String message, Throwable cause) {
|
||||
super(String.format("[%s] %s", stageName, message), cause);
|
||||
this.stageName = stageName;
|
||||
}
|
||||
|
||||
public String getStageName() {
|
||||
return stageName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
package com.ycwl.basic.face.pipeline.factory;
|
||||
|
||||
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||
import com.ycwl.basic.pipeline.core.Pipeline;
|
||||
import com.ycwl.basic.pipeline.core.PipelineBuilder;
|
||||
import com.ycwl.basic.face.pipeline.enums.FaceMatchingScene;
|
||||
import com.ycwl.basic.face.pipeline.stages.*;
|
||||
import com.ycwl.basic.service.pc.helper.ScenicConfigFacade;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 人脸匹配Pipeline工厂
|
||||
* 负责为不同场景组装Pipeline
|
||||
*
|
||||
* 支持的场景:
|
||||
* 1. 自动人脸匹配(新用户/老用户)
|
||||
* 2. 自定义人脸匹配
|
||||
* 3. 仅识别
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class FaceMatchingPipelineFactory {
|
||||
|
||||
// ==================== 通用 Stage(13个)====================
|
||||
@Autowired
|
||||
private PrepareContextStage prepareContextStage;
|
||||
@Autowired
|
||||
private RecordMetricsStage recordMetricsStage;
|
||||
@Autowired
|
||||
private FaceRecognitionStage faceRecognitionStage;
|
||||
@Autowired
|
||||
private FaceRecoveryStage faceRecoveryStage;
|
||||
@Autowired
|
||||
private UpdateFaceResultStage updateFaceResultStage;
|
||||
@Autowired
|
||||
private BuildSourceRelationStage buildSourceRelationStage;
|
||||
@Autowired
|
||||
private ProcessFreeSourceStage processFreeSourceStage;
|
||||
@Autowired
|
||||
private ProcessBuyStatusStage processBuyStatusStage;
|
||||
@Autowired
|
||||
private HandleVideoRecreationStage handleVideoRecreationStage;
|
||||
@Autowired
|
||||
private PersistRelationsStage persistRelationsStage;
|
||||
@Autowired
|
||||
private CreateTaskStage createTaskStage;
|
||||
@Autowired
|
||||
private SetTaskStatusStage setTaskStatusStage;
|
||||
@Autowired
|
||||
private GeneratePuzzleStage generatePuzzleStage;
|
||||
|
||||
// ==================== 自定义匹配专属 Stage(6个)====================
|
||||
@Autowired
|
||||
private RecordCustomMatchMetricsStage recordCustomMatchMetricsStage;
|
||||
@Autowired
|
||||
private LoadFaceSamplesStage loadFaceSamplesStage;
|
||||
@Autowired
|
||||
private CustomFaceSearchStage customFaceSearchStage;
|
||||
@Autowired
|
||||
private LoadMatchedSamplesStage loadMatchedSamplesStage;
|
||||
@Autowired
|
||||
private FilterByTimeRangeStage filterByTimeRangeStage;
|
||||
@Autowired
|
||||
private FilterByDevicePhotoLimitStage filterByDevicePhotoLimitStage;
|
||||
@Autowired
|
||||
private DeleteOldRelationsStage deleteOldRelationsStage;
|
||||
|
||||
// ==================== 辅助服务 ====================
|
||||
@Autowired
|
||||
private ScenicConfigFacade scenicConfigFacade;
|
||||
|
||||
/**
|
||||
* 创建自动人脸匹配Pipeline
|
||||
*
|
||||
* @param isNew 是否新用户
|
||||
* @return Pipeline
|
||||
*/
|
||||
public Pipeline<FaceMatchingContext> createAutoMatchingPipeline(boolean isNew) {
|
||||
PipelineBuilder<FaceMatchingContext> builder = new PipelineBuilder<>("AutoMatching-" + (isNew ? "New" : "Old"));
|
||||
|
||||
// 1. 准备上下文
|
||||
builder.addStage(prepareContextStage);
|
||||
|
||||
// 2. 新用户设置任务状态
|
||||
if (isNew) {
|
||||
builder.addStage(setTaskStatusStage);
|
||||
}
|
||||
|
||||
// 3. 记录识别次数
|
||||
builder.addStage(recordMetricsStage);
|
||||
|
||||
// 4. 执行人脸识别
|
||||
builder.addStage(faceRecognitionStage);
|
||||
|
||||
// 5. 人脸识别补救
|
||||
builder.addStage(faceRecoveryStage);
|
||||
|
||||
// 6. 更新人脸结果
|
||||
builder.addStage(updateFaceResultStage);
|
||||
|
||||
// 7. 构建源文件关联
|
||||
builder.addStage(buildSourceRelationStage);
|
||||
|
||||
// 8. 处理免费源文件逻辑
|
||||
builder.addStage(processFreeSourceStage);
|
||||
|
||||
// 9. 处理购买状态
|
||||
builder.addStage(processBuyStatusStage);
|
||||
|
||||
// 10. 处理视频重切
|
||||
builder.addStage(handleVideoRecreationStage);
|
||||
|
||||
// 11. 持久化关联关系
|
||||
builder.addStage(persistRelationsStage);
|
||||
|
||||
// 12. 创建任务
|
||||
builder.addStage(createTaskStage);
|
||||
|
||||
// 13. 异步生成拼图模板
|
||||
builder.addStage(generatePuzzleStage);
|
||||
|
||||
log.debug("创建自动人脸匹配Pipeline: isNew={}, stageCount={}", isNew, builder.build().getStageCount());
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建自定义人脸匹配Pipeline
|
||||
*
|
||||
* @return Pipeline
|
||||
*/
|
||||
public Pipeline<FaceMatchingContext> createCustomMatchingPipeline() {
|
||||
PipelineBuilder<FaceMatchingContext> builder = new PipelineBuilder<>("CustomMatching");
|
||||
|
||||
// 1. 准备上下文
|
||||
builder.addStage(prepareContextStage);
|
||||
|
||||
// 2. 记录自定义匹配次数
|
||||
builder.addStage(recordCustomMatchMetricsStage);
|
||||
|
||||
// 3. 加载用户选择的人脸样本
|
||||
builder.addStage(loadFaceSamplesStage);
|
||||
|
||||
// 4. 根据配置执行自定义人脸搜索
|
||||
builder.addStage(customFaceSearchStage);
|
||||
|
||||
// 5. 加载匹配样本实体到缓存
|
||||
builder.addStage(loadMatchedSamplesStage);
|
||||
|
||||
// 6. 应用时间范围筛选
|
||||
builder.addStage(filterByTimeRangeStage);
|
||||
|
||||
// 7. 应用设备照片数量限制筛选
|
||||
builder.addStage(filterByDevicePhotoLimitStage);
|
||||
|
||||
// 8. 更新人脸结果
|
||||
builder.addStage(updateFaceResultStage);
|
||||
|
||||
// 9. 删除旧关系数据
|
||||
builder.addStage(deleteOldRelationsStage);
|
||||
|
||||
// 10. 构建源文件关联
|
||||
builder.addStage(buildSourceRelationStage);
|
||||
|
||||
// 11. 处理免费源文件逻辑
|
||||
builder.addStage(processFreeSourceStage);
|
||||
|
||||
// 12. 处理购买状态
|
||||
builder.addStage(processBuyStatusStage);
|
||||
|
||||
// 13. 处理视频重切
|
||||
builder.addStage(handleVideoRecreationStage);
|
||||
|
||||
// 14. 持久化关联关系
|
||||
builder.addStage(persistRelationsStage);
|
||||
|
||||
// 15. 创建任务
|
||||
builder.addStage(createTaskStage);
|
||||
|
||||
log.debug("创建自定义人脸匹配Pipeline: stageCount={}", builder.build().getStageCount());
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建仅识别Pipeline
|
||||
* 只执行人脸识别,不处理后续业务逻辑
|
||||
*
|
||||
* @return Pipeline
|
||||
*/
|
||||
public Pipeline<FaceMatchingContext> createRecognitionOnlyPipeline() {
|
||||
PipelineBuilder<FaceMatchingContext> builder = new PipelineBuilder<>("RecognitionOnly");
|
||||
|
||||
// 1. 准备上下文
|
||||
builder.addStage(prepareContextStage);
|
||||
|
||||
// 2. 执行人脸识别
|
||||
builder.addStage(faceRecognitionStage);
|
||||
|
||||
// 3. 人脸识别补救
|
||||
builder.addStage(faceRecoveryStage);
|
||||
|
||||
log.debug("创建仅识别Pipeline: stageCount={}", builder.build().getStageCount());
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据场景创建Pipeline
|
||||
*
|
||||
* @param scene 场景
|
||||
* @param isNew 是否新用户(仅AUTO_MATCHING场景需要)
|
||||
* @return Pipeline
|
||||
*/
|
||||
public Pipeline<FaceMatchingContext> createPipeline(FaceMatchingScene scene, boolean isNew) {
|
||||
return switch (scene) {
|
||||
case AUTO_MATCHING -> createAutoMatchingPipeline(isNew);
|
||||
case CUSTOM_MATCHING -> createCustomMatchingPipeline();
|
||||
case RECOGNITION_ONLY -> createRecognitionOnlyPipeline();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据Context创建Pipeline
|
||||
*
|
||||
* @param context 上下文
|
||||
* @return Pipeline
|
||||
*/
|
||||
public Pipeline<FaceMatchingContext> createPipeline(FaceMatchingContext context) {
|
||||
return createPipeline(context.getScene(), context.isNew());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package com.ycwl.basic.face.pipeline.helper;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
|
||||
import com.ycwl.basic.puzzle.dto.PuzzleGenerateRequest;
|
||||
import com.ycwl.basic.puzzle.dto.PuzzleGenerateResponse;
|
||||
import com.ycwl.basic.puzzle.dto.PuzzleTemplateDTO;
|
||||
import com.ycwl.basic.puzzle.service.IPuzzleGenerateService;
|
||||
import com.ycwl.basic.puzzle.service.IPuzzleTemplateService;
|
||||
import com.ycwl.basic.repository.ScenicRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 拼图生成编排器
|
||||
* 负责编排拼图模板的批量生成逻辑
|
||||
*
|
||||
* 职责:
|
||||
* 1. 查询景区的所有启用拼图模板
|
||||
* 2. 构建动态数据
|
||||
* 3. 逐个生成拼图图片
|
||||
* 4. 记录统计信息
|
||||
*
|
||||
* 设计说明:
|
||||
* - 从GeneratePuzzleStage中抽离出来,符合"薄Stage,厚Service"原则
|
||||
* - Stage只负责触发异步任务,业务逻辑由此Orchestrator承担
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class PuzzleGenerationOrchestrator {
|
||||
|
||||
@Autowired
|
||||
private IPuzzleTemplateService puzzleTemplateService;
|
||||
|
||||
@Autowired
|
||||
private IPuzzleGenerateService puzzleGenerateService;
|
||||
|
||||
@Autowired
|
||||
private ScenicRepository scenicRepository;
|
||||
|
||||
/**
|
||||
* 异步生成景区所有启用的拼图模板
|
||||
*
|
||||
* @param scenicId 景区ID
|
||||
* @param faceId 人脸ID
|
||||
* @param memberId 会员ID
|
||||
* @param faceUrl 人脸URL
|
||||
*/
|
||||
public void generateAllTemplatesAsync(Long scenicId, Long faceId, Long memberId, String faceUrl) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
log.info("开始异步生成景区拼图模板: scenicId={}, faceId={}", scenicId, faceId);
|
||||
|
||||
// 1. 查询该景区所有启用状态的拼图模板
|
||||
List<PuzzleTemplateDTO> templateList = puzzleTemplateService.listTemplates(
|
||||
scenicId, null, 1); // 查询启用状态的模板
|
||||
|
||||
if (templateList == null || templateList.isEmpty()) {
|
||||
log.info("景区不存在启用的拼图模板,跳过生成: scenicId={}", scenicId);
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("景区存在 {} 个启用的拼图模板,开始逐个生成: scenicId={}", templateList.size(), scenicId);
|
||||
|
||||
// 2. 获取景区信息用于动态数据
|
||||
ScenicV2DTO scenicBasic = scenicRepository.getScenicBasic(scenicId);
|
||||
|
||||
// 3. 准备公共动态数据
|
||||
Map<String, String> baseDynamicData = buildBaseDynamicData(faceId, faceUrl, scenicBasic);
|
||||
|
||||
// 4. 使用虚拟线程池并行生成所有模板
|
||||
java.util.concurrent.atomic.AtomicInteger successCount = new java.util.concurrent.atomic.AtomicInteger(0);
|
||||
java.util.concurrent.atomic.AtomicInteger failCount = new java.util.concurrent.atomic.AtomicInteger(0);
|
||||
|
||||
try (java.util.concurrent.ExecutorService executor = java.util.concurrent.Executors.newVirtualThreadPerTaskExecutor()) {
|
||||
// 为每个模板创建一个异步任务
|
||||
List<java.util.concurrent.CompletableFuture<Void>> futures = templateList.stream()
|
||||
.map(template -> java.util.concurrent.CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
generateSingleTemplate(scenicId, faceId, memberId, template, baseDynamicData);
|
||||
successCount.incrementAndGet();
|
||||
} catch (Exception e) {
|
||||
log.error("拼图生成失败: scenicId={}, templateCode={}, templateName={}",
|
||||
scenicId, template.getCode(), template.getName(), e);
|
||||
failCount.incrementAndGet();
|
||||
}
|
||||
}, executor))
|
||||
.toList();
|
||||
|
||||
// 等待所有任务完成
|
||||
java.util.concurrent.CompletableFuture.allOf(futures.toArray(new java.util.concurrent.CompletableFuture[0])).join();
|
||||
}
|
||||
|
||||
log.info("景区拼图模板批量生成完成: scenicId={}, 总数={}, 成功={}, 失败={}",
|
||||
scenicId, templateList.size(), successCount.get(), failCount.get());
|
||||
|
||||
} catch (Exception e) {
|
||||
// 异步任务失败不影响主流程,仅记录日志
|
||||
log.error("异步生成拼图模板失败: scenicId={}, faceId={}", scenicId, faceId, e);
|
||||
}
|
||||
}, "PuzzleTemplateGenerator-" + scenicId + "-" + faceId).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建基础动态数据
|
||||
*/
|
||||
private Map<String, String> buildBaseDynamicData(Long faceId, String faceUrl, ScenicV2DTO scenicBasic) {
|
||||
Map<String, String> baseDynamicData = new HashMap<>();
|
||||
|
||||
if (faceUrl != null) {
|
||||
baseDynamicData.put("faceImage", faceUrl);
|
||||
baseDynamicData.put("userAvatar", faceUrl);
|
||||
}
|
||||
|
||||
baseDynamicData.put("faceId", String.valueOf(faceId));
|
||||
baseDynamicData.put("scenicName", scenicBasic.getName());
|
||||
baseDynamicData.put("scenicText", scenicBasic.getName());
|
||||
baseDynamicData.put("dateStr", DateUtil.format(new Date(), "yyyy.MM.dd"));
|
||||
|
||||
return baseDynamicData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成单个拼图模板
|
||||
*/
|
||||
private void generateSingleTemplate(Long scenicId, Long faceId, Long memberId,
|
||||
PuzzleTemplateDTO template,
|
||||
Map<String, String> baseDynamicData) {
|
||||
log.info("开始生成拼图: scenicId={}, templateCode={}, templateName={}",
|
||||
scenicId, template.getCode(), template.getName());
|
||||
|
||||
// 构建生成请求
|
||||
PuzzleGenerateRequest generateRequest = new PuzzleGenerateRequest();
|
||||
generateRequest.setScenicId(scenicId);
|
||||
generateRequest.setUserId(memberId);
|
||||
generateRequest.setFaceId(faceId);
|
||||
generateRequest.setBusinessType("face_matching");
|
||||
generateRequest.setTemplateCode(template.getCode());
|
||||
generateRequest.setOutputFormat("PNG");
|
||||
generateRequest.setQuality(90);
|
||||
generateRequest.setDynamicData(new HashMap<>(baseDynamicData));
|
||||
generateRequest.setRequireRuleMatch(true);
|
||||
|
||||
// 调用拼图生成服务
|
||||
PuzzleGenerateResponse response = puzzleGenerateService.generate(generateRequest);
|
||||
|
||||
log.info("拼图生成成功: scenicId={}, templateCode={}, imageUrl={}",
|
||||
scenicId, template.getCode(), response.getImageUrl());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.ycwl.basic.face.pipeline.stages;
|
||||
|
||||
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||
import com.ycwl.basic.pipeline.annotation.StageConfig;
|
||||
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
|
||||
import com.ycwl.basic.pipeline.core.StageResult;
|
||||
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
|
||||
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
||||
import com.ycwl.basic.service.pc.processor.SourceRelationProcessor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 构建源文件关联Stage
|
||||
* 负责根据匹配到的样本ID构建member_source关联关系
|
||||
*
|
||||
* 职责:
|
||||
* 1. 从context.sampleListIds读取匹配的样本ID列表
|
||||
* 2. 调用sourceRelationProcessor.processMemberSources()生成MemberSourceEntity列表
|
||||
* 3. 更新context.memberSourceList
|
||||
*
|
||||
* 前置条件: context.sampleListIds不为空
|
||||
* 后置条件: context.memberSourceList已设置
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@StageConfig(
|
||||
stageId = "build_source_relation",
|
||||
optionalMode = StageOptionalMode.UNSUPPORT,
|
||||
description = "构建源文件关联关系"
|
||||
)
|
||||
public class BuildSourceRelationStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||
|
||||
@Autowired
|
||||
private SourceRelationProcessor sourceRelationProcessor;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "BuildSourceRelation";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
|
||||
// 只有当sampleListIds不为空时才执行
|
||||
List<Long> sampleListIds = context.getSampleListIds();
|
||||
if (sampleListIds == null || sampleListIds.isEmpty()) {
|
||||
// 从searchResult中获取
|
||||
if (context.getSearchResult() != null) {
|
||||
sampleListIds = context.getSearchResult().getSampleListIds();
|
||||
context.setSampleListIds(sampleListIds);
|
||||
}
|
||||
}
|
||||
|
||||
if (sampleListIds == null || sampleListIds.isEmpty()) {
|
||||
log.debug("sampleListIds为空,跳过源文件关联,faceId={}", context.getFaceId());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||
List<Long> sampleListIds = context.getSampleListIds();
|
||||
Long faceId = context.getFaceId();
|
||||
|
||||
// 防御性检查:sampleListIds为空
|
||||
if (sampleListIds == null || sampleListIds.isEmpty()) {
|
||||
// 尝试从searchResult中获取
|
||||
if (context.getSearchResult() != null) {
|
||||
sampleListIds = context.getSearchResult().getSampleListIds();
|
||||
if (sampleListIds != null && !sampleListIds.isEmpty()) {
|
||||
context.setSampleListIds(sampleListIds);
|
||||
} else {
|
||||
log.debug("sampleListIds为空,跳过源文件关联,faceId={}", faceId);
|
||||
return StageResult.skipped("sampleListIds为空");
|
||||
}
|
||||
} else {
|
||||
log.debug("sampleListIds为空,跳过源文件关联,faceId={}", faceId);
|
||||
return StageResult.skipped("sampleListIds为空");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// 处理源文件关联
|
||||
List<MemberSourceEntity> memberSourceEntityList =
|
||||
sourceRelationProcessor.processMemberSources(sampleListIds, context.getFace());
|
||||
|
||||
if (memberSourceEntityList == null || memberSourceEntityList.isEmpty()) {
|
||||
log.warn("未找到有效的源文件,faceId={}, sampleListIds={}", faceId, sampleListIds);
|
||||
return StageResult.skipped("未找到有效的源文件");
|
||||
}
|
||||
|
||||
context.setMemberSourceList(memberSourceEntityList);
|
||||
|
||||
log.info("构建源文件关联成功: faceId={}, 关联源文件数={}", faceId, memberSourceEntityList.size());
|
||||
|
||||
return StageResult.success(String.format("构建了%d个源文件关联", memberSourceEntityList.size()));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("构建源文件关联失败,faceId={}, sampleListIds={}", faceId, sampleListIds, e);
|
||||
// 源文件关联失败不影响主流程,返回降级
|
||||
return StageResult.degraded("构建源文件关联失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.ycwl.basic.face.pipeline.stages;
|
||||
|
||||
import com.ycwl.basic.biz.TaskStatusBiz;
|
||||
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||
import com.ycwl.basic.pipeline.annotation.StageConfig;
|
||||
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
|
||||
import com.ycwl.basic.pipeline.core.StageResult;
|
||||
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
|
||||
import com.ycwl.basic.service.pc.helper.ScenicConfigFacade;
|
||||
import com.ycwl.basic.service.task.TaskService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 创建任务Stage
|
||||
* 负责根据配置决定是否自动创建任务
|
||||
*
|
||||
* 职责:
|
||||
* 1. 检查face_select_first配置
|
||||
* 2. 如果配置为false,则调用taskService.autoCreateTaskByFaceId()
|
||||
* 3. 如果配置为true,则设置任务状态为2(等待用户选择)
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@StageConfig(
|
||||
stageId = "create_task",
|
||||
optionalMode = StageOptionalMode.UNSUPPORT,
|
||||
description = "根据配置创建视频任务"
|
||||
)
|
||||
public class CreateTaskStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||
|
||||
@Autowired
|
||||
private ScenicConfigFacade scenicConfigFacade;
|
||||
|
||||
@Autowired
|
||||
private TaskService taskService;
|
||||
|
||||
@Autowired
|
||||
private TaskStatusBiz taskStatusBiz;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "CreateTask";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||
Long scenicId = context.getFace().getScenicId();
|
||||
Long faceId = context.getFaceId();
|
||||
|
||||
try {
|
||||
boolean faceSelectFirst = scenicConfigFacade.isFaceSelectFirst(scenicId);
|
||||
|
||||
if (!faceSelectFirst) {
|
||||
// 配置为自动创建任务
|
||||
taskService.autoCreateTaskByFaceId(faceId);
|
||||
log.info("自动创建任务成功: faceId={}", faceId);
|
||||
return StageResult.success("自动创建任务成功");
|
||||
} else {
|
||||
// 配置为等待用户选择
|
||||
taskStatusBiz.setFaceCutStatus(faceId, 2);
|
||||
log.debug("景区配置 face_select_first=true,跳过自动创建任务: faceId={}", faceId);
|
||||
return StageResult.skipped("等待用户手动选择");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("创建任务失败,faceId={}", faceId, e);
|
||||
// 任务创建失败不影响主流程,返回降级而不是失败
|
||||
return StageResult.degraded("任务创建失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.ycwl.basic.face.pipeline.stages;
|
||||
|
||||
import com.ycwl.basic.exception.BaseException;
|
||||
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||
import com.ycwl.basic.pipeline.annotation.StageConfig;
|
||||
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
|
||||
import com.ycwl.basic.pipeline.core.StageResult;
|
||||
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
|
||||
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
||||
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
|
||||
import com.ycwl.basic.service.pc.helper.SearchResultMerger;
|
||||
import com.ycwl.basic.service.task.TaskFaceService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 自定义人脸搜索Stage
|
||||
* 负责根据faceSelectPostMode执行不同的搜索策略
|
||||
*
|
||||
* 职责:
|
||||
* 1. 从context.faceSelectPostMode读取配置
|
||||
* 2. 模式2: 直接使用用户选择的样本,不搜索
|
||||
* 3. 模式0/1: 对每个样本搜索,然后合并结果
|
||||
* 4. 更新context.searchResult
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@StageConfig(
|
||||
stageId = "custom_face_search",
|
||||
optionalMode = StageOptionalMode.FORCE_ON,
|
||||
description = "根据配置执行自定义人脸搜索"
|
||||
)
|
||||
public class CustomFaceSearchStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||
|
||||
@Autowired
|
||||
private TaskFaceService taskFaceService;
|
||||
|
||||
@Autowired
|
||||
private SearchResultMerger resultMerger;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "CustomFaceSearch";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||
Integer faceSelectPostMode = context.getFaceSelectPostMode();
|
||||
List<FaceSampleEntity> faceSamples = context.getFaceSamples();
|
||||
List<Long> faceSampleIds = context.getFaceSampleIds();
|
||||
Long faceId = context.getFaceId();
|
||||
|
||||
if (faceSelectPostMode == null) {
|
||||
faceSelectPostMode = 0; // 默认为并集模式
|
||||
}
|
||||
|
||||
log.debug("face_select_post_mode配置值: {}, faceId={}", faceSelectPostMode, faceId);
|
||||
|
||||
try {
|
||||
SearchFaceRespVo mergedResult;
|
||||
|
||||
// 模式2:不搜索,直接使用用户选择的faceSampleIds
|
||||
if (Integer.valueOf(2).equals(faceSelectPostMode)) {
|
||||
log.debug("使用模式2:直接使用用户选择的人脸样本,不进行搜索,faceId={}", faceId);
|
||||
mergedResult = resultMerger.createDirectResult(faceSampleIds);
|
||||
// 保留原始matchResult
|
||||
if (context.getFace().getMatchResult() != null) {
|
||||
mergedResult.setSearchResultJson(context.getFace().getMatchResult());
|
||||
}
|
||||
} else {
|
||||
// 模式0(并集)和模式1(交集):需要进行搜索
|
||||
List<SearchFaceRespVo> searchResults = new ArrayList<>();
|
||||
|
||||
for (FaceSampleEntity faceSample : faceSamples) {
|
||||
try {
|
||||
SearchFaceRespVo result = taskFaceService.searchFace(
|
||||
context.getFaceBodyAdapter(),
|
||||
String.valueOf(context.getFace().getScenicId()),
|
||||
faceSample.getFaceUrl(),
|
||||
"自定义人脸匹配");
|
||||
|
||||
if (result != null) {
|
||||
searchResults.add(result);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("人脸样本搜索失败,faceSampleId={}, faceUrl={}, faceId={}",
|
||||
faceSample.getId(), faceSample.getFaceUrl(), faceId, e);
|
||||
// 继续处理其他样本,不中断整个流程
|
||||
}
|
||||
}
|
||||
|
||||
if (searchResults.isEmpty()) {
|
||||
log.warn("所有人脸样本搜索都失败,faceId={}, faceSampleIds={}", faceId, faceSampleIds);
|
||||
throw new BaseException("人脸识别失败,请重试");
|
||||
}
|
||||
|
||||
// 根据模式整合多个搜索结果
|
||||
mergedResult = resultMerger.merge(searchResults, faceSelectPostMode);
|
||||
}
|
||||
|
||||
context.setSearchResult(mergedResult);
|
||||
context.setSampleListIds(mergedResult.getSampleListIds());
|
||||
|
||||
log.info("自定义人脸搜索完成: faceId={}, mode={}, 匹配数={}",
|
||||
faceId, faceSelectPostMode,
|
||||
mergedResult.getSampleListIds() != null ? mergedResult.getSampleListIds().size() : 0);
|
||||
|
||||
return StageResult.success(String.format("自定义搜索完成,模式=%d", faceSelectPostMode));
|
||||
|
||||
} catch (BaseException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("自定义人脸搜索失败,faceId={}, faceSampleIds={}", faceId, faceSampleIds, e);
|
||||
return StageResult.failed("自定义人脸搜索失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.ycwl.basic.face.pipeline.stages;
|
||||
|
||||
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||
import com.ycwl.basic.pipeline.annotation.StageConfig;
|
||||
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
|
||||
import com.ycwl.basic.pipeline.core.StageResult;
|
||||
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
|
||||
import com.ycwl.basic.mapper.SourceMapper;
|
||||
import com.ycwl.basic.mapper.VideoMapper;
|
||||
import com.ycwl.basic.repository.MemberRelationRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 删除旧关系Stage
|
||||
* 负责在保存新关系前,删除该人脸的旧数据关系
|
||||
*
|
||||
* 职责:
|
||||
* 1. 删除member_source中该人脸的未购买关系
|
||||
* 2. 删除member_video中该人脸的未购买关系
|
||||
* 3. 清除缓存
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@StageConfig(
|
||||
stageId = "delete_old_relations",
|
||||
optionalMode = StageOptionalMode.FORCE_ON,
|
||||
description = "删除人脸旧关系数据"
|
||||
)
|
||||
public class DeleteOldRelationsStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||
|
||||
@Autowired
|
||||
private SourceMapper sourceMapper;
|
||||
|
||||
@Autowired
|
||||
private VideoMapper videoMapper;
|
||||
|
||||
@Autowired
|
||||
private MemberRelationRepository memberRelationRepository;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "DeleteOldRelations";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||
Long faceId = context.getFaceId();
|
||||
Long memberId = context.getFace().getMemberId();
|
||||
|
||||
try {
|
||||
log.debug("删除人脸旧关系数据:faceId={}, memberId={}", faceId, memberId);
|
||||
|
||||
// 1. 删除member_source中的未购买关系
|
||||
sourceMapper.deleteNotBuyFaceRelation(memberId, faceId);
|
||||
|
||||
// 2. 删除member_video中的未购买关系
|
||||
videoMapper.deleteNotBuyFaceRelations(memberId, faceId);
|
||||
|
||||
// 3. 清除缓存
|
||||
memberRelationRepository.clearSCacheByFace(faceId);
|
||||
|
||||
log.debug("人脸旧关系数据删除完成:faceId={}", faceId);
|
||||
|
||||
return StageResult.success("旧关系数据已删除");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("删除旧关系数据失败,faceId={}", faceId, e);
|
||||
// 删除失败不影响主流程,返回降级
|
||||
return StageResult.degraded("删除旧关系数据失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.ycwl.basic.face.pipeline.stages;
|
||||
|
||||
import com.ycwl.basic.exception.BaseException;
|
||||
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||
import com.ycwl.basic.pipeline.annotation.StageConfig;
|
||||
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
|
||||
import com.ycwl.basic.pipeline.core.StageResult;
|
||||
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
|
||||
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
|
||||
import com.ycwl.basic.service.task.TaskFaceService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 人脸识别Stage
|
||||
* 负责执行核心的人脸识别搜索
|
||||
*
|
||||
* 职责:
|
||||
* 1. 调用taskFaceService.searchFace()执行人脸搜索
|
||||
* 2. 将结果存入context.searchResult
|
||||
* 3. 识别失败则返回FAILED
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@StageConfig(
|
||||
stageId = "face_recognition",
|
||||
optionalMode = StageOptionalMode.FORCE_ON,
|
||||
description = "执行人脸识别搜索"
|
||||
)
|
||||
public class FaceRecognitionStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||
|
||||
@Autowired
|
||||
private TaskFaceService taskFaceService;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "FaceRecognition";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||
try {
|
||||
SearchFaceRespVo searchResult = taskFaceService.searchFace(
|
||||
context.getFaceBodyAdapter(),
|
||||
String.valueOf(context.getFace().getScenicId()),
|
||||
context.getFace().getFaceUrl(),
|
||||
"人脸识别");
|
||||
|
||||
if (searchResult == null) {
|
||||
log.warn("人脸识别返回结果为空,faceId={}", context.getFaceId());
|
||||
return StageResult.failed("人脸识别失败,请换一张试试把~");
|
||||
}
|
||||
|
||||
context.setSearchResult(searchResult);
|
||||
|
||||
log.info("人脸识别完成: faceId={}, score={}, 匹配数={}",
|
||||
context.getFaceId(),
|
||||
searchResult.getScore(),
|
||||
searchResult.getSampleListIds() != null ? searchResult.getSampleListIds().size() : 0);
|
||||
|
||||
return StageResult.success(String.format("识别成功,匹配数=%d",
|
||||
searchResult.getSampleListIds() != null ? searchResult.getSampleListIds().size() : 0));
|
||||
|
||||
} catch (BaseException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("人脸识别服务调用失败,faceId={}, scenicId={}",
|
||||
context.getFaceId(), context.getFace().getScenicId(), e);
|
||||
return StageResult.failed("人脸识别失败,请换一张试试把~", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.ycwl.basic.face.pipeline.stages;
|
||||
|
||||
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||
import com.ycwl.basic.pipeline.annotation.StageConfig;
|
||||
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
|
||||
import com.ycwl.basic.pipeline.core.StageResult;
|
||||
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
|
||||
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
|
||||
import com.ycwl.basic.service.pc.processor.FaceRecoveryStrategy;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 人脸识别补救Stage
|
||||
* 负责执行人脸识别的补救逻辑(降级)
|
||||
*
|
||||
* 职责:
|
||||
* 1. 从context.searchResult读取识别结果
|
||||
* 2. 调用faceRecoveryStrategy.executeFaceRecoveryLogic()执行补救
|
||||
* 3. 如果触发补救,更新searchResult并返回DEGRADED
|
||||
* 4. 否则返回SUCCESS
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@StageConfig(
|
||||
stageId = "face_recovery",
|
||||
optionalMode = StageOptionalMode.SUPPORT,
|
||||
description = "执行人脸识别补救逻辑",
|
||||
defaultEnabled = true
|
||||
)
|
||||
public class FaceRecoveryStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||
|
||||
@Autowired
|
||||
private FaceRecoveryStrategy faceRecoveryStrategy;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "FaceRecovery";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
|
||||
// 只有当searchResult不为空时才执行
|
||||
if (context.getSearchResult() == null) {
|
||||
log.debug("searchResult为空,跳过补救逻辑,faceId={}", context.getFaceId());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||
SearchFaceRespVo searchResult = context.getSearchResult();
|
||||
Long faceId = context.getFaceId();
|
||||
|
||||
// 防御性检查:searchResult为空
|
||||
if (searchResult == null) {
|
||||
log.debug("searchResult为空,跳过补救逻辑,faceId={}", faceId);
|
||||
return StageResult.skipped("searchResult为空");
|
||||
}
|
||||
|
||||
try {
|
||||
// 执行补救逻辑(补救逻辑内部会判断是否需要触发)
|
||||
SearchFaceRespVo recoveredResult = faceRecoveryStrategy.executeFaceRecoveryLogic(
|
||||
searchResult,
|
||||
context.getScenicConfig(),
|
||||
context.getFaceBodyAdapter(),
|
||||
context.getFace().getScenicId());
|
||||
|
||||
// 如果结果发生变化,说明触发了补救
|
||||
if (recoveredResult != searchResult) {
|
||||
context.setSearchResult(recoveredResult);
|
||||
log.info("触发补救逻辑,重新搜索: faceId={}", faceId);
|
||||
return StageResult.degraded("触发补救逻辑,重新搜索");
|
||||
}
|
||||
|
||||
return StageResult.success("无需补救");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("补救逻辑执行失败,faceId={}", faceId, e);
|
||||
// 补救失败不影响主流程,返回降级
|
||||
return StageResult.degraded("补救逻辑执行失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
package com.ycwl.basic.face.pipeline.stages;
|
||||
|
||||
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||
import com.ycwl.basic.pipeline.annotation.StageConfig;
|
||||
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
|
||||
import com.ycwl.basic.pipeline.core.StageResult;
|
||||
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
|
||||
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
|
||||
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
||||
import com.ycwl.basic.repository.DeviceRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 按设备照片数量限制筛选样本Stage
|
||||
* 负责根据设备配置的照片数量限制(limit_photo)筛选匹配样本
|
||||
*
|
||||
* 职责:
|
||||
* 1. 从context.faceSamples读取样本实体缓存
|
||||
* 2. 按设备ID分组
|
||||
* 3. 对每个设备,根据其limit_photo配置筛选样本:
|
||||
* - 如果样本数 > limit_photo + 2: 按时间排序,去掉首尾,保留中间limit_photo张
|
||||
* - 如果样本数 > limit_photo + 1: 按时间排序,去掉尾部,保留前limit_photo张
|
||||
* - 如果样本数 > limit_photo: 保留前limit_photo张
|
||||
* - 否则: 保留全部
|
||||
* 4. 更新context.sampleListIds
|
||||
*
|
||||
* 前置条件: context.faceSamples不为空 (由LoadMatchedSamplesStage加载)
|
||||
* 配置说明: limit_photo=0或null表示不限制数量
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@StageConfig(
|
||||
stageId = "filter_by_device_photo_limit",
|
||||
optionalMode = StageOptionalMode.SUPPORT,
|
||||
description = "按设备照片数量限制筛选样本",
|
||||
defaultEnabled = true
|
||||
)
|
||||
public class FilterByDevicePhotoLimitStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||
|
||||
@Autowired
|
||||
private DeviceRepository deviceRepository;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "FilterByDevicePhotoLimit";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
|
||||
// 检查faceSamples是否为空
|
||||
List<FaceSampleEntity> faceSamples = context.getFaceSamples();
|
||||
if (faceSamples == null || faceSamples.isEmpty()) {
|
||||
log.debug("faceSamples为空,跳过设备照片限制筛选,faceId={}", context.getFaceId());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||
List<FaceSampleEntity> faceSamples = context.getFaceSamples();
|
||||
List<Long> sampleListIds = context.getSampleListIds();
|
||||
Long faceId = context.getFaceId();
|
||||
|
||||
// 防御性检查:faceSamples为空
|
||||
if (faceSamples == null || faceSamples.isEmpty()) {
|
||||
log.debug("faceSamples为空,跳过设备照片限制筛选,faceId={}", faceId);
|
||||
return StageResult.skipped("faceSamples为空");
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 构建样本ID到实体的映射
|
||||
Map<Long, FaceSampleEntity> sampleMap = faceSamples.stream()
|
||||
.collect(Collectors.toMap(FaceSampleEntity::getId, sample -> sample, (a, b) -> a));
|
||||
|
||||
// 2. 按设备ID分组
|
||||
Map<Long, List<FaceSampleEntity>> deviceSamplesMap = new LinkedHashMap<>();
|
||||
Set<Long> passthroughSampleIds = new LinkedHashSet<>();
|
||||
|
||||
for (Long sampleId : sampleListIds) {
|
||||
FaceSampleEntity sample = sampleMap.get(sampleId);
|
||||
if (sample == null || sample.getDeviceId() == null) {
|
||||
passthroughSampleIds.add(sampleId); // 无设备ID的样本直接保留
|
||||
continue;
|
||||
}
|
||||
deviceSamplesMap
|
||||
.computeIfAbsent(sample.getDeviceId(), key -> new ArrayList<>())
|
||||
.add(sample);
|
||||
}
|
||||
|
||||
// 3. 对每个设备应用照片数量限制
|
||||
Map<Long, Integer> limitCache = new HashMap<>();
|
||||
Set<Long> retainedSampleIds = new LinkedHashSet<>(passthroughSampleIds);
|
||||
|
||||
for (Map.Entry<Long, List<FaceSampleEntity>> entry : deviceSamplesMap.entrySet()) {
|
||||
Long deviceId = entry.getKey();
|
||||
List<FaceSampleEntity> deviceSamples = entry.getValue();
|
||||
|
||||
// 读取设备配置
|
||||
Integer limitPhoto = limitCache.computeIfAbsent(deviceId, id -> {
|
||||
DeviceConfigManager deviceConfig = deviceRepository.getDeviceConfigManager(id);
|
||||
return deviceConfig != null ? deviceConfig.getInteger("limit_photo") : null;
|
||||
});
|
||||
|
||||
List<Long> retainedForDevice = applyLimitForDevice(deviceId, deviceSamples, limitPhoto);
|
||||
retainedSampleIds.addAll(retainedForDevice);
|
||||
}
|
||||
|
||||
// 4. 按原始顺序保留筛选后的样本ID
|
||||
List<Long> resultIds = sampleListIds.stream()
|
||||
.filter(retainedSampleIds::contains)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 5. 更新context
|
||||
context.setSampleListIds(resultIds);
|
||||
|
||||
log.info("设备照片数量限制筛选完成: faceId={}, 原始样本数={}, 筛选后数={}",
|
||||
faceId, sampleListIds.size(), resultIds.size());
|
||||
|
||||
return StageResult.success(String.format("设备限制筛选: %d → %d",
|
||||
sampleListIds.size(), resultIds.size()));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("设备照片数量限制筛选失败,faceId={}", faceId, e);
|
||||
// 筛选失败不影响主流程,返回降级
|
||||
return StageResult.degraded("设备照片数量限制筛选失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对单个设备的样本应用照片数量限制
|
||||
*/
|
||||
private List<Long> applyLimitForDevice(Long deviceId, List<FaceSampleEntity> deviceSamples, Integer limitPhoto) {
|
||||
List<Long> deviceSampleIds = deviceSamples.stream()
|
||||
.map(FaceSampleEntity::getId)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 无限制或限制数量<=0,保留全部
|
||||
if (limitPhoto == null || limitPhoto <= 0) {
|
||||
log.debug("设备照片限制: 设备ID={}, 无限制, 保留{}张照片", deviceId, deviceSampleIds.size());
|
||||
return deviceSampleIds;
|
||||
}
|
||||
|
||||
int sampleCount = deviceSamples.size();
|
||||
|
||||
// 样本数 > limit_photo + 2: 按时间排序,去掉首尾
|
||||
if (sampleCount > (limitPhoto + 2)) {
|
||||
List<Long> retained = processDeviceSamples(deviceSamples, limitPhoto, true);
|
||||
log.debug("设备照片限制: 设备ID={}, 限制={}张, 原始{}张, 去首尾后最终{}张",
|
||||
deviceId, limitPhoto, sampleCount, retained.size());
|
||||
return retained;
|
||||
}
|
||||
|
||||
// 样本数 > limit_photo + 1: 按时间排序,去掉尾部
|
||||
if (sampleCount > (limitPhoto + 1)) {
|
||||
List<Long> retained = processDeviceSamples(deviceSamples, limitPhoto, false);
|
||||
log.debug("设备照片限制: 设备ID={}, 限制={}张, 原始{}张, 去尾部后最终{}张",
|
||||
deviceId, limitPhoto, sampleCount, retained.size());
|
||||
return retained;
|
||||
}
|
||||
|
||||
// 样本数 > limit_photo: 保留前limit_photo张
|
||||
if (sampleCount > limitPhoto) {
|
||||
List<Long> retained = deviceSamples.stream()
|
||||
.limit(limitPhoto)
|
||||
.map(FaceSampleEntity::getId)
|
||||
.collect(Collectors.toList());
|
||||
log.debug("设备照片限制: 设备ID={}, 限制={}张, 原始{}张, 取前{}张",
|
||||
deviceId, limitPhoto, sampleCount, retained.size());
|
||||
return retained;
|
||||
}
|
||||
|
||||
// 样本数 <= limit_photo: 保留全部
|
||||
log.debug("设备照片限制: 设备ID={}, 限制={}张, 原始{}张, 无需筛选, 保留全部",
|
||||
deviceId, limitPhoto, sampleCount);
|
||||
return deviceSampleIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理设备样本,根据参数决定是否去掉首尾
|
||||
*
|
||||
* @param deviceSamples 设备样本列表
|
||||
* @param limitPhoto 限制数量
|
||||
* @param removeBoth 是否去掉首尾,true去掉首尾,false只去掉尾部
|
||||
* @return 处理后的样本ID列表
|
||||
*/
|
||||
private List<Long> processDeviceSamples(List<FaceSampleEntity> deviceSamples, int limitPhoto, boolean removeBoth) {
|
||||
// 创建原始排序的索引映射,用于后续恢复排序
|
||||
Map<Long, Integer> originalIndexMap = new HashMap<>();
|
||||
for (int i = 0; i < deviceSamples.size(); i++) {
|
||||
originalIndexMap.put(deviceSamples.get(i).getId(), i);
|
||||
}
|
||||
|
||||
// 按创建时间排序
|
||||
List<FaceSampleEntity> sortedByCreateTime = deviceSamples.stream()
|
||||
.sorted(Comparator.comparing(FaceSampleEntity::getCreateAt))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 根据参数决定去掉首尾还是只去掉尾部
|
||||
List<FaceSampleEntity> filteredSamples;
|
||||
if (removeBoth && sortedByCreateTime.size() > 2) {
|
||||
// 去掉首尾
|
||||
filteredSamples = sortedByCreateTime.subList(1, sortedByCreateTime.size() - 1);
|
||||
} else if (!removeBoth && sortedByCreateTime.size() > 1) {
|
||||
// 只去掉尾部
|
||||
filteredSamples = sortedByCreateTime.subList(0, sortedByCreateTime.size() - 1);
|
||||
} else {
|
||||
filteredSamples = sortedByCreateTime;
|
||||
}
|
||||
|
||||
// 取前limitPhoto个
|
||||
List<FaceSampleEntity> limitedSamples = filteredSamples.stream()
|
||||
.limit(limitPhoto)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 按原始顺序排序
|
||||
List<Long> resultIds = limitedSamples.stream()
|
||||
.sorted(Comparator.comparing(sample -> originalIndexMap.get(sample.getId())))
|
||||
.map(FaceSampleEntity::getId)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return resultIds;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package com.ycwl.basic.face.pipeline.stages;
|
||||
|
||||
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||
import com.ycwl.basic.pipeline.annotation.StageConfig;
|
||||
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
|
||||
import com.ycwl.basic.pipeline.core.StageResult;
|
||||
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
|
||||
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 按时间范围筛选样本Stage
|
||||
* 负责根据景区配置的游览时间(tour_time)筛选匹配样本
|
||||
*
|
||||
* 职责:
|
||||
* 1. 从context.scenicConfig读取tour_time配置(分钟)
|
||||
* 2. 从context.faceSamples读取样本实体缓存
|
||||
* 3. 找到最新的样本,以其拍摄时间为基准
|
||||
* 4. 筛选出时间范围内(最新样本时间 ± tour_time分钟)的样本
|
||||
* 5. 更新context.sampleListIds
|
||||
*
|
||||
* 前置条件:
|
||||
* - context.faceSamples不为空 (由LoadMatchedSamplesStage加载)
|
||||
* - context.scenicConfig配置了tour_time
|
||||
*
|
||||
* 配置说明: tour_time=0或null表示不限制时间范围
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@StageConfig(
|
||||
stageId = "filter_by_time_range",
|
||||
optionalMode = StageOptionalMode.SUPPORT,
|
||||
description = "按游览时间范围筛选样本",
|
||||
defaultEnabled = true
|
||||
)
|
||||
public class FilterByTimeRangeStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "FilterByTimeRange";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
|
||||
// 检查faceSamples是否为空
|
||||
List<FaceSampleEntity> faceSamples = context.getFaceSamples();
|
||||
if (faceSamples == null || faceSamples.isEmpty()) {
|
||||
log.debug("faceSamples为空,跳过时间范围筛选,faceId={}", context.getFaceId());
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否配置了tour_time
|
||||
Integer tourMinutes = context.getScenicConfig() != null
|
||||
? context.getScenicConfig().getInteger("tour_time")
|
||||
: null;
|
||||
if (tourMinutes == null || tourMinutes <= 0) {
|
||||
log.debug("景区未配置tour_time或配置为0,跳过时间范围筛选,faceId={}", context.getFaceId());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||
List<FaceSampleEntity> faceSamples = context.getFaceSamples();
|
||||
List<Long> sampleListIds = context.getSampleListIds();
|
||||
Long faceId = context.getFaceId();
|
||||
|
||||
// 防御性检查:faceSamples为空
|
||||
if (faceSamples == null || faceSamples.isEmpty()) {
|
||||
log.debug("faceSamples为空,跳过时间范围筛选,faceId={}", faceId);
|
||||
return StageResult.skipped("faceSamples为空");
|
||||
}
|
||||
|
||||
// 防御性检查:tour_time配置
|
||||
Integer tourMinutes = context.getScenicConfig() != null
|
||||
? context.getScenicConfig().getInteger("tour_time")
|
||||
: null;
|
||||
if (tourMinutes == null || tourMinutes <= 0) {
|
||||
log.debug("景区未配置tour_time或配置为0,跳过时间范围筛选,faceId={}", faceId);
|
||||
return StageResult.skipped("未配置tour_time");
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 构建样本ID到实体的映射
|
||||
Map<Long, FaceSampleEntity> sampleMap = faceSamples.stream()
|
||||
.collect(Collectors.toMap(FaceSampleEntity::getId, sample -> sample, (a, b) -> a));
|
||||
|
||||
// 2. 找到最新的样本(拍摄时间最晚)
|
||||
FaceSampleEntity topMatchSample = faceSamples.stream()
|
||||
.filter(sample -> sample.getCreateAt() != null)
|
||||
.max(Comparator.comparing(FaceSampleEntity::getCreateAt))
|
||||
.orElse(null);
|
||||
|
||||
if (topMatchSample == null || topMatchSample.getCreateAt() == null) {
|
||||
log.warn("未找到有效的样本拍摄时间,保留所有样本,faceId={}", faceId);
|
||||
return StageResult.success("样本无拍摄时间,保留所有");
|
||||
}
|
||||
|
||||
Date referenceTime = topMatchSample.getCreateAt();
|
||||
long referenceMillis = referenceTime.getTime();
|
||||
long tourMillis = tourMinutes * 60 * 1000L;
|
||||
|
||||
// 3. 筛选时间范围内的样本
|
||||
List<Long> filteredIds = sampleListIds.stream()
|
||||
.filter(sampleId -> {
|
||||
FaceSampleEntity sample = sampleMap.get(sampleId);
|
||||
if (sample == null || sample.getCreateAt() == null) {
|
||||
return false; // 无时间信息的样本被过滤
|
||||
}
|
||||
long timeDiff = Math.abs(sample.getCreateAt().getTime() - referenceMillis);
|
||||
return timeDiff <= tourMillis;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 4. 更新context
|
||||
context.setSampleListIds(filteredIds);
|
||||
|
||||
log.info("时间范围筛选完成: faceId={}, tour_time={}分钟, 原始样本数={}, 筛选后数={}",
|
||||
faceId, tourMinutes, sampleListIds.size(), filteredIds.size());
|
||||
|
||||
return StageResult.success(String.format("时间筛选: %d → %d",
|
||||
sampleListIds.size(), filteredIds.size()));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("时间范围筛选失败,faceId={}", faceId, e);
|
||||
// 筛选失败不影响主流程,返回降级
|
||||
return StageResult.degraded("时间范围筛选失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.ycwl.basic.face.pipeline.stages;
|
||||
|
||||
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||
import com.ycwl.basic.pipeline.annotation.StageConfig;
|
||||
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
|
||||
import com.ycwl.basic.pipeline.core.StageResult;
|
||||
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
|
||||
import com.ycwl.basic.face.pipeline.helper.PuzzleGenerationOrchestrator;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 生成拼图模板Stage
|
||||
* 负责触发景区拼图模板的异步生成任务
|
||||
*
|
||||
* 职责:
|
||||
* 1. 从context读取必要参数(scenicId, faceId, memberId, faceUrl)
|
||||
* 2. 调用puzzleOrchestrator.generateAllTemplatesAsync()触发异步生成
|
||||
* 3. 立即返回,不等待生成完成
|
||||
*
|
||||
* 业务说明:
|
||||
* - 拼图生成是异步的,不影响主流程
|
||||
* - 具体的拼图生成逻辑由PuzzleGenerationOrchestrator负责
|
||||
* - Stage只负责触发任务,符合"薄Stage,厚Service"原则
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@StageConfig(
|
||||
stageId = "generate_puzzle",
|
||||
optionalMode = StageOptionalMode.SUPPORT,
|
||||
description = "异步生成拼图模板",
|
||||
defaultEnabled = true
|
||||
)
|
||||
public class GeneratePuzzleStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||
|
||||
@Autowired
|
||||
private PuzzleGenerationOrchestrator puzzleOrchestrator;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "GeneratePuzzle";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||
Long scenicId = context.getFace().getScenicId();
|
||||
Long faceId = context.getFaceId();
|
||||
Long memberId = context.getFace().getMemberId();
|
||||
String faceUrl = context.getFace().getFaceUrl();
|
||||
|
||||
try {
|
||||
// 触发异步生成拼图模板
|
||||
puzzleOrchestrator.generateAllTemplatesAsync(scenicId, faceId, memberId, faceUrl);
|
||||
|
||||
log.debug("拼图模板异步生成任务已提交: scenicId={}, faceId={}", scenicId, faceId);
|
||||
|
||||
return StageResult.success("拼图模板已提交异步生成");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("提交拼图生成任务失败: scenicId={}, faceId={}", scenicId, faceId, e);
|
||||
// 拼图生成失败不影响主流程,返回降级
|
||||
return StageResult.degraded("提交拼图生成任务失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.ycwl.basic.face.pipeline.stages;
|
||||
|
||||
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||
import com.ycwl.basic.pipeline.annotation.StageConfig;
|
||||
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
|
||||
import com.ycwl.basic.pipeline.core.StageResult;
|
||||
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
|
||||
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
||||
import com.ycwl.basic.service.pc.processor.VideoRecreationHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 处理视频重切Stage
|
||||
* 负责触发视频重新切片处理
|
||||
*
|
||||
* 职责:
|
||||
* 1. 从context读取必要参数(scenicId, memberSourceList, faceId, memberId, sampleListIds, isNew)
|
||||
* 2. 调用videoRecreationHandler.handleVideoRecreation()触发视频重切
|
||||
*
|
||||
* 前置条件: context.memberSourceList不为空
|
||||
* 业务说明: 视频重切用于根据人脸识别结果重新生成个性化视频片段
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@StageConfig(
|
||||
stageId = "handle_video_recreation",
|
||||
optionalMode = StageOptionalMode.SUPPORT,
|
||||
description = "处理视频重切逻辑",
|
||||
defaultEnabled = true
|
||||
)
|
||||
public class HandleVideoRecreationStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||
|
||||
@Autowired
|
||||
private VideoRecreationHandler videoRecreationHandler;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "HandleVideoRecreation";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
|
||||
// 只有当memberSourceList不为空时才执行
|
||||
List<MemberSourceEntity> memberSourceList = context.getMemberSourceList();
|
||||
if (memberSourceList == null || memberSourceList.isEmpty()) {
|
||||
log.debug("memberSourceList为空,跳过视频重切,faceId={}", context.getFaceId());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||
Long scenicId = context.getFace().getScenicId();
|
||||
List<MemberSourceEntity> memberSourceEntityList = context.getMemberSourceList();
|
||||
Long faceId = context.getFaceId();
|
||||
Long memberId = context.getFace().getMemberId();
|
||||
List<Long> sampleListIds = context.getSampleListIds();
|
||||
boolean isNew = context.isNew();
|
||||
|
||||
// 防御性检查:memberSourceList为空
|
||||
if (memberSourceEntityList == null || memberSourceEntityList.isEmpty()) {
|
||||
log.debug("memberSourceList为空,跳过视频重切,faceId={}", faceId);
|
||||
return StageResult.skipped("memberSourceList为空");
|
||||
}
|
||||
|
||||
try {
|
||||
// 处理视频重切
|
||||
videoRecreationHandler.handleVideoRecreation(
|
||||
scenicId,
|
||||
memberSourceEntityList,
|
||||
faceId,
|
||||
memberId,
|
||||
sampleListIds,
|
||||
isNew);
|
||||
|
||||
log.info("视频重切处理完成: faceId={}, scenicId={}, 源文件数={}",
|
||||
faceId, scenicId, memberSourceEntityList.size());
|
||||
|
||||
return StageResult.success("视频重切处理完成");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理视频重切失败,faceId={}", faceId, e);
|
||||
// 视频重切失败不影响主流程,返回降级
|
||||
return StageResult.degraded("视频重切处理失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.ycwl.basic.face.pipeline.stages;
|
||||
|
||||
import com.ycwl.basic.exception.BaseException;
|
||||
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||
import com.ycwl.basic.pipeline.annotation.StageConfig;
|
||||
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
|
||||
import com.ycwl.basic.pipeline.core.StageResult;
|
||||
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
|
||||
import com.ycwl.basic.mapper.FaceSampleMapper;
|
||||
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 加载人脸样本Stage
|
||||
* 负责加载用户选择的人脸样本数据
|
||||
*
|
||||
* 职责:
|
||||
* 1. 从context.faceSampleIds读取用户选择的样本ID列表
|
||||
* 2. 调用faceSampleMapper.listByIds()加载样本实体
|
||||
* 3. 更新context.faceSamples
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@StageConfig(
|
||||
stageId = "load_face_samples",
|
||||
optionalMode = StageOptionalMode.FORCE_ON,
|
||||
description = "加载用户选择的人脸样本"
|
||||
)
|
||||
public class LoadFaceSamplesStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||
|
||||
@Autowired
|
||||
private FaceSampleMapper faceSampleMapper;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "LoadFaceSamples";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||
List<Long> faceSampleIds = context.getFaceSampleIds();
|
||||
Long faceId = context.getFaceId();
|
||||
|
||||
if (faceSampleIds == null || faceSampleIds.isEmpty()) {
|
||||
log.warn("faceSampleIds为空,faceId={}", faceId);
|
||||
return StageResult.failed("faceSampleIds不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
List<FaceSampleEntity> faceSamples = faceSampleMapper.listByIds(faceSampleIds);
|
||||
|
||||
if (faceSamples.isEmpty()) {
|
||||
log.warn("未找到指定的人脸样本,faceSampleIds: {}, faceId={}", faceSampleIds, faceId);
|
||||
throw new BaseException("未找到指定的人脸样本");
|
||||
}
|
||||
|
||||
context.setFaceSamples(faceSamples);
|
||||
|
||||
log.info("加载人脸样本成功: faceId={}, sampleCount={}", faceId, faceSamples.size());
|
||||
return StageResult.success(String.format("加载了%d个人脸样本", faceSamples.size()));
|
||||
|
||||
} catch (BaseException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("加载人脸样本失败,faceId={}, faceSampleIds={}", faceId, faceSampleIds, e);
|
||||
return StageResult.failed("加载人脸样本失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.ycwl.basic.face.pipeline.stages;
|
||||
|
||||
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||
import com.ycwl.basic.pipeline.annotation.StageConfig;
|
||||
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
|
||||
import com.ycwl.basic.pipeline.core.StageResult;
|
||||
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
|
||||
import com.ycwl.basic.mapper.FaceSampleMapper;
|
||||
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 加载匹配样本实体Stage
|
||||
* 负责将sampleListIds对应的样本实体加载到context.faceSamples,供后续Stage使用
|
||||
*
|
||||
* 职责:
|
||||
* 1. 从context.sampleListIds读取匹配到的样本ID列表
|
||||
* 2. 调用faceSampleMapper.listByIds()批量加载样本实体
|
||||
* 3. 更新context.faceSamples作为样本实体缓存
|
||||
*
|
||||
* 设计目的:
|
||||
* - 避免后续多个Stage重复调用faceSampleMapper.listByIds()
|
||||
* - 统一加载时机,提高性能
|
||||
* - 为后续筛选Stage提供样本实体数据源
|
||||
*
|
||||
* 前置条件: context.sampleListIds不为空
|
||||
*
|
||||
* 应用场景: 自定义匹配场景,在CustomFaceSearchStage之后
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@StageConfig(
|
||||
stageId = "load_matched_samples",
|
||||
optionalMode = StageOptionalMode.UNSUPPORT,
|
||||
description = "加载匹配样本实体到缓存"
|
||||
)
|
||||
public class LoadMatchedSamplesStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||
|
||||
@Autowired
|
||||
private FaceSampleMapper faceSampleMapper;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "LoadMatchedSamples";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
|
||||
// 检查sampleListIds是否为空
|
||||
List<Long> sampleListIds = context.getSampleListIds();
|
||||
if (sampleListIds == null || sampleListIds.isEmpty()) {
|
||||
log.debug("sampleListIds为空,跳过加载匹配样本,faceId={}", context.getFaceId());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||
List<Long> sampleListIds = context.getSampleListIds();
|
||||
Long faceId = context.getFaceId();
|
||||
|
||||
// 防御性检查:如果sampleListIds为空,直接跳过
|
||||
if (sampleListIds == null || sampleListIds.isEmpty()) {
|
||||
log.debug("sampleListIds为空,跳过加载匹配样本,faceId={}", faceId);
|
||||
return StageResult.skipped("sampleListIds为空");
|
||||
}
|
||||
|
||||
try {
|
||||
// 批量加载样本实体
|
||||
List<FaceSampleEntity> faceSamples = faceSampleMapper.listByIds(sampleListIds);
|
||||
|
||||
if (faceSamples == null || faceSamples.isEmpty()) {
|
||||
log.warn("未找到任何匹配样本实体,faceId={}, sampleListIds={}", faceId, sampleListIds);
|
||||
return StageResult.skipped("未找到匹配样本实体");
|
||||
}
|
||||
|
||||
// 存入context缓存,供后续Stage使用
|
||||
context.setFaceSamples(faceSamples);
|
||||
|
||||
log.info("加载匹配样本实体完成: faceId={}, 样本数={}", faceId, faceSamples.size());
|
||||
|
||||
return StageResult.success(String.format("已加载%d个样本实体", faceSamples.size()));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("加载匹配样本实体失败,faceId={}", faceId, e);
|
||||
// 加载失败影响后续流程,返回失败
|
||||
return StageResult.failed("加载匹配样本实体失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.ycwl.basic.face.pipeline.stages;
|
||||
|
||||
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||
import com.ycwl.basic.pipeline.annotation.StageConfig;
|
||||
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
|
||||
import com.ycwl.basic.pipeline.core.StageResult;
|
||||
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
|
||||
import com.ycwl.basic.mapper.SourceMapper;
|
||||
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
||||
import com.ycwl.basic.repository.MemberRelationRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 持久化关联关系Stage
|
||||
* 负责过滤并保存源文件关联关系到数据库
|
||||
*
|
||||
* 职责:
|
||||
* 1. 从context.memberSourceList读取关联关系
|
||||
* 2. 过滤已存在的关联关系和无效的source引用
|
||||
* 3. 保存到数据库
|
||||
* 4. 清除缓存
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@StageConfig(
|
||||
stageId = "persist_relations",
|
||||
optionalMode = StageOptionalMode.FORCE_ON,
|
||||
description = "持久化源文件关联关系"
|
||||
)
|
||||
public class PersistRelationsStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||
|
||||
@Autowired
|
||||
private SourceMapper sourceMapper;
|
||||
|
||||
@Autowired
|
||||
private MemberRelationRepository memberRelationRepository;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "PersistRelations";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
|
||||
// 只有当memberSourceList不为空时才执行
|
||||
List<MemberSourceEntity> memberSourceList = context.getMemberSourceList();
|
||||
if (memberSourceList == null || memberSourceList.isEmpty()) {
|
||||
log.debug("memberSourceList为空,跳过持久化,faceId={}", context.getFaceId());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||
List<MemberSourceEntity> memberSourceEntityList = context.getMemberSourceList();
|
||||
Long faceId = context.getFaceId();
|
||||
|
||||
// 防御性检查:memberSourceList为空
|
||||
if (memberSourceEntityList == null || memberSourceEntityList.isEmpty()) {
|
||||
log.debug("memberSourceList为空,跳过持久化,faceId={}", faceId);
|
||||
return StageResult.skipped("memberSourceList为空");
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 过滤已存在的关联关系
|
||||
List<MemberSourceEntity> existingFiltered = sourceMapper.filterExistingRelations(memberSourceEntityList);
|
||||
|
||||
// 2. 过滤无效的source引用
|
||||
List<MemberSourceEntity> validFiltered = sourceMapper.filterValidSourceRelations(existingFiltered);
|
||||
|
||||
if (!validFiltered.isEmpty()) {
|
||||
// 3. 保存到数据库
|
||||
sourceMapper.addRelations(validFiltered);
|
||||
|
||||
log.debug("创建关联关系: faceId={}, 原始数量={}, 过滤后数量={}",
|
||||
faceId, memberSourceEntityList.size(), validFiltered.size());
|
||||
} else {
|
||||
log.warn("没有有效的关联关系可创建: faceId={}, 原始数量={}",
|
||||
faceId, memberSourceEntityList.size());
|
||||
return StageResult.skipped("没有有效的关联关系可创建");
|
||||
}
|
||||
|
||||
// 4. 清除缓存
|
||||
memberRelationRepository.clearSCacheByFace(faceId);
|
||||
|
||||
return StageResult.success(String.format("持久化了%d条关联关系", validFiltered.size()));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("持久化关联关系失败,faceId={}", faceId, e);
|
||||
return StageResult.failed("保存关联关系失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.ycwl.basic.face.pipeline.stages;
|
||||
|
||||
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||
import com.ycwl.basic.pipeline.annotation.StageConfig;
|
||||
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
|
||||
import com.ycwl.basic.pipeline.core.StageResult;
|
||||
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
|
||||
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
|
||||
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
|
||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||
import com.ycwl.basic.repository.FaceRepository;
|
||||
import com.ycwl.basic.repository.ScenicRepository;
|
||||
import com.ycwl.basic.service.pc.ScenicService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 准备上下文Stage
|
||||
* 负责加载人脸实体、景区配置、识别适配器等必要数据
|
||||
*
|
||||
* 职责:
|
||||
* 1. 加载FaceEntity(如不存在则失败)
|
||||
* 2. 检查是否人工选择(是则跳过,除非isNew=true)
|
||||
* 3. 加载ScenicConfigManager和IFaceBodyAdapter
|
||||
* 4. 更新Context
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@StageConfig(
|
||||
stageId = "prepare_context",
|
||||
optionalMode = StageOptionalMode.FORCE_ON,
|
||||
description = "准备人脸匹配上下文数据"
|
||||
)
|
||||
public class PrepareContextStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||
|
||||
@Autowired
|
||||
private FaceRepository faceRepository;
|
||||
|
||||
@Autowired
|
||||
private ScenicRepository scenicRepository;
|
||||
|
||||
@Autowired
|
||||
private ScenicService scenicService;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "PrepareContext";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||
Long faceId = context.getFaceId();
|
||||
boolean isNew = context.isNew();
|
||||
|
||||
// 1. 加载人脸实体
|
||||
FaceEntity face = faceRepository.getFace(faceId);
|
||||
if (face == null) {
|
||||
log.warn("人脸不存在,faceId: {}", faceId);
|
||||
return StageResult.failed("人脸不存在,faceId: " + faceId);
|
||||
}
|
||||
|
||||
context.setFace(face);
|
||||
log.debug("加载人脸实体成功: faceId={}, memberId={}, scenicId={}",
|
||||
faceId, face.getMemberId(), face.getScenicId());
|
||||
|
||||
// 2. 检查是否人工选择
|
||||
// 人工选择的无需重新匹配(新用户除外)
|
||||
if (!isNew && Integer.valueOf(1).equals(face.getIsManual())) {
|
||||
log.info("人工选择的人脸,无需匹配,faceId: {}", faceId);
|
||||
return StageResult.skipped("人工选择的人脸,无需重新匹配");
|
||||
}
|
||||
|
||||
// 3. 加载景区配置
|
||||
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
|
||||
context.setScenicConfig(scenicConfig);
|
||||
log.debug("加载景区配置成功: scenicId={}", face.getScenicId());
|
||||
|
||||
// 4. 加载人脸识别适配器
|
||||
IFaceBodyAdapter faceBodyAdapter = scenicService.getScenicFaceBodyAdapter(face.getScenicId());
|
||||
if (faceBodyAdapter == null) {
|
||||
log.error("无法获取人脸识别适配器,scenicId: {}", face.getScenicId());
|
||||
return StageResult.failed("人脸识别服务不可用,请稍后再试");
|
||||
}
|
||||
|
||||
context.setFaceBodyAdapter(faceBodyAdapter);
|
||||
log.debug("加载人脸识别适配器成功: scenicId={}", face.getScenicId());
|
||||
|
||||
return StageResult.success("上下文准备完成");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.ycwl.basic.face.pipeline.stages;
|
||||
|
||||
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||
import com.ycwl.basic.pipeline.annotation.StageConfig;
|
||||
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
|
||||
import com.ycwl.basic.pipeline.core.StageResult;
|
||||
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
|
||||
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
||||
import com.ycwl.basic.service.pc.processor.BuyStatusProcessor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 处理购买状态Stage
|
||||
* 负责更新源文件的购买状态标记
|
||||
*
|
||||
* 职责:
|
||||
* 1. 从context.memberSourceList读取源文件关联列表
|
||||
* 2. 从context.freeSourceIds读取免费源文件ID列表
|
||||
* 3. 调用buyStatusProcessor.processBuyStatus()更新购买状态
|
||||
*
|
||||
* 前置条件: context.memberSourceList不为空
|
||||
* 业务说明: 购买状态影响前端显示和用户下载权限
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@StageConfig(
|
||||
stageId = "process_buy_status",
|
||||
optionalMode = StageOptionalMode.SUPPORT,
|
||||
description = "处理源文件购买状态",
|
||||
defaultEnabled = true
|
||||
)
|
||||
public class ProcessBuyStatusStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||
|
||||
@Autowired
|
||||
private BuyStatusProcessor buyStatusProcessor;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "ProcessBuyStatus";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
|
||||
// 只有当memberSourceList不为空时才执行
|
||||
List<MemberSourceEntity> memberSourceList = context.getMemberSourceList();
|
||||
if (memberSourceList == null || memberSourceList.isEmpty()) {
|
||||
log.debug("memberSourceList为空,跳过购买状态处理,faceId={}", context.getFaceId());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||
List<MemberSourceEntity> memberSourceEntityList = context.getMemberSourceList();
|
||||
List<Long> freeSourceIds = context.getFreeSourceIds();
|
||||
Long memberId = context.getFace().getMemberId();
|
||||
Long scenicId = context.getFace().getScenicId();
|
||||
Long faceId = context.getFaceId();
|
||||
|
||||
// 防御性检查:memberSourceList为空
|
||||
if (memberSourceEntityList == null || memberSourceEntityList.isEmpty()) {
|
||||
log.debug("memberSourceList为空,跳过购买状态处理,faceId={}", faceId);
|
||||
return StageResult.skipped("memberSourceList为空");
|
||||
}
|
||||
|
||||
try {
|
||||
// 处理购买状态
|
||||
buyStatusProcessor.processBuyStatus(
|
||||
memberSourceEntityList,
|
||||
freeSourceIds,
|
||||
memberId,
|
||||
scenicId,
|
||||
faceId);
|
||||
|
||||
log.info("购买状态处理完成: faceId={}, 源文件数={}, 免费数={}",
|
||||
faceId, memberSourceEntityList.size(),
|
||||
freeSourceIds != null ? freeSourceIds.size() : 0);
|
||||
|
||||
return StageResult.success("购买状态处理完成");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理购买状态失败,faceId={}", faceId, e);
|
||||
// 购买状态处理失败不影响主流程,返回降级
|
||||
return StageResult.degraded("购买状态处理失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.ycwl.basic.face.pipeline.stages;
|
||||
|
||||
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||
import com.ycwl.basic.pipeline.annotation.StageConfig;
|
||||
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
|
||||
import com.ycwl.basic.pipeline.core.StageResult;
|
||||
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
|
||||
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
||||
import com.ycwl.basic.service.pc.processor.SourceRelationProcessor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 处理免费源文件Stage
|
||||
* 负责根据业务规则确定哪些源文件可以免费访问
|
||||
*
|
||||
* 职责:
|
||||
* 1. 从context.memberSourceList读取源文件关联列表
|
||||
* 2. 调用sourceRelationProcessor.processFreeSourceLogic()确定免费源文件
|
||||
* 3. 更新context.freeSourceIds
|
||||
*
|
||||
* 前置条件: context.memberSourceList不为空
|
||||
* 后置条件: context.freeSourceIds已设置
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@StageConfig(
|
||||
stageId = "process_free_source",
|
||||
optionalMode = StageOptionalMode.SUPPORT,
|
||||
description = "处理免费源文件逻辑",
|
||||
defaultEnabled = true
|
||||
)
|
||||
public class ProcessFreeSourceStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||
|
||||
@Autowired
|
||||
private SourceRelationProcessor sourceRelationProcessor;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "ProcessFreeSource";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
|
||||
// 只有当memberSourceList不为空时才执行
|
||||
List<MemberSourceEntity> memberSourceList = context.getMemberSourceList();
|
||||
if (memberSourceList == null || memberSourceList.isEmpty()) {
|
||||
log.debug("memberSourceList为空,跳过免费逻辑,faceId={}", context.getFaceId());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||
List<MemberSourceEntity> memberSourceEntityList = context.getMemberSourceList();
|
||||
Long scenicId = context.getFace().getScenicId();
|
||||
boolean isNew = context.isNew();
|
||||
Long faceId = context.getFaceId();
|
||||
|
||||
// 防御性检查:memberSourceList为空
|
||||
if (memberSourceEntityList == null || memberSourceEntityList.isEmpty()) {
|
||||
log.debug("memberSourceList为空,跳过免费逻辑,faceId={}", faceId);
|
||||
return StageResult.skipped("memberSourceList为空");
|
||||
}
|
||||
|
||||
try {
|
||||
// 处理免费逻辑
|
||||
List<Long> freeSourceIds = sourceRelationProcessor.processFreeSourceLogic(
|
||||
memberSourceEntityList, scenicId, isNew);
|
||||
|
||||
context.setFreeSourceIds(freeSourceIds);
|
||||
|
||||
log.info("免费源文件处理完成: faceId={}, 总源文件数={}, 免费数={}",
|
||||
faceId, memberSourceEntityList.size(), freeSourceIds != null ? freeSourceIds.size() : 0);
|
||||
|
||||
return StageResult.success(String.format("确定了%d个免费源文件",
|
||||
freeSourceIds != null ? freeSourceIds.size() : 0));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理免费源文件失败,faceId={}", faceId, e);
|
||||
// 免费逻辑失败不影响主流程,返回降级
|
||||
return StageResult.degraded("免费源文件处理失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.ycwl.basic.face.pipeline.stages;
|
||||
|
||||
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||
import com.ycwl.basic.face.pipeline.enums.FaceMatchingScene;
|
||||
import com.ycwl.basic.pipeline.annotation.StageConfig;
|
||||
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
|
||||
import com.ycwl.basic.pipeline.core.StageResult;
|
||||
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
|
||||
import com.ycwl.basic.service.pc.helper.FaceMetricsRecorder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 记录自定义匹配次数Stage
|
||||
* 负责记录自定义人脸匹配调用次数,用于监控
|
||||
*
|
||||
* 职责:
|
||||
* 1. 仅在CUSTOM_MATCHING场景执行
|
||||
* 2. 调用metricsRecorder.recordCustomMatchCount()记录次数
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@StageConfig(
|
||||
stageId = "record_custom_match_metrics",
|
||||
optionalMode = StageOptionalMode.SUPPORT,
|
||||
description = "记录自定义匹配指标",
|
||||
defaultEnabled = true
|
||||
)
|
||||
public class RecordCustomMatchMetricsStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||
|
||||
@Autowired
|
||||
private FaceMetricsRecorder metricsRecorder;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "RecordCustomMatchMetrics";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
|
||||
// 只有自定义匹配场景才执行
|
||||
if (context.getScene() != FaceMatchingScene.CUSTOM_MATCHING) {
|
||||
log.debug("非自定义匹配场景,跳过记录,faceId={}", context.getFaceId());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||
Long faceId = context.getFaceId();
|
||||
|
||||
// 防御性检查:只有自定义匹配场景才执行
|
||||
if (context.getScene() != FaceMatchingScene.CUSTOM_MATCHING) {
|
||||
log.debug("非自定义匹配场景,跳过记录,faceId={}", faceId);
|
||||
return StageResult.skipped("非自定义匹配场景");
|
||||
}
|
||||
|
||||
try {
|
||||
metricsRecorder.recordCustomMatchCount(faceId);
|
||||
log.debug("记录自定义匹配次数: faceId={}", faceId);
|
||||
return StageResult.success("自定义匹配指标记录完成");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("记录自定义匹配指标失败,faceId={}", faceId, e);
|
||||
// 指标记录失败不影响主流程,返回降级
|
||||
return StageResult.degraded("指标记录失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.ycwl.basic.face.pipeline.stages;
|
||||
|
||||
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||
import com.ycwl.basic.pipeline.annotation.StageConfig;
|
||||
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
|
||||
import com.ycwl.basic.pipeline.core.StageResult;
|
||||
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
|
||||
import com.ycwl.basic.service.pc.helper.FaceMetricsRecorder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 记录识别次数Stage
|
||||
* 负责记录人脸识别调用次数,用于监控和防重复检查
|
||||
*
|
||||
* 职责:
|
||||
* 1. 调用metricsRecorder.recordRecognitionCount()记录识别次数
|
||||
* 2. 检查searchResult是否触发低阈值检测
|
||||
* 3. 如果是,调用metricsRecorder.recordLowThreshold()记录
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@StageConfig(
|
||||
stageId = "record_metrics",
|
||||
optionalMode = StageOptionalMode.SUPPORT,
|
||||
description = "记录人脸识别指标",
|
||||
defaultEnabled = true
|
||||
)
|
||||
public class RecordMetricsStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||
|
||||
@Autowired
|
||||
private FaceMetricsRecorder metricsRecorder;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "RecordMetrics";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||
Long faceId = context.getFaceId();
|
||||
|
||||
try {
|
||||
// 1. 记录识别次数
|
||||
metricsRecorder.recordRecognitionCount(faceId);
|
||||
log.debug("记录识别次数: faceId={}", faceId);
|
||||
|
||||
// 2. 检查是否触发低阈值检测
|
||||
if (context.getSearchResult() != null && context.getSearchResult().isLowThreshold()) {
|
||||
metricsRecorder.recordLowThreshold(faceId);
|
||||
log.debug("触发低阈值检测,记录faceId: {}", faceId);
|
||||
}
|
||||
|
||||
return StageResult.success("识别指标记录完成");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("记录识别指标失败,faceId={}", faceId, e);
|
||||
// 指标记录失败不影响主流程,返回降级
|
||||
return StageResult.degraded("指标记录失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.ycwl.basic.face.pipeline.stages;
|
||||
|
||||
import com.ycwl.basic.biz.TaskStatusBiz;
|
||||
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||
import com.ycwl.basic.pipeline.annotation.StageConfig;
|
||||
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
|
||||
import com.ycwl.basic.pipeline.core.StageResult;
|
||||
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 设置任务状态Stage
|
||||
* 负责为新用户设置任务状态为待处理
|
||||
*
|
||||
* 职责:
|
||||
* 1. 仅在isNew=true时执行
|
||||
* 2. 调用taskStatusBiz.setFaceCutStatus(faceId, 0)
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@StageConfig(
|
||||
stageId = "set_task_status",
|
||||
optionalMode = StageOptionalMode.FORCE_ON,
|
||||
description = "设置新用户任务状态"
|
||||
)
|
||||
public class SetTaskStatusStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||
|
||||
@Autowired
|
||||
private TaskStatusBiz taskStatusBiz;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "SetTaskStatus";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
|
||||
// 只有新用户才执行
|
||||
if (!context.isNew()) {
|
||||
log.debug("非新用户,跳过设置任务状态,faceId={}", context.getFaceId());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||
Long faceId = context.getFaceId();
|
||||
|
||||
// 防御性检查:只有新用户才执行
|
||||
if (!context.isNew()) {
|
||||
log.debug("非新用户,跳过设置任务状态,faceId={}", faceId);
|
||||
return StageResult.skipped("非新用户");
|
||||
}
|
||||
|
||||
try {
|
||||
taskStatusBiz.setFaceCutStatus(faceId, 0);
|
||||
log.debug("设置新用户任务状态: faceId={}, status=0", faceId);
|
||||
return StageResult.success("任务状态已设置");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("设置任务状态失败,faceId={}", faceId, e);
|
||||
// 任务状态设置失败不影响主流程,返回降级
|
||||
return StageResult.degraded("任务状态设置失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.ycwl.basic.face.pipeline.stages;
|
||||
|
||||
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||
import com.ycwl.basic.pipeline.annotation.StageConfig;
|
||||
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
|
||||
import com.ycwl.basic.pipeline.core.StageResult;
|
||||
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
|
||||
import com.ycwl.basic.mapper.FaceMapper;
|
||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
|
||||
import com.ycwl.basic.repository.FaceRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 更新人脸结果Stage
|
||||
* 负责将人脸识别结果保存到数据库
|
||||
*
|
||||
* 职责:
|
||||
* 1. 从context.searchResult读取识别结果
|
||||
* 2. 更新FaceEntity(score、matchResult、firstMatchRate、matchSampleIds)
|
||||
* 3. 清除缓存
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@StageConfig(
|
||||
stageId = "update_face_result",
|
||||
optionalMode = StageOptionalMode.FORCE_ON,
|
||||
description = "更新人脸识别结果到数据库"
|
||||
)
|
||||
public class UpdateFaceResultStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||
|
||||
@Autowired
|
||||
private FaceMapper faceMapper;
|
||||
|
||||
@Autowired
|
||||
private FaceRepository faceRepository;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "UpdateFaceResult";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||
SearchFaceRespVo searchResult = context.getSearchResult();
|
||||
if (searchResult == null) {
|
||||
log.warn("searchResult为空,跳过更新人脸结果,faceId={}", context.getFaceId());
|
||||
return StageResult.skipped("searchResult为空");
|
||||
}
|
||||
|
||||
try {
|
||||
FaceEntity originalFace = context.getFace();
|
||||
Long faceId = context.getFaceId();
|
||||
|
||||
FaceEntity faceEntity = new FaceEntity();
|
||||
faceEntity.setId(faceId);
|
||||
faceEntity.setScore(searchResult.getScore());
|
||||
faceEntity.setMatchResult(searchResult.getSearchResultJson());
|
||||
|
||||
if (searchResult.getFirstMatchRate() != null) {
|
||||
faceEntity.setFirstMatchRate(BigDecimal.valueOf(searchResult.getFirstMatchRate()));
|
||||
}
|
||||
|
||||
if (searchResult.getSampleListIds() != null) {
|
||||
faceEntity.setMatchSampleIds(searchResult.getSampleListIds().stream()
|
||||
.map(String::valueOf)
|
||||
.collect(Collectors.joining(",")));
|
||||
}
|
||||
|
||||
faceEntity.setCreateAt(new Date());
|
||||
faceEntity.setScenicId(originalFace.getScenicId());
|
||||
faceEntity.setMemberId(originalFace.getMemberId());
|
||||
faceEntity.setFaceUrl(originalFace.getFaceUrl());
|
||||
|
||||
faceMapper.update(faceEntity);
|
||||
faceRepository.clearFaceCache(faceId);
|
||||
|
||||
log.debug("人脸结果更新成功:faceId={}, score={}, sampleCount={}",
|
||||
faceId, searchResult.getScore(),
|
||||
searchResult.getSampleListIds() != null ? searchResult.getSampleListIds().size() : 0);
|
||||
|
||||
return StageResult.success("人脸结果更新成功");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("更新人脸结果失败,faceId={}", context.getFaceId(), e);
|
||||
return StageResult.failed("保存人脸识别结果失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user