You've already forked FrameTour-BE
Compare commits
829 Commits
52649e4e77
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ed12af8c9 | |||
| ecbdec4518 | |||
| bf6b866e67 | |||
| 93f9c1486f | |||
| e87e38be03 | |||
| 85d0fc0996 | |||
| d25d09cb66 | |||
| c40c6a0966 | |||
| 4fc0984994 | |||
| 918ff860c3 | |||
| 8b3bea8bed | |||
| be54bbaa82 | |||
| 68a674ba51 | |||
| 80f8a6b56b | |||
| 973bd73e9a | |||
| 819caab047 | |||
| 00bf4b5a8b | |||
| 6c305f4cd1 | |||
| 82e844a779 | |||
| c3fcfdd633 | |||
| a8156976be | |||
| ce48bd00c9 | |||
| c5df277e6c | |||
| 9a31e71e42 | |||
| e268d236f4 | |||
| 143426db1f | |||
| fcc4b06295 | |||
| f876dc59fa | |||
| 8e6d10ad95 | |||
| 42bf3d3d0a | |||
| 679f2d3a79 | |||
| 3084afc6a7 | |||
| 91626626f4 | |||
| b1cfef278d | |||
| c42474256e | |||
| 63180159d2 | |||
| e647ad75c6 | |||
| 4a07f5bba9 | |||
| 1f7e6d69f4 | |||
| 50aaf7cb1a | |||
| f2c739160a | |||
| 2efc66292e | |||
| 0eced869fa | |||
| aa2611d369 | |||
| 6a8f679540 | |||
| 4fac129c3a | |||
| 830dd17071 | |||
| 83c831887e | |||
| 5ab2882777 | |||
| a5a9ff09f2 | |||
| 83e47ed843 | |||
| e9a4c26a83 | |||
| 8c76a4fb03 | |||
| 8198b0c537 | |||
| 0235d1d121 | |||
| 8d5a10cce1 | |||
| eba727b446 | |||
| 27a18096b5 | |||
| d15d070cb4 | |||
| fb4568721a | |||
| 63d31d69a9 | |||
| 2fb6aa42cf | |||
| fed92c5445 | |||
| 6d774e4d76 | |||
| 57b71c309e | |||
| 93e28828ad | |||
| f8c6604a8a | |||
| 3bd658cc1f | |||
| 7b417aa4f1 | |||
| 6ca7dceb0e | |||
| 0b3dd19de5 | |||
| e56c2e6642 | |||
| 482789b523 | |||
| d902b480b8 | |||
| fc0d5fed9b | |||
| 31b9220a32 | |||
| c4b78f1b09 | |||
| c9cc90c842 | |||
| 02f1392355 | |||
| d02aca9bf1 | |||
| 05e269a305 | |||
| 74c146c104 | |||
| 42000df311 | |||
| 8b7f3d8eae | |||
| 6e345f2da4 | |||
| d7c2c5b830 | |||
| 07593694c8 | |||
| 3ff76a0bea | |||
| 5952390093 | |||
| e896f58d82 | |||
| 3291371dd7 | |||
| 917668da0c | |||
| d3884c8aa2 | |||
| a652124a93 | |||
| 54cdee333d | |||
| 286062a81a | |||
| e0856a1b9c | |||
| 123a081eab | |||
| 95e86fb996 | |||
| 6c3a413778 | |||
| da2286bc80 | |||
| 1df6a4bc23 | |||
| 981a4ba7bd | |||
| 017ced34fa | |||
| a9ae00d580 | |||
| 99f75b6805 | |||
| 295815f1fa | |||
| 010bac1091 | |||
| eb9b781fd3 | |||
| 8d3dae32f3 | |||
| 43775f550b | |||
| 24f72091b3 | |||
| cc62fb4c18 | |||
| d1962ed615 | |||
| e1023b6ea8 | |||
| aec5e57df7 | |||
| 52ce26e630 | |||
| 32297dc29c | |||
| 21d8c56e82 | |||
| f8374519c3 | |||
| 44f5008fd1 | |||
| 6e0ebcd1bd | |||
| 5caf9a0ebf | |||
| 06bc2c2020 | |||
| f1a2958251 | |||
| 81dc2f1b86 | |||
| 41e90bab9c | |||
| b4628bd3e8 | |||
| cfb4284b7c | |||
| 5a61432dc9 | |||
| 91160a1adb | |||
| 991a8b10e3 | |||
| ab1e8cf7ef | |||
| 2a3b4ca19f | |||
| 85599aa84a | |||
| fbd4cfa83c | |||
| 4596a61ba8 | |||
| d6780ccb7a | |||
| 58e8189b13 | |||
| 84cb5ad8f9 | |||
| 71bfa00c25 | |||
| 1916dd96a2 | |||
| c583d4b007 | |||
| 50ee14cf8f | |||
| 3f4d3cb7ac | |||
| f2ba5ed65b | |||
| 677893272a | |||
| 956ace77a8 | |||
| 08e2a4ebec | |||
| 3c8b3b0ace | |||
| 94b37d47ec | |||
| cd4422eb23 | |||
| 8dc0e993e1 | |||
| 2432cf496f | |||
| 95a5977ae2 | |||
| 7e157eaba9 | |||
| 00dd6a16a3 | |||
| 9e6b623b0e | |||
| 10b39ec4c1 | |||
| 3e938ad171 | |||
| 6c404e210e | |||
| 25681806c3 | |||
| 99d0b9c340 | |||
| 8e0990832b | |||
| 144c338972 | |||
| 2dcb736df5 | |||
| c8560e3aca | |||
| 171932c05c | |||
| 6cd47649fc | |||
| 00890c764e | |||
| a9c33352f7 | |||
| a9555d612a | |||
| c1f35e1f3a | |||
| a5903a9831 | |||
| b207b5805a | |||
| 5d7fe1638e | |||
| c0f07ee9f4 | |||
| 832f6a2339 | |||
| 7348994427 | |||
| 0665eef37d | |||
| adabe88648 | |||
| 3c838ec36e | |||
| 5bef712b1c | |||
| f08d590a3d | |||
| 844bc318ae | |||
| c9c4d9454a | |||
| 398a3750f8 | |||
| aceea9af18 | |||
| 54088f46d9 | |||
| 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 | |||
| 5d5643e7d7 | |||
| 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 | |||
| 8064c68b8b | |||
| 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 | |||
| bdeb41bead | |||
| feac2e8d93 | |||
| dc4091e058 | |||
| 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 | |||
| a5c815b6ed | |||
| 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 | |||
| 8745cde2fb | |||
| 1d5e4562f2 | |||
| cdd434317f | |||
| f91b98c68e | |||
| d34603062a | |||
| a9d64402f2 | |||
| ada7158a48 | |||
| 047feec045 | |||
| 966568156c | |||
| 57b087a4fb | |||
| 607c5bc057 | |||
| fc8818a595 | |||
| b1deabc7c1 | |||
| 1ac375e491 | |||
| 60af636639 | |||
| 792deb5c4d | |||
| 93a424058a | |||
| 98ae9f2930 | |||
| e2b760caab | |||
| 5a66856e72 | |||
| bc2b2fb10f | |||
| 4dac46bb46 | |||
| 3fbfb7df54 | |||
| 346c484cbc | |||
| e95e0a04ff | |||
| af79a5ffa6 | |||
| 5c2629237e | |||
| 798ff3b9b5 | |||
| 46fb255e66 | |||
| c03678bd93 | |||
| f451b835b9 | |||
| ce7e055ada | |||
| c7d5399931 | |||
| 5bb2bc1ac3 | |||
| 95d8b742ee | |||
| ff320ba3e8 | |||
| 98bbaccb3a | |||
| f2ac6aaea0 | |||
| 21f76ff9c5 | |||
| 7d40b8043d | |||
| b67fb87989 | |||
| 42e7b7da95 | |||
| 6bc94a65a6 | |||
| 7c2db2ad22 | |||
| f6bd7e48a3 | |||
| f0aeb27566 | |||
| 5871beb84e | |||
| 291b3d620f | |||
| 32feaa9692 | |||
| e694aac928 | |||
| 581efa327c | |||
| 3d79777b84 | |||
| 2710bfce2c | |||
| e85559ed72 | |||
| d1bc34b52b | |||
| 0c43e3f99a | |||
| 6b20e700f0 | |||
| ea9945b9e0 | |||
| 0204b3bc23 | |||
| 4c794cdda2 | |||
| a04e7b5183 | |||
| dc8d9448e6 | |||
| 2c0b7a094d | |||
| 3d49c47006 | |||
| b4b542046f | |||
| eb327723cd | |||
| e9035af542 | |||
| 85a179c5b4 | |||
| 9e0286e66e | |||
| 9fef17bae5 | |||
| 4787efd328 | |||
| 16e07ee9ef | |||
| 688459d2da | |||
| af5c59dc67 | |||
| 9c932b6ba8 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,6 +1,8 @@
|
||||
.idea/
|
||||
logs/
|
||||
target/
|
||||
|
||||
.*
|
||||
.claude
|
||||
.vscode
|
||||
.vscode
|
||||
*.jpg
|
||||
!.gitignore
|
||||
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.
|
||||
142
CLAUDE.md
142
CLAUDE.md
@@ -1,123 +1,27 @@
|
||||
# CLAUDE.md
|
||||
# Repository Guidelines
|
||||
|
||||
本文件为 Claude Code (claude.ai/code) 在此代码仓库中工作时提供指导。
|
||||
## 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. 遵循现有的错误处理和日志记录模式
|
||||
## 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.
|
||||
|
||||
70
pom.xml
70
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>
|
||||
@@ -181,6 +187,10 @@
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-annotations</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 引入commons-lang3 工具类 -->
|
||||
<dependency>
|
||||
@@ -262,6 +272,45 @@
|
||||
<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>
|
||||
|
||||
<!-- Caffeine Cache -->
|
||||
<dependency>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
<artifactId>caffeine</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>
|
||||
|
||||
<!-- ClickHouse JDBC Driver -->
|
||||
<dependency>
|
||||
<groupId>com.clickhouse</groupId>
|
||||
<artifactId>clickhouse-jdbc</artifactId>
|
||||
<version>0.8.5</version>
|
||||
<classifier>all</classifier>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
@@ -289,16 +338,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>
|
||||
|
||||
@@ -321,17 +360,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,13 +1,14 @@
|
||||
package com.ycwl.basic.biz;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import com.ycwl.basic.clickhouse.service.StatsQueryService;
|
||||
import com.ycwl.basic.mapper.BrokerMapper;
|
||||
import com.ycwl.basic.mapper.BrokerRecordMapper;
|
||||
import com.ycwl.basic.mapper.StatisticsMapper;
|
||||
import com.ycwl.basic.model.pc.broker.entity.BrokerRecord;
|
||||
import com.ycwl.basic.model.pc.broker.resp.BrokerRespVO;
|
||||
import com.ycwl.basic.model.pc.order.entity.OrderEntity;
|
||||
import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity;
|
||||
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
|
||||
import com.ycwl.basic.repository.OrderRepository;
|
||||
import com.ycwl.basic.repository.ScenicRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -34,7 +35,7 @@ public class BrokerBiz {
|
||||
@Autowired
|
||||
private ScenicRepository scenicRepository;
|
||||
@Autowired
|
||||
private StatisticsMapper statisticsMapper;
|
||||
private StatsQueryService statsQueryService;
|
||||
|
||||
public void processOrder(Long orderId) {
|
||||
log.info("开始处理订单分佣,订单ID:{}", orderId);
|
||||
@@ -43,16 +44,16 @@ public class BrokerBiz {
|
||||
log.info("订单不存在,订单ID:{}", orderId);
|
||||
return;
|
||||
}
|
||||
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(order.getScenicId());
|
||||
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(order.getScenicId());
|
||||
if (scenicConfig == null) {
|
||||
log.info("景区不存在,订单ID:{}", orderId);
|
||||
return;
|
||||
}
|
||||
int expireDay = 3;
|
||||
if (scenicConfig.getSampleStoreDay() != null) {
|
||||
expireDay = scenicConfig.getSampleStoreDay();
|
||||
if (scenicConfig.getInteger("sample_store_day") != null) {
|
||||
expireDay = scenicConfig.getInteger("sample_store_day");
|
||||
}
|
||||
List<Long> brokerIdList = statisticsMapper.getBrokerIdListForUser(order.getMemberId(), DateUtil.offsetDay(DateUtil.beginOfDay(order.getCreateAt()), -expireDay), order.getCreateAt());
|
||||
List<Long> brokerIdList = statsQueryService.getBrokerIdListForUser(order.getMemberId(), DateUtil.offsetDay(DateUtil.beginOfDay(order.getCreateAt()), -expireDay), order.getCreateAt());
|
||||
if (brokerIdList == null || brokerIdList.isEmpty()) {
|
||||
log.info("用户与推客无关,订单ID:{}", orderId);
|
||||
return;
|
||||
@@ -103,7 +104,7 @@ public class BrokerBiz {
|
||||
BigDecimal realRate = broker.getBrokerRate();
|
||||
BigDecimal brokerPrice = order.getPayPrice().multiply(realRate).divide(BigDecimal.valueOf(100), 2, RoundingMode.DOWN);
|
||||
// todo 需要计算实际提成比例
|
||||
BigDecimal firstRate = scenicConfig.getBrokerDirectRate();
|
||||
BigDecimal firstRate = scenicConfig.getBigDecimal("broker_direct_rate");
|
||||
if (firstRate == null) {
|
||||
firstRate = BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
package com.ycwl.basic.biz;
|
||||
|
||||
import com.ycwl.basic.mapper.CouponMapper;
|
||||
import com.ycwl.basic.mapper.CouponRecordMapper;
|
||||
import com.ycwl.basic.model.pc.coupon.entity.CouponEntity;
|
||||
import com.ycwl.basic.model.pc.couponRecord.entity.CouponRecordEntity;
|
||||
import com.ycwl.basic.model.pc.couponRecord.resp.CouponRecordQueryResp;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Component
|
||||
public class CouponBiz {
|
||||
@Autowired
|
||||
private CouponMapper couponMapper;
|
||||
@Autowired
|
||||
private CouponRecordMapper couponRecordMapper;
|
||||
|
||||
public CouponRecordQueryResp queryUserCouponRecord(Long scenicId, Long memberId, Long faceId, String goodsId) {
|
||||
CouponRecordQueryResp resp = new CouponRecordQueryResp();
|
||||
List<CouponRecordEntity> recordList = couponRecordMapper.queryByUserWithGoodsId(scenicId, memberId, goodsId);
|
||||
if (recordList != null && !recordList.isEmpty()) {
|
||||
Optional<CouponRecordEntity> record = recordList.stream().filter(item -> item.getStatus() == 0).filter(item -> item.getFaceId() == null || item.getFaceId().equals(faceId)).findAny();
|
||||
if (record.isPresent()) {
|
||||
CouponRecordEntity recordEntity = record.get();
|
||||
resp.setExist(true);
|
||||
resp.setId(recordEntity.getId());
|
||||
resp.setCouponId(recordEntity.getCouponId());
|
||||
CouponEntity coupon = couponMapper.getById(recordEntity.getCouponId());
|
||||
if (coupon != null) {
|
||||
resp.setMemberId(recordEntity.getMemberId());
|
||||
resp.setFaceId(recordEntity.getFaceId());
|
||||
resp.setStatus(recordEntity.getStatus());
|
||||
resp.setCreateTime(recordEntity.getCreateTime());
|
||||
resp.setUsedTime(recordEntity.getUsedTime());
|
||||
resp.setUsedOrderId(recordEntity.getUsedOrderId());
|
||||
resp.setCoupon(coupon);
|
||||
} else {
|
||||
resp.setExist(false);
|
||||
}
|
||||
} else {
|
||||
Optional<CouponRecordEntity> usedRecord = recordList.stream().filter(item -> item.getStatus() != 0).filter(item -> item.getFaceId() == null || item.getFaceId().equals(faceId)).findAny();
|
||||
if (usedRecord.isPresent()) {
|
||||
CouponRecordEntity recordEntity = usedRecord.get();
|
||||
resp.setExist(true);
|
||||
resp.setId(recordEntity.getId());
|
||||
resp.setCouponId(recordEntity.getCouponId());
|
||||
CouponEntity coupon = couponMapper.getById(recordEntity.getCouponId());
|
||||
if (coupon != null) {
|
||||
resp.setMemberId(recordEntity.getMemberId());
|
||||
resp.setFaceId(recordEntity.getFaceId());
|
||||
resp.setStatus(recordEntity.getStatus());
|
||||
resp.setCreateTime(recordEntity.getCreateTime());
|
||||
resp.setUsedTime(recordEntity.getUsedTime());
|
||||
resp.setUsedOrderId(recordEntity.getUsedOrderId());
|
||||
resp.setCoupon(coupon);
|
||||
} else {
|
||||
resp.setExist(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
|
||||
public boolean userGetCoupon(Long memberId, Long faceId, Integer couponId) {
|
||||
CouponEntity coupon = couponMapper.getById(couponId);
|
||||
if (coupon == null) {
|
||||
return false;
|
||||
}
|
||||
CouponRecordEntity entity = new CouponRecordEntity();
|
||||
entity.setCouponId(couponId);
|
||||
entity.setFaceId(faceId);
|
||||
entity.setMemberId(memberId);
|
||||
entity.setStatus(0);
|
||||
entity.setCreateTime(new Date());
|
||||
return couponRecordMapper.insert(entity) > 0;
|
||||
}
|
||||
|
||||
public boolean userUseCoupon(Long memberId, Long faceId, Integer couponRecordId, Long orderId) {
|
||||
CouponRecordEntity entity = new CouponRecordEntity();
|
||||
entity.setId(couponRecordId);
|
||||
entity.setStatus(1);
|
||||
entity.setUsedTime(new Date());
|
||||
entity.setUsedOrderId(orderId);
|
||||
return couponRecordMapper.updateById(entity) > 0;
|
||||
}
|
||||
}
|
||||
384
src/main/java/com/ycwl/basic/biz/FaceStatusManager.java
Normal file
384
src/main/java/com/ycwl/basic/biz/FaceStatusManager.java
Normal file
@@ -0,0 +1,384 @@
|
||||
package com.ycwl.basic.biz;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import com.ycwl.basic.enums.FaceCutStatus;
|
||||
import com.ycwl.basic.enums.FacePieceUpdateStatus;
|
||||
import com.ycwl.basic.enums.TemplateRenderStatus;
|
||||
import com.ycwl.basic.mapper.TaskMapper;
|
||||
import com.ycwl.basic.model.pc.task.entity.TaskEntity;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 人脸状态缓存管理器
|
||||
* 统一管理人脸相关的内存缓存状态(使用Caffeine)
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class FaceStatusManager {
|
||||
|
||||
/**
|
||||
* 默认过期时间:1小时
|
||||
*/
|
||||
private static final long DEFAULT_EXPIRE_SECONDS = 3600L;
|
||||
|
||||
/**
|
||||
* 人脸切片状态缓存
|
||||
*/
|
||||
private final Cache<String, Integer> faceCutStatusCache;
|
||||
|
||||
/**
|
||||
* 人脸片段更新状态缓存(全局和模板级)
|
||||
* 键存在=无新片段,键不存在=有新片段
|
||||
*/
|
||||
private final Cache<String, Boolean> faceNoPieceUpdateCache;
|
||||
|
||||
/**
|
||||
* 人脸模板渲染状态缓存
|
||||
*/
|
||||
private final Cache<String, Integer> templateRenderCache;
|
||||
|
||||
/**
|
||||
* 拼图素材版本缓存
|
||||
* 键:faceId:puzzleTemplateId -> 当时的图片源数量
|
||||
* 用于判断拼图模板的素材是否发生变化,避免重复生成
|
||||
*/
|
||||
private final Cache<String, Integer> puzzleSourceVersionCache;
|
||||
|
||||
@Autowired
|
||||
private TaskMapper taskMapper;
|
||||
|
||||
public FaceStatusManager() {
|
||||
// 初始化三个独立的缓存实例
|
||||
this.faceCutStatusCache = Caffeine.newBuilder()
|
||||
.expireAfterWrite(DEFAULT_EXPIRE_SECONDS, TimeUnit.SECONDS)
|
||||
.maximumSize(10000)
|
||||
.build();
|
||||
|
||||
this.faceNoPieceUpdateCache = Caffeine.newBuilder()
|
||||
.expireAfterWrite(DEFAULT_EXPIRE_SECONDS, TimeUnit.SECONDS)
|
||||
.maximumSize(10000)
|
||||
.build();
|
||||
|
||||
this.templateRenderCache = Caffeine.newBuilder()
|
||||
.expireAfterWrite(DEFAULT_EXPIRE_SECONDS, TimeUnit.SECONDS)
|
||||
.maximumSize(10000)
|
||||
.build();
|
||||
|
||||
this.puzzleSourceVersionCache = Caffeine.newBuilder()
|
||||
.expireAfterWrite(DEFAULT_EXPIRE_SECONDS, TimeUnit.SECONDS)
|
||||
.maximumSize(10000)
|
||||
.build();
|
||||
}
|
||||
|
||||
// ==================== 切片状态相关方法 ====================
|
||||
|
||||
/**
|
||||
* 设置人脸切片状态
|
||||
* @param faceId 人脸ID
|
||||
* @param status 切片状态
|
||||
*/
|
||||
public void setFaceCutStatus(Long faceId, FaceCutStatus status) {
|
||||
if (faceId == null || status == null) {
|
||||
log.warn("设置切片状态参数为空: faceId={}, status={}", faceId, status);
|
||||
return;
|
||||
}
|
||||
faceCutStatusCache.put(String.valueOf(faceId), status.getCode());
|
||||
log.debug("设置切片状态: faceId={}, status={}", faceId, status.getDescription());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取人脸切片状态
|
||||
* @param faceId 人脸ID
|
||||
* @return 切片状态,缓存不存在时返回 COMPLETED(已完成)
|
||||
*/
|
||||
public FaceCutStatus getFaceCutStatus(Long faceId) {
|
||||
if (faceId == null) {
|
||||
log.warn("获取切片状态参数为空: faceId={}", faceId);
|
||||
return FaceCutStatus.COMPLETED;
|
||||
}
|
||||
Integer code = faceCutStatusCache.getIfPresent(String.valueOf(faceId));
|
||||
if (code == null) {
|
||||
log.debug("切片状态缓存不存在,返回默认值COMPLETED: faceId={}", faceId);
|
||||
return FaceCutStatus.COMPLETED;
|
||||
}
|
||||
return FaceCutStatus.fromCodeOrDefault(code, FaceCutStatus.COMPLETED);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除人脸切片状态缓存
|
||||
* @param faceId 人脸ID
|
||||
*/
|
||||
public void deleteFaceCutStatus(Long faceId) {
|
||||
if (faceId == null) {
|
||||
return;
|
||||
}
|
||||
faceCutStatusCache.invalidate(String.valueOf(faceId));
|
||||
log.debug("删除切片状态缓存: faceId={}", faceId);
|
||||
}
|
||||
|
||||
// ==================== 片段更新状态相关方法 ====================
|
||||
|
||||
/**
|
||||
* 标记无新片段(设置缓存键)
|
||||
* @param faceId 人脸ID
|
||||
* @param templateId 模板ID(可选,为null时标记全局状态)
|
||||
*/
|
||||
public void markNoNewPieces(Long faceId, Long templateId) {
|
||||
if (faceId == null) {
|
||||
log.warn("标记无新片段参数为空: faceId={}", faceId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (templateId == null) {
|
||||
// 全局标记:该人脸的所有模板都无新片段
|
||||
faceNoPieceUpdateCache.put(String.valueOf(faceId), Boolean.TRUE);
|
||||
log.debug("标记无新片段(全局): faceId={}", faceId);
|
||||
} else {
|
||||
// 模板级标记:该人脸在该模板下无新片段
|
||||
faceNoPieceUpdateCache.put(faceId + ":" + templateId, Boolean.TRUE);
|
||||
log.debug("标记无新片段(模板): faceId={}, templateId={}", faceId, templateId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记有新片段(删除缓存键)
|
||||
* @param faceId 人脸ID
|
||||
* @param templateId 模板ID(可选,为null时标记全局状态)
|
||||
*/
|
||||
public void markHasNewPieces(Long faceId, Long templateId) {
|
||||
if (faceId == null) {
|
||||
log.warn("标记有新片段参数为空: faceId={}", faceId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (templateId == null) {
|
||||
// 全局标记:该人脸有新片段
|
||||
faceNoPieceUpdateCache.invalidate(String.valueOf(faceId));
|
||||
log.debug("标记有新片段(全局): faceId={}", faceId);
|
||||
} else {
|
||||
// 模板级标记:该人脸在该模板下有新片段
|
||||
faceNoPieceUpdateCache.invalidate(faceId + ":" + templateId);
|
||||
log.debug("标记有新片段(模板): faceId={}, templateId={}", faceId, templateId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取人脸片段更新状态
|
||||
* @param faceId 人脸ID
|
||||
* @param templateId 模板ID(可选,为null时查询全局状态)
|
||||
* @return 片段更新状态,键存在=无新片段,键不存在=有新片段
|
||||
*/
|
||||
public FacePieceUpdateStatus getFacePieceUpdateStatus(Long faceId, Long templateId) {
|
||||
if (faceId == null) {
|
||||
log.warn("获取片段更新状态参数为空: faceId={}", faceId);
|
||||
return FacePieceUpdateStatus.HAS_NEW_PIECES;
|
||||
}
|
||||
|
||||
String key = templateId == null ? String.valueOf(faceId) : faceId + ":" + templateId;
|
||||
boolean exists = faceNoPieceUpdateCache.getIfPresent(key) != null;
|
||||
FacePieceUpdateStatus status = FacePieceUpdateStatus.fromKeyExists(exists);
|
||||
|
||||
if (templateId == null) {
|
||||
log.debug("获取片段更新状态(全局): faceId={}, status={}", faceId, status.getDescription());
|
||||
} else {
|
||||
log.debug("获取片段更新状态(模板): faceId={}, templateId={}, status={}",
|
||||
faceId, templateId, status.getDescription());
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取人脸片段更新状态 - 全局版本
|
||||
* @param faceId 人脸ID
|
||||
* @return 片段更新状态,键存在=无新片段,键不存在=有新片段
|
||||
*/
|
||||
public FacePieceUpdateStatus getFacePieceUpdateStatus(Long faceId) {
|
||||
return getFacePieceUpdateStatus(faceId, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否有新片段
|
||||
* @param faceId 人脸ID
|
||||
* @param templateId 模板ID(可选,为null时查询全局状态)
|
||||
* @return true=有新片段,false=无新片段;如果templateId为null则默认返回true(有新片段)
|
||||
*/
|
||||
public boolean hasNewPieces(Long faceId, Long templateId) {
|
||||
if (templateId == null) {
|
||||
// 如果没有指定templateId,默认认为有新片段
|
||||
log.debug("未指定templateId,默认返回有新片段: faceId={}", faceId);
|
||||
return true;
|
||||
}
|
||||
return getFacePieceUpdateStatus(faceId, templateId).hasNewPieces();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否有新片段 - 全局版本
|
||||
* @param faceId 人脸ID
|
||||
* @return true=有新片段,false=无新片段
|
||||
*/
|
||||
public boolean hasNewPieces(Long faceId) {
|
||||
return getFacePieceUpdateStatus(faceId, null).hasNewPieces();
|
||||
}
|
||||
|
||||
// ==================== 模板渲染状态相关方法 ====================
|
||||
|
||||
/**
|
||||
* 设置人脸模板渲染状态
|
||||
* @param faceId 人脸ID
|
||||
* @param templateId 模板ID
|
||||
* @param status 渲染状态
|
||||
*/
|
||||
public void setTemplateRenderStatus(Long faceId, Long templateId, TemplateRenderStatus status) {
|
||||
if (faceId == null || templateId == null || status == null) {
|
||||
log.warn("设置模板渲染状态参数为空: faceId={}, templateId={}, status={}", faceId, templateId, status);
|
||||
return;
|
||||
}
|
||||
templateRenderCache.put(faceId + ":" + templateId, status.getCode());
|
||||
log.debug("设置模板渲染状态: faceId={}, templateId={}, status={}", faceId, templateId, status.getDescription());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取人脸模板渲染状态
|
||||
* @param faceId 人脸ID
|
||||
* @param templateId 模板ID
|
||||
* @return 渲染状态,缓存不存在时返回 null
|
||||
*/
|
||||
public TemplateRenderStatus getTemplateRenderStatus(Long faceId, Long templateId) {
|
||||
if (faceId == null || templateId == null) {
|
||||
log.warn("获取模板渲染状态参数为空: faceId={}, templateId={}", faceId, templateId);
|
||||
return null;
|
||||
}
|
||||
Integer code = templateRenderCache.getIfPresent(faceId + ":" + templateId);
|
||||
if (code == null) {
|
||||
log.debug("模板渲染状态缓存不存在: faceId={}, templateId={}", faceId, templateId);
|
||||
// 查询数据库
|
||||
TaskEntity task = taskMapper.listLastFaceTemplateTask(faceId, templateId);
|
||||
if (task == null) {
|
||||
setTemplateRenderStatus(faceId, templateId, TemplateRenderStatus.NONE);
|
||||
return TemplateRenderStatus.NONE;
|
||||
}
|
||||
if (Integer.valueOf(2).equals(task.getStatus())) {
|
||||
setTemplateRenderStatus(faceId, templateId, TemplateRenderStatus.RENDERING);
|
||||
}
|
||||
if (Integer.valueOf(1).equals(task.getStatus())) {
|
||||
setTemplateRenderStatus(faceId, templateId, TemplateRenderStatus.RENDERED);
|
||||
}
|
||||
return TemplateRenderStatus.NONE;
|
||||
}
|
||||
return TemplateRenderStatus.fromCode(code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除人脸模板渲染状态缓存
|
||||
* @param faceId 人脸ID
|
||||
* @param templateId 模板ID
|
||||
*/
|
||||
public void deleteTemplateRenderStatus(Long faceId, Long templateId) {
|
||||
if (faceId == null || templateId == null) {
|
||||
return;
|
||||
}
|
||||
templateRenderCache.invalidate(faceId + ":" + templateId);
|
||||
log.debug("删除模板渲染状态缓存: faceId={}, templateId={}", faceId, templateId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除人脸的所有模板渲染状态(使用模式匹配)
|
||||
* 注意:此操作可能影响性能,谨慎使用
|
||||
* @param faceId 人脸ID
|
||||
*/
|
||||
public void deleteAllTemplateRenderStatus(Long faceId) {
|
||||
if (faceId == null) {
|
||||
return;
|
||||
}
|
||||
String prefix = faceId + ":";
|
||||
long count = templateRenderCache.asMap().keySet().stream()
|
||||
.filter(key -> key.startsWith(prefix))
|
||||
.peek(templateRenderCache::invalidate)
|
||||
.count();
|
||||
if (count > 0) {
|
||||
log.debug("批量删除模板渲染状态缓存: faceId={}, count={}", faceId, count);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 拼图素材版本相关方法 ====================
|
||||
|
||||
/**
|
||||
* 标记拼图素材版本(记录当前的图片源数量)
|
||||
* 在拼图生成成功后调用,用于后续判断素材是否变化
|
||||
*
|
||||
* @param faceId 人脸ID
|
||||
* @param puzzleTemplateId 拼图模板ID(全局唯一)
|
||||
* @param sourceCount 当前的图片源数量
|
||||
*/
|
||||
public void markPuzzleSourceVersion(Long faceId, Long puzzleTemplateId, int sourceCount) {
|
||||
if (faceId == null || puzzleTemplateId == null) {
|
||||
log.warn("标记拼图素材版本参数为空: faceId={}, puzzleTemplateId={}", faceId, puzzleTemplateId);
|
||||
return;
|
||||
}
|
||||
String key = faceId + ":" + puzzleTemplateId;
|
||||
puzzleSourceVersionCache.put(key, sourceCount);
|
||||
log.debug("标记拼图素材版本: faceId={}, puzzleTemplateId={}, sourceCount={}", faceId, puzzleTemplateId, sourceCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断拼图素材是否发生变化
|
||||
* 通过比较当前的图片源数量与缓存中记录的数量
|
||||
*
|
||||
* @param faceId 人脸ID
|
||||
* @param puzzleTemplateId 拼图模板ID(全局唯一)
|
||||
* @param currentSourceCount 当前的图片源数量
|
||||
* @return true=素材已变化(需要重新生成),false=素材未变化(可以跳过生成)
|
||||
*/
|
||||
public boolean isPuzzleSourceChanged(Long faceId, Long puzzleTemplateId, int currentSourceCount) {
|
||||
if (faceId == null || puzzleTemplateId == null) {
|
||||
log.warn("判断拼图素材变化参数为空: faceId={}, puzzleTemplateId={}", faceId, puzzleTemplateId);
|
||||
return true; // 参数不合法时默认认为有变化
|
||||
}
|
||||
|
||||
String key = faceId + ":" + puzzleTemplateId;
|
||||
Integer cachedCount = puzzleSourceVersionCache.getIfPresent(key);
|
||||
|
||||
if (cachedCount == null) {
|
||||
// 缓存不存在,认为有变化(首次生成或缓存过期)
|
||||
log.debug("拼图素材版本缓存不存在,需要生成: faceId={}, puzzleTemplateId={}", faceId, puzzleTemplateId);
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean changed = !cachedCount.equals(currentSourceCount);
|
||||
if (changed) {
|
||||
log.debug("拼图素材已变化: faceId={}, puzzleTemplateId={}, cachedCount={}, currentCount={}",
|
||||
faceId, puzzleTemplateId, cachedCount, currentSourceCount);
|
||||
} else {
|
||||
log.debug("拼图素材未变化,可跳过生成: faceId={}, puzzleTemplateId={}, sourceCount={}",
|
||||
faceId, puzzleTemplateId, currentSourceCount);
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使指定人脸的所有拼图素材版本缓存失效
|
||||
* 当人脸的图片关联发生变化时调用(如人脸匹配后新增了关联)
|
||||
*
|
||||
* @param faceId 人脸ID
|
||||
*/
|
||||
public void invalidatePuzzleSourceVersion(Long faceId) {
|
||||
if (faceId == null) {
|
||||
return;
|
||||
}
|
||||
String prefix = faceId + ":";
|
||||
long count = puzzleSourceVersionCache.asMap().keySet().stream()
|
||||
.filter(key -> key.startsWith(prefix))
|
||||
.peek(puzzleSourceVersionCache::invalidate)
|
||||
.count();
|
||||
if (count > 0) {
|
||||
log.debug("批量使拼图素材版本缓存失效: faceId={}, count={}", faceId, count);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,24 @@
|
||||
package com.ycwl.basic.biz;
|
||||
|
||||
import com.ycwl.basic.clickhouse.service.StatsQueryService;
|
||||
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.IsBuyRespVO;
|
||||
import com.ycwl.basic.model.mobile.order.PriceObj;
|
||||
import com.ycwl.basic.model.mobile.statistic.req.StatisticsRecordAddReq;
|
||||
import com.ycwl.basic.model.pc.coupon.entity.CouponEntity;
|
||||
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.integration.common.manager.ScenicConfigManager;
|
||||
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;
|
||||
import com.ycwl.basic.pricing.enums.ProductType;
|
||||
import com.ycwl.basic.pricing.service.IPriceCalculationService;
|
||||
import com.ycwl.basic.profitsharing.biz.ProfitSharingBiz;
|
||||
import com.ycwl.basic.repository.FaceRepository;
|
||||
import com.ycwl.basic.repository.OrderRepository;
|
||||
@@ -33,23 +28,18 @@ 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.Comparator;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Component
|
||||
public class OrderBiz {
|
||||
|
||||
@Autowired
|
||||
private VideoMapper videoMapper;
|
||||
@Autowired
|
||||
private ScenicRepository scenicRepository;
|
||||
@Autowired
|
||||
@@ -67,28 +57,26 @@ public class OrderBiz {
|
||||
@Autowired
|
||||
private OrderMapper orderMapper;
|
||||
@Autowired
|
||||
private SourceMapper sourceMapper;
|
||||
@Autowired
|
||||
private ProfitSharingBiz profitSharingBiz;
|
||||
@Autowired
|
||||
private VideoTaskRepository videoTaskRepository;
|
||||
@Autowired
|
||||
private BrokerBiz brokerBiz;
|
||||
@Autowired
|
||||
private CouponBiz couponBiz;
|
||||
@Autowired
|
||||
@Lazy
|
||||
private PrinterService printerService;
|
||||
@Autowired
|
||||
private IPriceCalculationService iPriceCalculationService;
|
||||
@Autowired
|
||||
private StatsQueryService statsQueryService;
|
||||
|
||||
public PriceObj queryPrice(Long scenicId, int goodsType, Long goodsId) {
|
||||
public PriceObj queryPrice(Long scenicId, Long memberId, int goodsType, Long goodsId) {
|
||||
PriceObj priceObj = new PriceObj();
|
||||
priceObj.setGoodsType(goodsType);
|
||||
priceObj.setGoodsId(goodsId);
|
||||
ScenicEntity scenic = scenicRepository.getScenic(scenicId);
|
||||
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId);
|
||||
priceObj.setScenicAllPrice(scenic.getPrice());
|
||||
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(scenicId);
|
||||
if (scenicConfig != null) {
|
||||
if (Integer.valueOf(1).equals(scenicConfig.getAllFree())) {
|
||||
if (Boolean.TRUE.equals(scenicConfig.getBoolean("all_free"))) {
|
||||
// 景区全免
|
||||
priceObj.setFree(true);
|
||||
priceObj.setPrice(BigDecimal.ZERO);
|
||||
@@ -102,32 +90,85 @@ public class OrderBiz {
|
||||
if (video == null) {
|
||||
return null;
|
||||
}
|
||||
TaskEntity task = videoTaskRepository.getTaskById(video.getTaskId());
|
||||
if (task != null) {
|
||||
priceObj.setFaceId(task.getFaceId());
|
||||
}
|
||||
priceObj.setFaceId(video.getFaceId());
|
||||
TemplateRespVO template = templateRepository.getTemplate(video.getTemplateId());
|
||||
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.setUserId(memberId);
|
||||
vlogCalculationRequest.setFaceId(priceObj.getFaceId());
|
||||
vlogCalculationRequest.setPreviewOnly(true); // 仅查询价格,不实际使用优惠
|
||||
vlogCalculationRequest.setAutoUseCoupon(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
|
||||
priceObj.setPrice(scenic.getSourceVideoPrice());
|
||||
priceObj.setSlashPrice(scenic.getSourceVideoPrice());
|
||||
case 2: // source
|
||||
FaceEntity face = faceRepository.getFace(goodsId);
|
||||
PriceCalculationRequest calculationRequest = new PriceCalculationRequest();
|
||||
ProductItem productItem = new ProductItem();
|
||||
productItem.setProductType(goodsType == 1 ? ProductType.RECORDING_SET : ProductType.PHOTO_SET);
|
||||
productItem.setProductId(scenicId.toString());
|
||||
productItem.setPurchaseCount(1);
|
||||
productItem.setScenicId(scenicId.toString());
|
||||
calculationRequest.setProducts(Collections.singletonList(productItem));
|
||||
if (face != null) {
|
||||
calculationRequest.setUserId(face.getMemberId());
|
||||
}
|
||||
calculationRequest.setUserId(memberId);
|
||||
calculationRequest.setFaceId(goodsId);
|
||||
calculationRequest.setPreviewOnly(true); // 仅查询价格,不实际使用优惠
|
||||
calculationRequest.setAutoUseCoupon(true);
|
||||
PriceCalculationResult priceCalculationResult = iPriceCalculationService.calculatePrice(calculationRequest);
|
||||
priceObj.setPrice(priceCalculationResult.getFinalAmount());
|
||||
priceObj.setSlashPrice(priceCalculationResult.getOriginalAmount());
|
||||
priceObj.setFaceId(goodsId);
|
||||
break;
|
||||
case 2: // source
|
||||
priceObj.setPrice(scenic.getSourceImagePrice());
|
||||
priceObj.setSlashPrice(scenic.getSourceImagePrice());
|
||||
case 5:
|
||||
PriceCalculationRequest plogCalculationRequest = new PriceCalculationRequest();
|
||||
ProductItem plogProductItem = new ProductItem();
|
||||
plogProductItem.setProductType(ProductType.PHOTO_LOG);
|
||||
plogProductItem.setProductId(scenicId.toString());
|
||||
plogProductItem.setPurchaseCount(1);
|
||||
plogProductItem.setScenicId(scenicId.toString());
|
||||
plogCalculationRequest.setProducts(Collections.singletonList(plogProductItem));
|
||||
plogCalculationRequest.setUserId(memberId);
|
||||
plogCalculationRequest.setFaceId(goodsId);
|
||||
plogCalculationRequest.setPreviewOnly(true); // 仅查询价格,不实际使用优惠
|
||||
plogCalculationRequest.setAutoUseCoupon(true);
|
||||
PriceCalculationResult plogPriceCalculationResult = iPriceCalculationService.calculatePrice(plogCalculationRequest);
|
||||
priceObj.setPrice(plogPriceCalculationResult.getFinalAmount());
|
||||
priceObj.setSlashPrice(plogPriceCalculationResult.getOriginalAmount());
|
||||
priceObj.setFaceId(goodsId);
|
||||
priceObj.setScenicId(scenicId);
|
||||
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.setUserId(memberId);
|
||||
aiCamCalculationRequest.setFaceId(goodsId);
|
||||
aiCamCalculationRequest.setPreviewOnly(true); // 仅查询价格,不实际使用优惠
|
||||
aiCamCalculationRequest.setAutoUseCoupon(true);
|
||||
PriceCalculationResult aiCamPriceCalculationResult = iPriceCalculationService.calculatePrice(aiCamCalculationRequest);
|
||||
priceObj.setPrice(aiCamPriceCalculationResult.getFinalAmount());
|
||||
priceObj.setSlashPrice(aiCamPriceCalculationResult.getOriginalAmount());
|
||||
priceObj.setFaceId(goodsId);
|
||||
priceObj.setScenicId(scenicId);
|
||||
break;
|
||||
}
|
||||
return priceObj;
|
||||
@@ -144,85 +185,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, memberId, goodsType, goodsId);
|
||||
if (priceObj == null) {
|
||||
return respVO;
|
||||
}
|
||||
respVO.setBuy(false);
|
||||
respVO.setOrigPrice(priceObj.getPrice());
|
||||
respVO.setSlashPrice(priceObj.getSlashPrice());
|
||||
return respVO;
|
||||
}
|
||||
|
||||
@@ -236,55 +238,26 @@ 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); // 更新完了,清理下
|
||||
Integer couponRecordId = order.getCouponRecordId();
|
||||
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());
|
||||
Long enterType = statsQueryService.getUserRecentEnterType(order.getMemberId(), order.getCreateAt());
|
||||
if(!Long.valueOf(1014).equals(enterType)){//
|
||||
statisticsRecordAddReq.setType(StatisticEnum.ON_SITE_PAYMENT.code);
|
||||
}else {
|
||||
@@ -309,9 +282,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); // 更新完了,清理下
|
||||
@@ -331,12 +306,15 @@ 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, "订单已退款");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
package com.ycwl.basic.biz;
|
||||
|
||||
import com.ycwl.basic.model.mobile.order.IsBuyBatchRespVO;
|
||||
import com.ycwl.basic.model.pc.coupon.entity.CouponEntity;
|
||||
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.price.entity.PriceConfigEntity;
|
||||
import com.ycwl.basic.model.pc.price.resp.GoodsListRespVO;
|
||||
import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity;
|
||||
import com.ycwl.basic.model.pc.price.resp.SimpleGoodsRespVO;
|
||||
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
|
||||
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.puzzle.repository.PuzzleRepository;
|
||||
import com.ycwl.basic.repository.FaceRepository;
|
||||
import com.ycwl.basic.repository.MemberRelationRepository;
|
||||
import com.ycwl.basic.repository.OrderRepository;
|
||||
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,15 @@ public class PriceBiz {
|
||||
@Autowired
|
||||
private FaceRepository faceRepository;
|
||||
@Autowired
|
||||
private CouponBiz couponBiz;
|
||||
private MemberRelationRepository memberRelationRepository;
|
||||
@Autowired
|
||||
private PuzzleTemplateMapper puzzleTemplateMapper;
|
||||
@Autowired
|
||||
private PuzzleRepository puzzleRepository;
|
||||
@Autowired
|
||||
private IProductTypeCapabilityManagementService productTypeCapabilityManagementService;
|
||||
@Autowired
|
||||
private OrderRepository orderRepository;
|
||||
|
||||
public List<GoodsListRespVO> listGoodsByScenic(Long scenicId) {
|
||||
List<GoodsListRespVO> goodsList = new ArrayList<>();
|
||||
@@ -46,17 +63,131 @@ 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);
|
||||
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(scenicId);
|
||||
if (scenicConfig != null) {
|
||||
if (!Integer.valueOf(1).equals(scenicConfig.getDisableSourceVideo())) {
|
||||
goodsList.add(new GoodsListRespVO(1L, "录像集"));
|
||||
if (!Boolean.TRUE.equals(scenicConfig.getBoolean("disable_source_video"))) {
|
||||
goodsList.add(new GoodsListRespVO(1L, "录像集", 1));
|
||||
}
|
||||
if (!Integer.valueOf(1).equals(scenicConfig.getDisableSourceImage())) {
|
||||
goodsList.add(new GoodsListRespVO(2L, "照片集"));
|
||||
if (!Boolean.TRUE.equals(scenicConfig.getBoolean("disable_source_image"))) {
|
||||
goodsList.add(new GoodsListRespVO(2L, "照片集", 2));
|
||||
}
|
||||
}
|
||||
// 拼图(使用缓存)
|
||||
puzzleRepository.listTemplateByScenic(scenicId).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模板
|
||||
List<PuzzleTemplateEntity> puzzleList = puzzleRepository.listTemplateByScenic(scenicId);
|
||||
puzzleList.stream()
|
||||
.map(template -> new SimpleGoodsRespVO(template.getId(), template.getName(), productType))
|
||||
.forEach(goodsList::add);
|
||||
if (!puzzleList.isEmpty()) {
|
||||
goodsList.addFirst(new SimpleGoodsRespVO(scenicId, "pLog图<景区打包>", productType));
|
||||
}
|
||||
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 +211,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) {
|
||||
@@ -90,39 +221,15 @@ public class PriceBiz {
|
||||
if (face != null && !face.getMemberId().equals(userId)) {
|
||||
return null;
|
||||
}
|
||||
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId);
|
||||
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(scenicId);
|
||||
if (scenicConfig != null) {
|
||||
if (Integer.valueOf(1).equals(scenicConfig.getAllFree())) {
|
||||
if (Boolean.TRUE.equals(scenicConfig.getBoolean("all_free"))) {
|
||||
// 景区全免
|
||||
respVO.setFree(true);
|
||||
respVO.setSlashPrice(BigDecimal.ZERO);
|
||||
return respVO;
|
||||
}
|
||||
}
|
||||
switch (type) {
|
||||
case 0: // 单个定价
|
||||
CouponRecordQueryResp recordQueryResp = couponBiz.queryUserCouponRecord(scenicId, userId, faceId, goodsIds);
|
||||
if (recordQueryResp.isUsable()) {
|
||||
respVO.setCouponId(recordQueryResp.getCouponId());
|
||||
respVO.setCouponRecordId(recordQueryResp.getId());
|
||||
CouponEntity coupon = recordQueryResp.getCoupon();
|
||||
if (coupon != null) {
|
||||
respVO.setCouponPrice(coupon.calculateDiscountPrice(priceConfig.getPrice()));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case -1:
|
||||
CouponRecordQueryResp oneCouponRecordQueryResp = couponBiz.queryUserCouponRecord(scenicId, userId, faceId, "-1");
|
||||
if (oneCouponRecordQueryResp.isUsable()) {
|
||||
respVO.setCouponId(oneCouponRecordQueryResp.getCouponId());
|
||||
respVO.setCouponRecordId(oneCouponRecordQueryResp.getId());
|
||||
CouponEntity coupon = oneCouponRecordQueryResp.getCoupon();
|
||||
if (coupon != null) {
|
||||
respVO.setCouponPrice(coupon.calculateDiscountPrice(priceConfig.getPrice()));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
respVO.setConfigId(priceConfig.getId());
|
||||
respVO.setGoodsIds(goodsIds);
|
||||
respVO.setType(type);
|
||||
@@ -138,6 +245,53 @@ 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 = orderRepository.checkUserBuyFaceItem(userId, faceId, -1, videoEntities.getFirst().getVideoId());
|
||||
if (!hasPurchasedTemplate) {
|
||||
allContentsPurchased = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查源文件购买状态(录像集和照片集)
|
||||
if (allContentsPurchased) {
|
||||
if (scenicConfig != null) {
|
||||
// 检查录像集
|
||||
if (!Boolean.TRUE.equals(scenicConfig.getBoolean("disable_source_video"))) {
|
||||
boolean hasPurchasedRecording = orderRepository.checkUserBuyFaceItem(userId, faceId, 1, faceId);
|
||||
if (!hasPurchasedRecording) {
|
||||
allContentsPurchased = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查照片集
|
||||
if (allContentsPurchased && !Boolean.TRUE.equals(scenicConfig.getBoolean("disable_source_image"))) {
|
||||
boolean hasPurchasedPhoto = orderRepository.checkUserBuyFaceItem(userId, faceId, 2, faceId);
|
||||
if (!hasPurchasedPhoto) {
|
||||
allContentsPurchased = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果所有内容都已购买,则认为已购买套餐
|
||||
if (allContentsPurchased) {
|
||||
respVO.setBuy(true);
|
||||
}
|
||||
}
|
||||
respVO.setShare(false);
|
||||
return respVO;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
package com.ycwl.basic.biz;
|
||||
|
||||
import com.ycwl.basic.mapper.FaceMapper;
|
||||
import com.ycwl.basic.mapper.TaskMapper;
|
||||
import com.ycwl.basic.mapper.VideoMapper;
|
||||
import com.ycwl.basic.model.mobile.goods.VideoTaskStatusVO;
|
||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||
import com.ycwl.basic.model.pc.face.resp.FaceRespVO;
|
||||
import com.ycwl.basic.model.pc.task.req.TaskReqQuery;
|
||||
import com.ycwl.basic.model.pc.task.resp.TaskRespVO;
|
||||
import com.ycwl.basic.model.pc.template.resp.TemplateRespVO;
|
||||
import com.ycwl.basic.model.pc.video.entity.VideoEntity;
|
||||
import com.ycwl.basic.repository.FaceRepository;
|
||||
import com.ycwl.basic.repository.TemplateRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Component
|
||||
public class TaskStatusBiz {
|
||||
public static final String TASK_STATUS_USER_CACHE_KEY = "task:status:user:%s:face:%s";
|
||||
public static final String TASK_STATUS_FACE_CACHE_KEY = "task:status:face:%s";
|
||||
public static final String TASK_STATUS_FACE_CACHE_KEY_CUT = "task:status:face:%s:cut";
|
||||
public static final String TASK_STATUS_FACE_CACHE_KEY_TEMPLATE = "task:status:face:%s:tpl:%s";
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate<String, String> redisTemplate;
|
||||
@Autowired
|
||||
private FaceRepository faceRepository;
|
||||
@Autowired
|
||||
private TemplateRepository templateRepository;
|
||||
@Autowired
|
||||
private FaceMapper faceMapper;
|
||||
@Autowired
|
||||
private TaskMapper taskMapper;
|
||||
@Autowired
|
||||
private VideoMapper videoMapper;
|
||||
@Autowired
|
||||
private TemplateBiz templateBiz;
|
||||
|
||||
public boolean getUserHaveFace(Long userId, Long faceId) {
|
||||
if (userId == null || faceId == null) {
|
||||
return false;
|
||||
}
|
||||
if (redisTemplate.hasKey(String.format(TASK_STATUS_USER_CACHE_KEY, userId, faceId))) {
|
||||
return true;
|
||||
}
|
||||
FaceEntity face = faceRepository.getFace(faceId);
|
||||
if (face == null) {
|
||||
return false;
|
||||
}
|
||||
if (face.getMemberId().equals(userId)) {
|
||||
redisTemplate.opsForValue().set(String.format(TASK_STATUS_USER_CACHE_KEY, userId, faceId), "1", 3600, TimeUnit.SECONDS);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void setFaceCutStatus(Long faceId, int status) {
|
||||
redisTemplate.opsForValue().set(String.format(TASK_STATUS_FACE_CACHE_KEY_CUT, faceId), String.valueOf(status), 3600, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public void setFaceTemplateStatus(Long faceId, Long templateId, Long videoId) {
|
||||
redisTemplate.opsForValue().set(String.format(TASK_STATUS_FACE_CACHE_KEY_TEMPLATE, faceId, templateId), String.valueOf(videoId), 3600, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public VideoTaskStatusVO getScenicUserStatus(Long scenicId, Long userId) {
|
||||
FaceRespVO lastFace = faceMapper.findLastFaceByScenicAndUserId(scenicId, userId);
|
||||
VideoTaskStatusVO response = new VideoTaskStatusVO();
|
||||
if (lastFace == null) {
|
||||
response.setStatus(-1);
|
||||
return response;
|
||||
}
|
||||
return getFaceStatus(lastFace.getId());
|
||||
}
|
||||
|
||||
public VideoTaskStatusVO getFaceStatus(Long faceId) {
|
||||
FaceEntity face = faceRepository.getFace(faceId);
|
||||
VideoTaskStatusVO response = new VideoTaskStatusVO();
|
||||
if (face == null) {
|
||||
response.setStatus(-1);
|
||||
return response;
|
||||
}
|
||||
response.setScenicId(face.getScenicId());
|
||||
response.setFaceId(faceId);
|
||||
List<TemplateRespVO> templateList = templateRepository.getTemplateListByScenicId(face.getScenicId());
|
||||
response.setMaxCount(templateList.size());
|
||||
int alreadyFinished = 0;
|
||||
for (TemplateRespVO template : templateList) {
|
||||
response.setTemplateId(template.getId());
|
||||
long videoId = getFaceTemplateVideoId(faceId, template.getId());
|
||||
if (videoId <= 0) {
|
||||
response.setStatus(2);
|
||||
} else {
|
||||
response.setVideoId(videoId);
|
||||
alreadyFinished++;
|
||||
}
|
||||
}
|
||||
response.setCount(alreadyFinished);
|
||||
if (alreadyFinished == 0) {
|
||||
response.setStatus(0);
|
||||
} else {
|
||||
response.setStatus(1);
|
||||
}
|
||||
if (alreadyFinished == 0) {
|
||||
int faceCutStatus = getFaceCutStatus(faceId);
|
||||
if (faceCutStatus != 1) {
|
||||
// 正在切片
|
||||
if (templateBiz.determineTemplateCanGenerate(templateList.getFirst().getId(), faceId, false)) {
|
||||
response.setStatus(2);
|
||||
} else {
|
||||
response.setStatus(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
public VideoTaskStatusVO getFaceTemplateStatus(Long faceId, Long templateId) {
|
||||
FaceEntity face = faceRepository.getFace(faceId);
|
||||
VideoTaskStatusVO response = new VideoTaskStatusVO();
|
||||
if (face == null) {
|
||||
response.setStatus(-1);
|
||||
return response;
|
||||
}
|
||||
response.setScenicId(face.getScenicId());
|
||||
response.setFaceId(faceId);
|
||||
response.setTemplateId(templateId);
|
||||
long videoId = getFaceTemplateVideoId(faceId, templateId);
|
||||
if (videoId < 0) {
|
||||
int faceCutStatus = getFaceCutStatus(faceId);
|
||||
if (faceCutStatus != 1) {
|
||||
// 正在切片
|
||||
response.setStatus(2);
|
||||
return response;
|
||||
}
|
||||
} else if (videoId == 0) {
|
||||
response.setStatus(2);
|
||||
} else {
|
||||
response.setVideoId(videoId);
|
||||
response.setStatus(1);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
public int getFaceCutStatus(Long faceId) {
|
||||
if (redisTemplate.hasKey(String.format(TASK_STATUS_FACE_CACHE_KEY_CUT, faceId))) {
|
||||
String status = redisTemplate.opsForValue().get(String.format(TASK_STATUS_FACE_CACHE_KEY_CUT, faceId));
|
||||
if (status != null) {
|
||||
return Integer.parseInt(status);
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
public long getFaceTemplateVideoId(Long faceId, Long templateId) {
|
||||
if (redisTemplate.hasKey(String.format(TASK_STATUS_FACE_CACHE_KEY_TEMPLATE, faceId, templateId))) {
|
||||
String status = redisTemplate.opsForValue().get(String.format(TASK_STATUS_FACE_CACHE_KEY_TEMPLATE, faceId, templateId));
|
||||
if (status != null) {
|
||||
return Long.parseLong(status);
|
||||
}
|
||||
}
|
||||
TaskReqQuery taskReqQuery = new TaskReqQuery();
|
||||
taskReqQuery.setFaceId(faceId);
|
||||
taskReqQuery.setTemplateId(templateId);
|
||||
List<TaskRespVO> list = taskMapper.list(taskReqQuery);
|
||||
Optional<TaskRespVO> min = list.stream().min(Comparator.comparing(TaskRespVO::getCreateTime));
|
||||
if (min.isPresent()) {
|
||||
TaskRespVO task = min.get();
|
||||
long taskStatus = 0;
|
||||
if (task.getStatus() == 1) {
|
||||
// 已完成
|
||||
VideoEntity video = videoMapper.findByTaskId(task.getId());
|
||||
if (video != null) {
|
||||
taskStatus = video.getId();
|
||||
}
|
||||
}
|
||||
setFaceTemplateStatus(faceId, templateId, taskStatus);
|
||||
} else {
|
||||
// 从来没生成过
|
||||
setFaceTemplateStatus(faceId, templateId, -1L);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package com.ycwl.basic.clickhouse.service;
|
||||
|
||||
import com.ycwl.basic.model.mobile.statistic.req.CommonQueryReq;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 统计数据查询服务接口
|
||||
* 用于抽象 t_stats 和 t_stats_record 表的查询
|
||||
* 支持 MySQL 和 ClickHouse 两种实现
|
||||
*/
|
||||
public interface StatsQueryService {
|
||||
|
||||
/**
|
||||
* 统计预览视频人数
|
||||
*/
|
||||
Integer countPreviewVideoOfMember(CommonQueryReq query);
|
||||
|
||||
/**
|
||||
* 统计扫码访问人数
|
||||
*/
|
||||
Integer countScanCodeOfMember(CommonQueryReq query);
|
||||
|
||||
/**
|
||||
* 统计推送订阅人数
|
||||
*/
|
||||
Integer countPushOfMember(CommonQueryReq query);
|
||||
|
||||
/**
|
||||
* 统计上传头像人数
|
||||
*/
|
||||
Integer countUploadFaceOfMember(CommonQueryReq query);
|
||||
|
||||
/**
|
||||
* 统计生成视频人数
|
||||
*/
|
||||
Integer countCompleteVideoOfMember(CommonQueryReq query);
|
||||
|
||||
/**
|
||||
* 统计生成视频条数
|
||||
*/
|
||||
Integer countCompleteOfVideo(CommonQueryReq query);
|
||||
|
||||
/**
|
||||
* 统计总访问人数
|
||||
*/
|
||||
Integer countTotalVisitorOfMember(CommonQueryReq query);
|
||||
|
||||
/**
|
||||
* 统计预览视频条数
|
||||
*/
|
||||
Integer countPreviewOfVideo(CommonQueryReq query);
|
||||
|
||||
/**
|
||||
* 获取用户分销员 ID 列表
|
||||
*/
|
||||
List<Long> getBrokerIdListForUser(Long memberId, Date startTime, Date endTime);
|
||||
|
||||
/**
|
||||
* 获取用户最近进入类型
|
||||
*/
|
||||
Long getUserRecentEnterType(Long memberId, Date endTime);
|
||||
|
||||
/**
|
||||
* 获取用户项目 ID 列表
|
||||
*/
|
||||
List<Long> getProjectIdListForUser(Long memberId, Date startTime, Date endTime);
|
||||
|
||||
/**
|
||||
* 统计分销员扫码次数
|
||||
*/
|
||||
Integer countBrokerScanCount(Long brokerId);
|
||||
|
||||
/**
|
||||
* 按日期统计分销员扫码数据
|
||||
*/
|
||||
List<HashMap<String, Object>> getDailyScanStats(Long brokerId, Date startTime, Date endTime);
|
||||
|
||||
/**
|
||||
* 按小时统计扫码人数(仅返回统计数据,不含订单)
|
||||
* 返回格式: [{t: "MM-dd HH", count: "xxx"}, ...]
|
||||
*/
|
||||
List<HashMap<String, String>> scanCodeMemberChartByHour(CommonQueryReq query);
|
||||
|
||||
/**
|
||||
* 按日期统计扫码人数(仅返回统计数据,不含订单)
|
||||
* 返回格式: [{t: "MM-dd", count: "xxx"}, ...]
|
||||
*/
|
||||
List<HashMap<String, String>> scanCodeMemberChartByDate(CommonQueryReq query);
|
||||
|
||||
/**
|
||||
* 按小时统计访问打印样片页面人数
|
||||
* 返回格式: [{t: "MM-dd HH", count: "xxx"}, ...]
|
||||
*/
|
||||
List<HashMap<String, String>> printerFromSampleChartByHour(CommonQueryReq query);
|
||||
|
||||
/**
|
||||
* 按日期统计访问打印样片页面人数
|
||||
* 返回格式: [{t: "MM-dd", count: "xxx"}, ...]
|
||||
*/
|
||||
List<HashMap<String, String>> printerFromSampleChartByDate(CommonQueryReq query);
|
||||
}
|
||||
@@ -0,0 +1,536 @@
|
||||
package com.ycwl.basic.clickhouse.service.impl;
|
||||
|
||||
import com.ycwl.basic.clickhouse.service.StatsQueryService;
|
||||
import com.ycwl.basic.mapper.TaskMapper;
|
||||
import com.ycwl.basic.model.mobile.statistic.req.CommonQueryReq;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* ClickHouse 统计数据查询服务实现
|
||||
* 当 clickhouse.enabled=true 时启用
|
||||
*
|
||||
* 注意:ClickHouse JDBC 驱动 0.6.x 对参数绑定支持有问题,
|
||||
* 因此使用字符串格式化方式构建 SQL(参数均为内部生成的数值或日期,无 SQL 注入风险)
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@ConditionalOnProperty(prefix = "clickhouse", name = "enabled", havingValue = "true")
|
||||
public class ClickHouseStatsQueryServiceImpl implements StatsQueryService {
|
||||
|
||||
private static final TimeZone CLICKHOUSE_TIMEZONE = TimeZone.getTimeZone("Asia/Shanghai");
|
||||
|
||||
/**
|
||||
* 创建日期格式化器(SimpleDateFormat 非线程安全,每次创建新实例)
|
||||
*/
|
||||
private SimpleDateFormat createDateFormat() {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
||||
sdf.setTimeZone(CLICKHOUSE_TIMEZONE);
|
||||
return sdf;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建日期时间格式化器
|
||||
*/
|
||||
private SimpleDateFormat createDateTimeFormat() {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
sdf.setTimeZone(CLICKHOUSE_TIMEZONE);
|
||||
return sdf;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
@Qualifier("clickHouseJdbcTemplate")
|
||||
private NamedParameterJdbcTemplate namedJdbcTemplate;
|
||||
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Autowired
|
||||
private TaskMapper taskMapper;
|
||||
|
||||
private JdbcTemplate getJdbcTemplate() {
|
||||
if (jdbcTemplate == null) {
|
||||
jdbcTemplate = namedJdbcTemplate.getJdbcTemplate();
|
||||
}
|
||||
return jdbcTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期时间为 ClickHouse 可识别的字符串
|
||||
*/
|
||||
private String formatDateTime(Date date) {
|
||||
return date != null ? "'" + createDateTimeFormat().format(date) + "'" : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期为 ClickHouse 可识别的字符串
|
||||
*/
|
||||
private String formatDate(Date date) {
|
||||
return date != null ? "'" + createDateFormat().format(date) + "'" : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接“进入景区”的 trace_id 子查询。
|
||||
* <p>
|
||||
* ClickHouse 上 t_stats_record 往往按时间分区/排序;给子查询补充时间范围可显著减少扫描量。
|
||||
*/
|
||||
private void appendEnterScenicTraceIdSubQuery(StringBuilder sql, Long scenicId, Date startTime, Date endTime) {
|
||||
sql.append("SELECT DISTINCT trace_id FROM t_stats_record ");
|
||||
sql.append("WHERE action = 'ENTER_SCENIC' ");
|
||||
if (scenicId != null) {
|
||||
sql.append("AND identifier = '").append(scenicId).append("' ");
|
||||
}
|
||||
if (startTime != null) {
|
||||
sql.append("AND create_time >= ").append(formatDateTime(startTime)).append(" ");
|
||||
}
|
||||
if (endTime != null) {
|
||||
sql.append("AND create_time <= ").append(formatDateTime(endTime)).append(" ");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer countPreviewVideoOfMember(CommonQueryReq query) {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT toInt32(uniqExact(s.member_id)) AS count ");
|
||||
sql.append("FROM t_stats_record r ");
|
||||
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
|
||||
sql.append("WHERE r.action = 'LOAD' ");
|
||||
sql.append("AND r.identifier = 'pages/videoSynthesis/buy' ");
|
||||
sql.append("AND r.trace_id IN (");
|
||||
appendEnterScenicTraceIdSubQuery(sql, query.getScenicId(), query.getStartTime(), query.getEndTime());
|
||||
sql.append(") ");
|
||||
sql.append("AND JSONExtractString(r.params, 'share') = '' ");
|
||||
if (query.getStartTime() != null) {
|
||||
sql.append("AND r.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
|
||||
}
|
||||
if (query.getEndTime() != null) {
|
||||
sql.append("AND r.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
|
||||
}
|
||||
|
||||
return getJdbcTemplate().queryForObject(sql.toString(), Integer.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer countScanCodeOfMember(CommonQueryReq query) {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT toInt32(uniqExact(s.member_id)) AS count ");
|
||||
sql.append("FROM t_stats_record r ");
|
||||
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
|
||||
sql.append("WHERE r.trace_id IN (");
|
||||
appendEnterScenicTraceIdSubQuery(sql, query.getScenicId(), query.getStartTime(), query.getEndTime());
|
||||
sql.append(") ");
|
||||
sql.append("AND r.action = 'LAUNCH' ");
|
||||
sql.append("AND JSONExtractInt(r.params, 'scene') IN (1047, 1048, 1049) ");
|
||||
if (query.getStartTime() != null) {
|
||||
sql.append("AND s.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
|
||||
}
|
||||
if (query.getEndTime() != null) {
|
||||
sql.append("AND s.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
|
||||
}
|
||||
|
||||
return getJdbcTemplate().queryForObject(sql.toString(), Integer.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer countPushOfMember(CommonQueryReq query) {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT toInt32(uniqExact(s.member_id)) AS count ");
|
||||
sql.append("FROM t_stats_record r ");
|
||||
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
|
||||
sql.append("WHERE r.trace_id IN (");
|
||||
appendEnterScenicTraceIdSubQuery(sql, query.getScenicId(), query.getStartTime(), query.getEndTime());
|
||||
sql.append(") ");
|
||||
sql.append("AND r.action = 'PERM_REQ' ");
|
||||
sql.append("AND r.identifier = 'NOTIFY' ");
|
||||
if (query.getStartTime() != null) {
|
||||
sql.append("AND r.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
|
||||
}
|
||||
if (query.getEndTime() != null) {
|
||||
sql.append("AND r.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
|
||||
}
|
||||
|
||||
return getJdbcTemplate().queryForObject(sql.toString(), Integer.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer countUploadFaceOfMember(CommonQueryReq query) {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT toInt32(uniqExact(s.member_id)) AS count ");
|
||||
sql.append("FROM t_stats_record r ");
|
||||
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
|
||||
sql.append("WHERE r.action = 'FACE_UPLOAD' ");
|
||||
sql.append("AND r.trace_id IN (");
|
||||
appendEnterScenicTraceIdSubQuery(sql, query.getScenicId(), query.getStartTime(), query.getEndTime());
|
||||
sql.append(") ");
|
||||
if (query.getStartTime() != null) {
|
||||
sql.append("AND s.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
|
||||
}
|
||||
if (query.getEndTime() != null) {
|
||||
sql.append("AND s.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
|
||||
}
|
||||
|
||||
return getJdbcTemplate().queryForObject(sql.toString(), Integer.class);
|
||||
}
|
||||
|
||||
private List<String> listFaceIdsWithUpload(CommonQueryReq query) {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT DISTINCT r.identifier FROM t_stats_record r ");
|
||||
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
|
||||
sql.append("WHERE r.action = 'FACE_UPLOAD' ");
|
||||
sql.append("AND r.trace_id IN (");
|
||||
appendEnterScenicTraceIdSubQuery(sql, query.getScenicId(), query.getStartTime(), query.getEndTime());
|
||||
sql.append(") ");
|
||||
if (query.getStartTime() != null) {
|
||||
sql.append("AND s.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
|
||||
}
|
||||
if (query.getEndTime() != null) {
|
||||
sql.append("AND s.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
|
||||
}
|
||||
|
||||
return getJdbcTemplate().queryForList(sql.toString(), String.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer countCompleteVideoOfMember(CommonQueryReq query) {
|
||||
List<String> faceIds = listFaceIdsWithUpload(query);
|
||||
if (faceIds == null || faceIds.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
return taskMapper.countCompletedTaskMembersByFaceIds(faceIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer countCompleteOfVideo(CommonQueryReq query) {
|
||||
List<String> faceIds = listFaceIdsWithUpload(query);
|
||||
if (faceIds == null || faceIds.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
return taskMapper.countCompletedTasksByFaceIds(faceIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer countTotalVisitorOfMember(CommonQueryReq query) {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT toInt32(uniqExact(s.member_id)) AS count ");
|
||||
sql.append("FROM t_stats_record r ");
|
||||
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
|
||||
sql.append("WHERE r.trace_id IN (");
|
||||
appendEnterScenicTraceIdSubQuery(sql, query.getScenicId(), query.getStartTime(), query.getEndTime());
|
||||
sql.append(") ");
|
||||
sql.append("AND r.action = 'LAUNCH' ");
|
||||
if (query.getStartTime() != null) {
|
||||
sql.append("AND s.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
|
||||
}
|
||||
if (query.getEndTime() != null) {
|
||||
sql.append("AND s.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
|
||||
}
|
||||
|
||||
return getJdbcTemplate().queryForObject(sql.toString(), Integer.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer countPreviewOfVideo(CommonQueryReq query) {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("WITH JSONExtractString(params, 'id') AS videoId, ");
|
||||
sql.append(" JSONExtractString(params, 'share') AS share ");
|
||||
sql.append("SELECT toInt32(uniqExact(videoId)) AS count ");
|
||||
sql.append("FROM t_stats_record r ");
|
||||
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
|
||||
sql.append("WHERE r.trace_id IN (");
|
||||
appendEnterScenicTraceIdSubQuery(sql, query.getScenicId(), query.getStartTime(), query.getEndTime());
|
||||
sql.append(") ");
|
||||
sql.append("AND r.action = 'LOAD' ");
|
||||
sql.append("AND r.identifier = 'pages/videoSynthesis/buy' ");
|
||||
sql.append("AND videoId != '' ");
|
||||
sql.append("AND share = '' ");
|
||||
if (query.getStartTime() != null) {
|
||||
sql.append("AND s.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
|
||||
}
|
||||
if (query.getEndTime() != null) {
|
||||
sql.append("AND s.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
|
||||
}
|
||||
|
||||
return getJdbcTemplate().queryForObject(sql.toString(), Integer.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Long> getBrokerIdListForUser(Long memberId, Date startTime, Date endTime) {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT toInt64(r.identifier) AS identifier ");
|
||||
sql.append("FROM t_stats_record r ");
|
||||
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
|
||||
sql.append("WHERE r.action = 'CODE_SCAN' ");
|
||||
sql.append(" AND s.member_id = ").append(memberId).append(" ");
|
||||
if (startTime != null) {
|
||||
sql.append(" AND r.create_time >= ").append(formatDateTime(startTime)).append(" ");
|
||||
}
|
||||
if (endTime != null) {
|
||||
sql.append(" AND r.create_time <= ").append(formatDateTime(endTime)).append(" ");
|
||||
}
|
||||
sql.append("GROUP BY r.identifier ");
|
||||
sql.append("ORDER BY max(r.create_time) DESC");
|
||||
|
||||
return getJdbcTemplate().queryForList(sql.toString(), Long.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getUserRecentEnterType(Long memberId, Date endTime) {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT JSONExtractInt(r.params, 'scene') AS scene ");
|
||||
sql.append("FROM t_stats_record r ");
|
||||
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
|
||||
sql.append("WHERE r.action = 'LAUNCH' ");
|
||||
sql.append(" AND s.member_id = ").append(memberId).append(" ");
|
||||
if (endTime != null) {
|
||||
sql.append(" AND r.create_time <= ").append(formatDateTime(endTime)).append(" ");
|
||||
}
|
||||
sql.append("ORDER BY r.create_time DESC LIMIT 1");
|
||||
|
||||
try {
|
||||
return getJdbcTemplate().queryForObject(sql.toString(), Long.class);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Long> getProjectIdListForUser(Long memberId, Date startTime, Date endTime) {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT toInt64(r.identifier) AS identifier ");
|
||||
sql.append("FROM t_stats_record r ");
|
||||
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
|
||||
sql.append("WHERE s.member_id = ").append(memberId).append(" ");
|
||||
sql.append(" AND r.action = 'ENTER_PROJECT' ");
|
||||
sql.append(" AND r.create_time < ").append(formatDateTime(endTime)).append(" ");
|
||||
sql.append(" AND r.create_time > ").append(formatDateTime(startTime)).append(" ");
|
||||
sql.append("ORDER BY r.create_time DESC LIMIT 1");
|
||||
|
||||
return getJdbcTemplate().queryForList(sql.toString(), Long.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer countBrokerScanCount(Long brokerId) {
|
||||
String sql = "SELECT count(1) AS count FROM t_stats_record " +
|
||||
"WHERE action = 'CODE_SCAN' AND identifier = '" + brokerId + "'";
|
||||
|
||||
return getJdbcTemplate().queryForObject(sql, Integer.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<HashMap<String, Object>> getDailyScanStats(Long brokerId, Date startTime, Date endTime) {
|
||||
SimpleDateFormat dateFormat = createDateFormat();
|
||||
String startDateStr = dateFormat.format(startTime);
|
||||
String endDateStr = dateFormat.format(endTime);
|
||||
String startDateTimeStr = "'" + startDateStr + " 00:00:00'";
|
||||
String endDateTimeStr = "'" + endDateStr + " 23:59:59'";
|
||||
|
||||
String sql = "SELECT toDate(create_time) AS date, count(DISTINCT id) AS scanCount " +
|
||||
"FROM t_stats_record " +
|
||||
"WHERE action = 'CODE_SCAN' " +
|
||||
" AND identifier = '" + brokerId + "' " +
|
||||
" AND create_time >= " + startDateTimeStr + " " +
|
||||
" AND create_time <= " + endDateTimeStr + " " +
|
||||
"GROUP BY toDate(create_time) " +
|
||||
"ORDER BY toDate(create_time)";
|
||||
|
||||
return getJdbcTemplate().query(sql, (rs, rowNum) -> {
|
||||
HashMap<String, Object> map = new HashMap<>();
|
||||
map.put("date", rs.getDate("date"));
|
||||
map.put("scanCount", rs.getLong("scanCount"));
|
||||
return map;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<HashMap<String, String>> scanCodeMemberChartByHour(CommonQueryReq query) {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT formatDateTime(toStartOfHour(s.create_time), '%m-%d %H') AS t, ");
|
||||
sql.append(" uniqExact(s.member_id) AS count ");
|
||||
sql.append("FROM t_stats_record r ");
|
||||
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
|
||||
sql.append("WHERE r.trace_id IN (");
|
||||
appendEnterScenicTraceIdSubQuery(sql, query.getScenicId(), query.getStartTime(), query.getEndTime());
|
||||
sql.append(") ");
|
||||
sql.append("AND r.action = 'LAUNCH' ");
|
||||
sql.append("AND JSONExtractInt(r.params, 'scene') IN (1047, 1048, 1049) ");
|
||||
sql.append("GROUP BY toStartOfHour(s.create_time) ");
|
||||
sql.append("ORDER BY toStartOfHour(s.create_time)");
|
||||
|
||||
List<HashMap<String, String>> rawData = getJdbcTemplate().query(sql.toString(), (rs, rowNum) -> {
|
||||
HashMap<String, String> map = new HashMap<>();
|
||||
map.put("t", rs.getString("t"));
|
||||
map.put("count", rs.getString("count"));
|
||||
return map;
|
||||
});
|
||||
|
||||
return fillHourSeries(rawData, query.getStartTime(), query.getEndTime());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<HashMap<String, String>> scanCodeMemberChartByDate(CommonQueryReq query) {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT formatDateTime(toStartOfDay(s.create_time), '%m-%d') AS t, ");
|
||||
sql.append(" uniqExact(s.member_id) AS count ");
|
||||
sql.append("FROM t_stats_record r ");
|
||||
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
|
||||
sql.append("WHERE r.trace_id IN (");
|
||||
appendEnterScenicTraceIdSubQuery(sql, query.getScenicId(), query.getStartTime(), query.getEndTime());
|
||||
sql.append(") ");
|
||||
sql.append("AND r.action = 'LAUNCH' ");
|
||||
sql.append("AND JSONExtractInt(r.params, 'scene') IN (1047, 1048, 1049) ");
|
||||
sql.append("GROUP BY toStartOfDay(s.create_time) ");
|
||||
sql.append("ORDER BY toStartOfDay(s.create_time)");
|
||||
|
||||
List<HashMap<String, String>> rawData = getJdbcTemplate().query(sql.toString(), (rs, rowNum) -> {
|
||||
HashMap<String, String> map = new HashMap<>();
|
||||
map.put("t", rs.getString("t"));
|
||||
map.put("count", rs.getString("count"));
|
||||
return map;
|
||||
});
|
||||
|
||||
return fillDateSeries(rawData, query.getStartTime(), query.getEndTime());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<HashMap<String, String>> printerFromSampleChartByHour(CommonQueryReq query) {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT formatDateTime(toStartOfHour(s.create_time), '%m-%d %H') AS t, ");
|
||||
sql.append(" uniqExact(s.member_id) AS count ");
|
||||
sql.append("FROM t_stats_record r ");
|
||||
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
|
||||
sql.append("WHERE r.action = 'LOAD' ");
|
||||
sql.append("AND r.identifier = 'pages/printer/hello' ");
|
||||
if (query.getScenicId() != null) {
|
||||
sql.append("AND JSONExtractString(r.params, 'scenicId') = '").append(query.getScenicId()).append("' ");
|
||||
}
|
||||
if (query.getStartTime() != null) {
|
||||
sql.append("AND r.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
|
||||
}
|
||||
if (query.getEndTime() != null) {
|
||||
sql.append("AND r.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
|
||||
}
|
||||
sql.append("GROUP BY toStartOfHour(s.create_time) ");
|
||||
sql.append("ORDER BY toStartOfHour(s.create_time)");
|
||||
|
||||
List<HashMap<String, String>> rawData = getJdbcTemplate().query(sql.toString(), (rs, rowNum) -> {
|
||||
HashMap<String, String> map = new HashMap<>();
|
||||
map.put("t", rs.getString("t"));
|
||||
map.put("count", rs.getString("count"));
|
||||
return map;
|
||||
});
|
||||
|
||||
return fillHourSeries(rawData, query.getStartTime(), query.getEndTime());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<HashMap<String, String>> printerFromSampleChartByDate(CommonQueryReq query) {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT formatDateTime(toStartOfDay(s.create_time), '%m-%d') AS t, ");
|
||||
sql.append(" uniqExact(s.member_id) AS count ");
|
||||
sql.append("FROM t_stats_record r ");
|
||||
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
|
||||
sql.append("WHERE r.action = 'LOAD' ");
|
||||
sql.append("AND r.identifier = 'pages/printer/hello' ");
|
||||
if (query.getScenicId() != null) {
|
||||
sql.append("AND JSONExtractString(r.params, 'scenicId') = '").append(query.getScenicId()).append("' ");
|
||||
}
|
||||
if (query.getStartTime() != null) {
|
||||
sql.append("AND r.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
|
||||
}
|
||||
if (query.getEndTime() != null) {
|
||||
sql.append("AND r.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
|
||||
}
|
||||
sql.append("GROUP BY toStartOfDay(s.create_time) ");
|
||||
sql.append("ORDER BY toStartOfDay(s.create_time)");
|
||||
|
||||
List<HashMap<String, String>> rawData = getJdbcTemplate().query(sql.toString(), (rs, rowNum) -> {
|
||||
HashMap<String, String> map = new HashMap<>();
|
||||
map.put("t", rs.getString("t"));
|
||||
map.put("count", rs.getString("count"));
|
||||
return map;
|
||||
});
|
||||
|
||||
return fillDateSeries(rawData, query.getStartTime(), query.getEndTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充小时序列,确保每个小时都有数据(缺失的填充为0)
|
||||
*/
|
||||
private List<HashMap<String, String>> fillHourSeries(List<HashMap<String, String>> rawData, Date startTime, Date endTime) {
|
||||
if (startTime == null || endTime == null) {
|
||||
return rawData;
|
||||
}
|
||||
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM-dd HH");
|
||||
LocalDateTime start = startTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime().truncatedTo(ChronoUnit.HOURS);
|
||||
LocalDateTime end = endTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime().truncatedTo(ChronoUnit.HOURS);
|
||||
|
||||
// 将原始数据转为 Map 以便快速查找
|
||||
Map<String, String> dataMap = rawData.stream()
|
||||
.collect(Collectors.toMap(
|
||||
m -> m.get("t"),
|
||||
m -> m.get("count"),
|
||||
(existing, replacement) -> existing
|
||||
));
|
||||
|
||||
List<HashMap<String, String>> result = new ArrayList<>();
|
||||
LocalDateTime current = start;
|
||||
while (!current.isAfter(end)) {
|
||||
String timeKey = current.format(formatter);
|
||||
HashMap<String, String> item = new HashMap<>();
|
||||
item.put("t", timeKey);
|
||||
item.put("count", dataMap.getOrDefault(timeKey, "0"));
|
||||
result.add(item);
|
||||
current = current.plusHours(1);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充日期序列,确保每天都有数据(缺失的填充为0)
|
||||
*/
|
||||
private List<HashMap<String, String>> fillDateSeries(List<HashMap<String, String>> rawData, Date startTime, Date endTime) {
|
||||
if (startTime == null || endTime == null) {
|
||||
return rawData;
|
||||
}
|
||||
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM-dd");
|
||||
LocalDate start = startTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
|
||||
LocalDate end = endTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
|
||||
|
||||
// 将原始数据转为 Map 以便快速查找
|
||||
Map<String, String> dataMap = rawData.stream()
|
||||
.collect(Collectors.toMap(
|
||||
m -> m.get("t"),
|
||||
m -> m.get("count"),
|
||||
(existing, replacement) -> existing
|
||||
));
|
||||
|
||||
List<HashMap<String, String>> result = new ArrayList<>();
|
||||
LocalDate current = start;
|
||||
while (!current.isAfter(end)) {
|
||||
String timeKey = current.format(formatter);
|
||||
HashMap<String, String> item = new HashMap<>();
|
||||
item.put("t", timeKey);
|
||||
item.put("count", dataMap.getOrDefault(timeKey, "0"));
|
||||
result.add(item);
|
||||
current = current.plusDays(1);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.ycwl.basic.clickhouse.service.impl;
|
||||
|
||||
import com.ycwl.basic.clickhouse.service.StatsQueryService;
|
||||
import com.ycwl.basic.mapper.StatisticsMapper;
|
||||
import com.ycwl.basic.model.mobile.statistic.req.CommonQueryReq;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* MySQL 统计数据查询服务实现
|
||||
* 当 clickhouse.enabled 未启用时使用此实现(兜底)
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@ConditionalOnProperty(prefix = "clickhouse", name = "enabled", havingValue = "false", matchIfMissing = true)
|
||||
public class MySqlStatsQueryServiceImpl implements StatsQueryService {
|
||||
|
||||
@Autowired
|
||||
private StatisticsMapper statisticsMapper;
|
||||
|
||||
@Override
|
||||
public Integer countPreviewVideoOfMember(CommonQueryReq query) {
|
||||
return statisticsMapper.countPreviewVideoOfMember(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer countScanCodeOfMember(CommonQueryReq query) {
|
||||
return statisticsMapper.countScanCodeOfMember(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer countPushOfMember(CommonQueryReq query) {
|
||||
return statisticsMapper.countPushOfMember(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer countUploadFaceOfMember(CommonQueryReq query) {
|
||||
return statisticsMapper.countUploadFaceOfMember(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer countCompleteVideoOfMember(CommonQueryReq query) {
|
||||
return statisticsMapper.countCompleteVideoOfMember(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer countCompleteOfVideo(CommonQueryReq query) {
|
||||
return statisticsMapper.countCompleteOfVideo(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer countTotalVisitorOfMember(CommonQueryReq query) {
|
||||
return statisticsMapper.countTotalVisitorOfMember(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer countPreviewOfVideo(CommonQueryReq query) {
|
||||
return statisticsMapper.countPreviewOfVideo(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Long> getBrokerIdListForUser(Long memberId, Date startTime, Date endTime) {
|
||||
return statisticsMapper.getBrokerIdListForUser(memberId, startTime, endTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getUserRecentEnterType(Long memberId, Date endTime) {
|
||||
return statisticsMapper.getUserRecentEnterType(memberId, endTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Long> getProjectIdListForUser(Long memberId, Date startTime, Date endTime) {
|
||||
return statisticsMapper.getProjectIdListForUser(memberId, startTime, endTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer countBrokerScanCount(Long brokerId) {
|
||||
return statisticsMapper.countBrokerScanCount(brokerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<HashMap<String, Object>> getDailyScanStats(Long brokerId, Date startTime, Date endTime) {
|
||||
return statisticsMapper.getDailyScanStats(brokerId, startTime, endTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<HashMap<String, String>> scanCodeMemberChartByHour(CommonQueryReq query) {
|
||||
return statisticsMapper.scanCodeMemberChartByHour(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<HashMap<String, String>> scanCodeMemberChartByDate(CommonQueryReq query) {
|
||||
return statisticsMapper.scanCodeMemberChartByDate(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<HashMap<String, String>> printerFromSampleChartByHour(CommonQueryReq query) {
|
||||
return statisticsMapper.printerFromSampleChartByHour(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<HashMap<String, String>> printerFromSampleChartByDate(CommonQueryReq query) {
|
||||
return statisticsMapper.printerFromSampleChartByDate(query);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.ycwl.basic.config;
|
||||
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
/**
|
||||
* ClickHouse 数据源配置
|
||||
* 用于 t_stats 和 t_stats_record 表的查询
|
||||
*
|
||||
* 使用 NamedParameterJdbcTemplate 而非 MyBatis,以避免干扰 MyBatis-Plus 的自动配置
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnProperty(prefix = "clickhouse", name = "enabled", havingValue = "true")
|
||||
public class ClickHouseDataSourceConfig {
|
||||
|
||||
/**
|
||||
* ClickHouse 数据源(非 Primary)
|
||||
*/
|
||||
@Bean(name = "clickHouseDataSource")
|
||||
@ConfigurationProperties(prefix = "clickhouse.datasource")
|
||||
public DataSource clickHouseDataSource() {
|
||||
return new HikariDataSource();
|
||||
}
|
||||
|
||||
@Bean(name = "clickHouseJdbcTemplate")
|
||||
public NamedParameterJdbcTemplate clickHouseJdbcTemplate(
|
||||
@Qualifier("clickHouseDataSource") DataSource dataSource) {
|
||||
return new NamedParameterJdbcTemplate(dataSource);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
package com.ycwl.basic.config;
|
||||
|
||||
import feign.Logger;
|
||||
import feign.RequestInterceptor;
|
||||
import feign.codec.ErrorDecoder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class FeignConfig {
|
||||
|
||||
@Bean
|
||||
public Logger.Level feignLoggerLevel() {
|
||||
return Logger.Level.BASIC;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RequestInterceptor requestInterceptor() {
|
||||
return requestTemplate -> {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes != null) {
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
|
||||
// 传递认证头
|
||||
String authorization = request.getHeader("Authorization");
|
||||
if (authorization != null) {
|
||||
requestTemplate.header("Authorization", authorization);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ErrorDecoder errorDecoder() {
|
||||
return new FeignErrorDecoder();
|
||||
}
|
||||
|
||||
public static class FeignErrorDecoder implements ErrorDecoder {
|
||||
private final ErrorDecoder defaultErrorDecoder = new Default();
|
||||
|
||||
@Override
|
||||
public Exception decode(String methodKey, feign.Response response) {
|
||||
log.error("Feign调用失败: method={}, status={}, reason={}",
|
||||
methodKey, response.status(), response.reason());
|
||||
|
||||
if (response.status() >= 400 && response.status() < 500) {
|
||||
// 4xx错误,客户端错误
|
||||
return new RuntimeException("客户端请求错误: " + response.reason());
|
||||
} else if (response.status() >= 500) {
|
||||
// 5xx错误,服务器错误
|
||||
return new RuntimeException("服务器内部错误: " + response.reason());
|
||||
}
|
||||
|
||||
return defaultErrorDecoder.decode(methodKey, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,16 @@
|
||||
package com.ycwl.basic.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
@Configuration
|
||||
public class JacksonConfiguration {
|
||||
|
||||
@@ -13,6 +19,16 @@ public class JacksonConfiguration {
|
||||
return builder -> {
|
||||
// 把 Long 类型序列化为 String
|
||||
builder.serializerByType(Long.class, ToStringSerializer.instance);
|
||||
|
||||
// 添加 JavaTimeModule 以支持 Java 8 时间类型
|
||||
builder.simpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
builder.serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
|
||||
builder.deserializers(new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JavaTimeModule javaTimeModule() {
|
||||
return new JavaTimeModule();
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.ycwl.basic.config;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
/**
|
||||
* MySQL 主数据源配置
|
||||
*
|
||||
* 当 ClickHouse 启用时,需要显式配置 MySQL 数据源并标记为 @Primary,
|
||||
* 以确保 MyBatis-Plus 和其他组件使用正确的数据源
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnProperty(prefix = "clickhouse", name = "enabled", havingValue = "true")
|
||||
public class MySqlPrimaryDataSourceConfig {
|
||||
|
||||
/**
|
||||
* MySQL 数据源属性
|
||||
*/
|
||||
@Primary
|
||||
@Bean
|
||||
@ConfigurationProperties(prefix = "spring.datasource")
|
||||
public DataSourceProperties mysqlDataSourceProperties() {
|
||||
return new DataSourceProperties();
|
||||
}
|
||||
|
||||
/**
|
||||
* MySQL 主数据源
|
||||
* 使用 @Primary 确保这是默认数据源
|
||||
*/
|
||||
@Primary
|
||||
@Bean(name = "dataSource")
|
||||
public DataSource mysqlDataSource(DataSourceProperties properties) {
|
||||
return properties.initializeDataSourceBuilder().build();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package com.ycwl.basic.config;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import com.ycwl.basic.interceptor.AuthInterceptor;
|
||||
import com.ycwl.basic.stats.interceptor.StatsInterceptor;
|
||||
import com.ycwl.basic.puzzle.edge.interceptor.PuzzleEdgeWorkerIpInterceptor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -25,20 +25,19 @@ import java.util.List;
|
||||
*/
|
||||
@Configuration
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
@Autowired
|
||||
private AuthInterceptor authInterceptor;
|
||||
@Autowired
|
||||
private StatsInterceptor statsInterceptor;
|
||||
private PuzzleEdgeWorkerIpInterceptor puzzleEdgeWorkerIpInterceptor;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(puzzleEdgeWorkerIpInterceptor)
|
||||
.addPathPatterns("/puzzle/render/v1/**");
|
||||
registry.addInterceptor(authInterceptor)
|
||||
// 拦截除指定接口外的所有请求,通过判断 注解 来决定是否需要做登录验证
|
||||
.addPathPatterns("/**")
|
||||
.excludePathPatterns("/swagger-resources/**", "/webjars/**", "/v2/api-docs", "/doc.html/**", "/error", "/");
|
||||
registry.addInterceptor(statsInterceptor)
|
||||
.addPathPatterns("/api/mobile/**");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
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,112 @@
|
||||
package com.ycwl.basic.controller;
|
||||
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckReqDTO;
|
||||
import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckRespDTO;
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查视频是否已被购买
|
||||
* 购买条件:
|
||||
* 1. 直接购买视频(order_item中goods_type=0且goods_id=视频id)
|
||||
* 2. 购买整个模板(order的face_id与video关联的task的face_id相同,goods_type=-1,goods_id为video的templateId)
|
||||
*
|
||||
* @param reqDTO 查询条件
|
||||
* @return 购买状态及订单ID列表
|
||||
*/
|
||||
@PostMapping("/check-purchase")
|
||||
public ApiResponse<VideoPurchaseCheckRespDTO> checkVideoPurchase(@RequestBody VideoPurchaseCheckReqDTO reqDTO) {
|
||||
log.info("检查视频购买状态,videoId: {}", reqDTO.getVideoId());
|
||||
VideoPurchaseCheckRespDTO respDTO = videoReviewService.checkVideoPurchase(reqDTO);
|
||||
return ApiResponse.success(respDTO);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -59,10 +58,6 @@ public class LyCompatibleController {
|
||||
@Autowired
|
||||
private VideoRepository videoRepository;
|
||||
@Autowired
|
||||
private VideoMapper videoMapper;
|
||||
@Autowired
|
||||
private TaskTaskServiceImpl taskTaskServiceImpl;
|
||||
@Autowired
|
||||
private RedisTemplate<String, String> redisTemplate;
|
||||
@Autowired
|
||||
private VideoTaskRepository videoTaskRepository;
|
||||
@@ -114,7 +109,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());
|
||||
}
|
||||
@@ -208,25 +203,31 @@ public class LyCompatibleController {
|
||||
return response;
|
||||
}
|
||||
List<Map<String, Object>> videoList = collect.get(0).stream().collect(Collectors.groupingBy(ContentPageVO::getTemplateId))
|
||||
.values().stream().map(contentPageVOs -> {
|
||||
ContentPageVO contentPageVO = contentPageVOs.getFirst();
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
.values().stream()
|
||||
.map(contentPageVOs -> {
|
||||
ContentPageVO contentPageVO = contentPageVOs.stream().filter(vo -> vo.getContentId() != null).findFirst().orElse(null);
|
||||
if (contentPageVO == null) {
|
||||
return null;
|
||||
}
|
||||
VideoEntity videoRespVO = videoRepository.getVideo(contentPageVO.getContentId());
|
||||
if (videoRespVO == null) {
|
||||
return null;
|
||||
}
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("id", videoRespVO.getId().toString());
|
||||
map.put("task_id", videoRespVO.getTaskId().toString());
|
||||
TaskEntity task = videoTaskRepository.getTaskById(videoRespVO.getTaskId());
|
||||
if (task != null) {
|
||||
map.put("face_id", String.valueOf(task.getFaceId()));
|
||||
if (videoRespVO.getFaceId() != null) {
|
||||
map.put("face_id", String.valueOf(videoRespVO.getFaceId()));
|
||||
}
|
||||
map.put("template_cover_image", contentPageVO.getTemplateCoverUrl());
|
||||
Date taskShotDate = taskTaskServiceImpl.getTaskShotDate(videoRespVO.getTaskId());
|
||||
Date taskShotDate = videoTaskRepository.getTaskShotDate(videoRespVO.getTaskId());
|
||||
map.put("shoottime", DateUtil.format(taskShotDate, "yyyy-MM-dd HH:mm"));
|
||||
map.put("openid", openId);
|
||||
map.put("scenicname", contentPageVO.getScenicName());
|
||||
map.put("title", contentPageVO.getName());
|
||||
map.put("ossurldm", videoRespVO.getVideoUrl());
|
||||
return map;
|
||||
}).collect(Collectors.toList());
|
||||
}).filter(java.util.Objects::nonNull).collect(Collectors.toList());
|
||||
GoodsReqQuery goodsReqQuery = new GoodsReqQuery();
|
||||
goodsReqQuery.setFaceId(faceVO.getId());
|
||||
goodsReqQuery.setSourceType(1);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.ycwl.basic.controller.mobile;
|
||||
|
||||
import com.ycwl.basic.model.mobile.claim.ClaimReq;
|
||||
import com.ycwl.basic.model.mobile.claim.ClaimResp;
|
||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||
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.integration.common.manager.ScenicConfigManager;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import lombok.AllArgsConstructor;
|
||||
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;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/mobile/claim/v1")
|
||||
@AllArgsConstructor
|
||||
public class AppClaimController {
|
||||
private final FaceRepository faceRepository;
|
||||
private final ScenicRepository scenicRepository;
|
||||
private final VoucherPrintService voucherPrintService;
|
||||
private final ICouponService couponService;
|
||||
|
||||
|
||||
@PostMapping("tryClaim")
|
||||
public ApiResponse<ClaimResp> tryClaim(@RequestBody ClaimReq req) {
|
||||
FaceEntity face = faceRepository.getFace(req.getFaceId());
|
||||
if (face == null) {
|
||||
return ApiResponse.fail("请选择人脸");
|
||||
}
|
||||
ClaimResp claimResp = new ClaimResp();
|
||||
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
|
||||
if (Boolean.TRUE.equals(scenicConfig.getBoolean("voucher_enable"))) {
|
||||
// 可以领券
|
||||
VoucherPrintResp voucherPrintResp = voucherPrintService.queryPrintedVoucher(face.getId());
|
||||
if (voucherPrintResp == null) {
|
||||
// 打印
|
||||
if (req.getMorphId() != null) {
|
||||
voucherPrintResp = voucherPrintService.printVoucherTicket(new VoucherPrintReq(face.getId(), req.getMorphId(), face.getScenicId()));
|
||||
}
|
||||
}
|
||||
if (voucherPrintResp != null) {
|
||||
claimResp.setHasCoupon(false);
|
||||
claimResp.setHasPrint(true);
|
||||
claimResp.setPrintCode(voucherPrintResp.getCode());
|
||||
claimResp.setPrintType(voucherPrintResp.getType());
|
||||
return ApiResponse.success(claimResp);
|
||||
}
|
||||
}
|
||||
if (Boolean.TRUE.equals(scenicConfig.getBoolean("booking_enable"))) {
|
||||
VoucherPrintResp voucherPrintResp = voucherPrintService.queryPrintedVoucher(face.getId());
|
||||
if (voucherPrintResp == null) {
|
||||
// 打印
|
||||
if (req.getMorphId() != null) {
|
||||
voucherPrintResp = voucherPrintService.printBookingTicket(new VoucherPrintReq(face.getId(), req.getMorphId(), face.getScenicId()));
|
||||
}
|
||||
}
|
||||
if (voucherPrintResp != null) {
|
||||
claimResp.setHasCoupon(false);
|
||||
claimResp.setHasPrint(true);
|
||||
claimResp.setPrintCode(voucherPrintResp.getCode());
|
||||
claimResp.setPrintType(voucherPrintResp.getType());
|
||||
return ApiResponse.success(claimResp);
|
||||
}
|
||||
}
|
||||
if (req.getType() != null) {
|
||||
// 第几次进入
|
||||
Integer couponId = scenicConfig.getInteger("coupon_id_for_type_" + req.getType());
|
||||
if (couponId != null) {
|
||||
// 可以领券
|
||||
CouponClaimRequest request = new CouponClaimRequest(face.getMemberId(), Long.valueOf(couponId));
|
||||
CouponClaimResult claimResult = couponService.claimCoupon(request);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ApiResponse.fail("异常");
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package com.ycwl.basic.controller.mobile;
|
||||
|
||||
import com.ycwl.basic.constant.BaseContextHandler;
|
||||
import com.ycwl.basic.model.mobile.coupon.req.ClaimCouponReq;
|
||||
import com.ycwl.basic.model.pc.coupon.entity.CouponEntity;
|
||||
import com.ycwl.basic.model.pc.couponRecord.entity.CouponRecordEntity;
|
||||
import com.ycwl.basic.service.mobile.AppCouponRecordService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/mobile/coupon/v1")
|
||||
public class AppCouponController {
|
||||
|
||||
@Autowired
|
||||
private AppCouponRecordService appCouponRecordService;
|
||||
|
||||
/**
|
||||
* 根据memberId、faceId和type查找优惠券记录
|
||||
*/
|
||||
@GetMapping("/record")
|
||||
public ApiResponse<CouponRecordEntity> getCouponRecords(
|
||||
@RequestParam Long faceId,
|
||||
@RequestParam Integer type) {
|
||||
CouponRecordEntity record = appCouponRecordService.queryByMemberIdAndFaceIdAndType(Long.valueOf(BaseContextHandler.getUserId()), faceId, type);
|
||||
return ApiResponse.success(record);
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取优惠券
|
||||
*/
|
||||
@PostMapping("/claim")
|
||||
public ApiResponse<CouponEntity> claimCoupon(@RequestBody ClaimCouponReq request) {
|
||||
request.setMemberId(Long.valueOf(BaseContextHandler.getUserId()));
|
||||
try {
|
||||
CouponEntity coupon = appCouponRecordService.claimCoupon(
|
||||
request.getMemberId(),
|
||||
request.getFaceId(),
|
||||
request.getType()
|
||||
);
|
||||
return ApiResponse.success(coupon);
|
||||
} catch (RuntimeException e) {
|
||||
return ApiResponse.fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,17 +59,23 @@ 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);
|
||||
}
|
||||
|
||||
@GetMapping("/{faceId}")
|
||||
public ApiResponse<FaceRespVO> getById(@PathVariable("faceId") Long faceId) {
|
||||
return faceService.getById(faceId);
|
||||
public ApiResponse<FaceEntity> getById(@PathVariable("faceId") Long faceId) {
|
||||
FaceEntity face = faceRepository.getFace(faceId);
|
||||
return ApiResponse.success(face);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{faceId}")
|
||||
public ApiResponse<String> deleteFace(@PathVariable("faceId") Long faceId) {
|
||||
FaceEntity face = faceRepository.getFace(faceId);
|
||||
if (face == null) {
|
||||
throw new BaseException("人脸数据不存在");
|
||||
}
|
||||
|
||||
return faceService.deleteFace(faceId);
|
||||
}
|
||||
|
||||
@@ -80,9 +96,44 @@ AppFaceController {
|
||||
// 绑定人脸
|
||||
@PostMapping("/{faceId}/bind")
|
||||
public ApiResponse<String> bind(@PathVariable Long faceId) {
|
||||
JwtInfo worker = JwtTokenUtil.getWorker();
|
||||
Long userId = worker.getUserId();
|
||||
faceService.bindFace(faceId, userId);
|
||||
// dummy item
|
||||
faceService.matchFaceId(faceId, true);
|
||||
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());
|
||||
}
|
||||
|
||||
// 源素材(原片/照片)商品列表
|
||||
@@ -45,12 +53,6 @@ public class AppGoodsController {
|
||||
return ApiResponse.success(count);
|
||||
}
|
||||
|
||||
@PostMapping("/sourceGoodsList/preview")
|
||||
public ApiResponse<List<GoodsUrlVO>> sourceGoodsListPreview(@RequestBody GoodsReqQuery query) {
|
||||
List<GoodsUrlVO> goodsUrlList = goodsService.sourceGoodsListPreview(query);
|
||||
return ApiResponse.success(goodsUrlList);
|
||||
}
|
||||
|
||||
@PostMapping("/sourceGoodsList/download")
|
||||
public ApiResponse<List<GoodsUrlVO>> sourceGoodsListDownload(@RequestBody GoodsReqQuery query) {
|
||||
List<GoodsUrlVO> goodsUrlList = goodsService.sourceGoodsListDownload(query);
|
||||
@@ -96,7 +98,18 @@ public class AppGoodsController {
|
||||
// 查询用户当前景区的具体模版视频合成任务状态 1 合成中 2 合成成功
|
||||
@GetMapping("/task/face/{faceId}/template/{templateId}")
|
||||
public ApiResponse<VideoTaskStatusVO> getTemplateTaskStatus(@PathVariable("faceId") Long faceId, @PathVariable("templateId") Long templateId) {
|
||||
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.ycwl.basic.annotation.IgnoreToken;
|
||||
import com.ycwl.basic.constant.BaseContextHandler;
|
||||
import com.ycwl.basic.model.mobile.weChat.DTO.WeChatUserInfoDTO;
|
||||
import com.ycwl.basic.model.mobile.weChat.DTO.WeChatUserInfoUpdateDTO;
|
||||
import com.ycwl.basic.model.pc.member.resp.MemberRespVO;
|
||||
@@ -67,7 +68,8 @@ public class AppMemberController {
|
||||
// 修改用户信息
|
||||
@PostMapping("/update")
|
||||
public ApiResponse<?> update(@RequestBody WeChatUserInfoUpdateDTO userInfoUpdateDTO) {
|
||||
return memberService.update(userInfoUpdateDTO);
|
||||
Long userId = Long.parseLong(BaseContextHandler.getUserId());
|
||||
return memberService.update(userId, userInfoUpdateDTO);
|
||||
}
|
||||
|
||||
// 新增或修改景区服务通知状态
|
||||
|
||||
@@ -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("该套餐暂未开放购买");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,435 @@
|
||||
package com.ycwl.basic.controller.mobile;
|
||||
|
||||
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.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;
|
||||
import com.ycwl.basic.pricing.dto.*;
|
||||
import com.ycwl.basic.pricing.service.IPriceCalculationService;
|
||||
import com.ycwl.basic.service.pc.FaceService;
|
||||
import com.ycwl.basic.service.PriceCacheService;
|
||||
import com.ycwl.basic.model.pc.face.resp.FaceRespVO;
|
||||
import com.ycwl.basic.dto.MobileOrderRequest;
|
||||
import com.ycwl.basic.order.service.IOrderService;
|
||||
import com.ycwl.basic.order.dto.OrderV2DetailResponse;
|
||||
import com.ycwl.basic.order.dto.OrderV2ListResponse;
|
||||
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.order.exception.DuplicatePurchaseException;
|
||||
import com.ycwl.basic.order.factory.DuplicatePurchaseCheckerFactory;
|
||||
import com.ycwl.basic.order.strategy.DuplicateCheckContext;
|
||||
import com.ycwl.basic.order.strategy.IDuplicatePurchaseChecker;
|
||||
import com.ycwl.basic.product.capability.DuplicateCheckStrategy;
|
||||
import com.ycwl.basic.product.service.IProductTypeCapabilityService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 移动端订单控制器V2
|
||||
* 包含价格查询和订单管理功能
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/mobile/order/v2")
|
||||
@RequiredArgsConstructor
|
||||
public class AppOrderV2Controller {
|
||||
|
||||
private final IPriceCalculationService priceCalculationService;
|
||||
private final FaceService faceService;
|
||||
private final PriceCacheService priceCacheService;
|
||||
private final IOrderService orderService;
|
||||
private final OrderService oldOrderService;
|
||||
private final SourceMapper sourceMapper;
|
||||
private final VideoMapper videoMapper;
|
||||
private final VideoTaskRepository videoTaskRepository;
|
||||
private final TemplateRepository templateRepository;
|
||||
private final VideoRepository videoRepository;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
private final IProductTypeCapabilityService productTypeCapabilityService;
|
||||
private final DuplicatePurchaseCheckerFactory duplicatePurchaseCheckerFactory;
|
||||
|
||||
/**
|
||||
* 移动端价格计算
|
||||
* 集成Redis缓存机制,提升查询性能
|
||||
*/
|
||||
@PostMapping("/calculate")
|
||||
public ApiResponse<PriceCalculationResult> calculatePrice(@RequestBody MobilePriceCalculationRequest request) {
|
||||
// 获取当前登录用户ID
|
||||
String currentUserIdStr = BaseContextHandler.getUserId();
|
||||
if (currentUserIdStr == null) {
|
||||
log.warn("移动端价格计算:用户未登录");
|
||||
return ApiResponse.fail("用户未登录");
|
||||
}
|
||||
|
||||
Long currentUserId = Long.valueOf(currentUserIdStr);
|
||||
log.info("移动端价格计算请求: userId={}, faceId={}, products={}",
|
||||
currentUserId, request.getFaceId(), request.getProducts().size());
|
||||
|
||||
// 验证faceId参数
|
||||
if (request.getFaceId() == null) {
|
||||
log.warn("移动端价格计算:faceId参数缺失");
|
||||
// return ApiResponse.fail("faceId参数不能为空");
|
||||
// 兼容:兼容旧版本
|
||||
ProductItem productItem = request.getProducts().getFirst();
|
||||
switch (productItem.getProductType()) {
|
||||
case VLOG_VIDEO -> {
|
||||
VideoEntity video = videoRepository.getVideo(Long.valueOf(productItem.getProductId()));
|
||||
request.setFaceId(video.getFaceId());
|
||||
}
|
||||
case RECORDING_SET, PHOTO_SET, AI_CAM_PHOTO_SET -> request.setFaceId(Long.valueOf(productItem.getProductId()));
|
||||
}
|
||||
}
|
||||
|
||||
// 查询人脸信息进行权限验证
|
||||
ApiResponse<FaceRespVO> faceResponse = faceService.getById(request.getFaceId());
|
||||
if (!faceResponse.isSuccess() || faceResponse.getData() == null) {
|
||||
log.warn("移动端价格计算:人脸信息不存在, faceId={}", request.getFaceId());
|
||||
return ApiResponse.fail("人脸信息不存在");
|
||||
}
|
||||
|
||||
FaceRespVO face = faceResponse.getData();
|
||||
Long scenicId = face.getScenicId();
|
||||
|
||||
request.getProducts().forEach(product -> {
|
||||
// 获取商品的重复检查策略
|
||||
DuplicateCheckStrategy strategy = productTypeCapabilityService
|
||||
.getDuplicateCheckStrategy(product.getProductType().name());
|
||||
|
||||
boolean hasPurchasedFlag;
|
||||
|
||||
switch (product.getProductType()) {
|
||||
case VLOG_VIDEO:
|
||||
List<MemberVideoEntity> videoEntities = videoMapper.listRelationByFaceAndTemplate(face.getId(), Long.valueOf(product.getProductId()));
|
||||
if (videoEntities != null && !videoEntities.isEmpty()) {
|
||||
product.setQuantity(videoTaskRepository.getTaskLensNum(videoEntities.getFirst().getTaskId()));
|
||||
} else {
|
||||
product.setQuantity(1);
|
||||
}
|
||||
break;
|
||||
case RECORDING_SET:
|
||||
case PHOTO_SET:
|
||||
SourceReqQuery sourceReqQuery = new SourceReqQuery();
|
||||
sourceReqQuery.setMemberId(currentUserId);
|
||||
sourceReqQuery.setType(product.getProductType() == ProductType.RECORDING_SET ? 1 : 2);
|
||||
sourceReqQuery.setFaceId(face.getId());
|
||||
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;
|
||||
}
|
||||
|
||||
// 使用 DuplicatePurchaseChecker 检查是否已购买
|
||||
hasPurchasedFlag = checkIfPurchased(strategy, currentUserId, String.valueOf(scenicId),
|
||||
product.getProductType().name(), product.getProductId(), face.getId());
|
||||
|
||||
// 设置是否已购买标识
|
||||
product.setHasPurchased(hasPurchasedFlag);
|
||||
});
|
||||
|
||||
// 转换为标准价格计算请求
|
||||
PriceCalculationRequest standardRequest = request.toStandardRequest(currentUserId, scenicId);
|
||||
|
||||
// 执行价格计算
|
||||
PriceCalculationResult result = priceCalculationService.calculatePrice(standardRequest);
|
||||
|
||||
// 设置是否已购买标识(基于请求中的商品 hasPurchased 判断)
|
||||
// 只要有一个商品 hasPurchased = true,则整体 isPurchased = true
|
||||
boolean isPurchased = request.getProducts().stream()
|
||||
.anyMatch(product -> Boolean.TRUE.equals(product.getHasPurchased()));
|
||||
result.setIsPurchased(isPurchased);
|
||||
|
||||
// 将计算结果缓存到Redis
|
||||
String cacheKey = priceCacheService.cachePriceResult(currentUserId, scenicId, request.getProducts(), result);
|
||||
|
||||
log.info("移动端价格计算完成: userId={}, scenicId={}, originalAmount={}, finalAmount={}, cacheKey={}",
|
||||
currentUserId, scenicId, result.getOriginalAmount(), result.getFinalAmount(), cacheKey);
|
||||
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动端下单接口
|
||||
* 验证价格缓存有效性,确保5分钟内使用缓存价格下单
|
||||
*/
|
||||
@PostMapping("/add")
|
||||
public ApiResponse<String> addOrder(@RequestBody MobileOrderRequest request) {
|
||||
// 获取当前登录用户ID
|
||||
String currentUserIdStr = BaseContextHandler.getUserId();
|
||||
if (currentUserIdStr == null) {
|
||||
log.warn("移动端下单:用户未登录");
|
||||
return ApiResponse.fail("用户未登录");
|
||||
}
|
||||
|
||||
Long currentUserId = Long.valueOf(currentUserIdStr);
|
||||
log.info("移动端下单请求: userId={}, faceId={}, products={}, expectedFinalAmount={}",
|
||||
currentUserId, request.getFaceId(), request.getProducts().size(), request.getExpectedFinalAmount());
|
||||
|
||||
// 验证必填参数
|
||||
if (request.getFaceId() == null) {
|
||||
log.warn("移动端下单:faceId参数缺失");
|
||||
return ApiResponse.fail("faceId参数不能为空");
|
||||
}
|
||||
|
||||
if (request.getProducts() == null || request.getProducts().isEmpty()) {
|
||||
log.warn("移动端下单:商品列表为空");
|
||||
return ApiResponse.fail("商品列表不能为空");
|
||||
}
|
||||
|
||||
if (request.getExpectedFinalAmount() == null) {
|
||||
log.warn("移动端下单:预期价格参数缺失");
|
||||
return ApiResponse.fail("预期价格不能为空");
|
||||
}
|
||||
|
||||
// 查询人脸信息进行权限验证
|
||||
ApiResponse<FaceRespVO> faceResponse = faceService.getById(request.getFaceId());
|
||||
if (!faceResponse.isSuccess() || faceResponse.getData() == null) {
|
||||
log.warn("移动端下单:人脸信息不存在, faceId={}", request.getFaceId());
|
||||
return ApiResponse.fail("人脸信息不存在");
|
||||
}
|
||||
|
||||
FaceRespVO face = faceResponse.getData();
|
||||
Long scenicId = face.getScenicId();
|
||||
|
||||
// 验证并消费价格缓存(一次性使用)
|
||||
PriceCalculationResult cachedResult = priceCacheService.validateAndConsumePriceCache(
|
||||
currentUserId, scenicId, request.getProducts());
|
||||
|
||||
if (cachedResult == null) {
|
||||
log.warn("移动端下单:价格缓存已过期或不存在, userId={}, scenicId={}", currentUserId, scenicId);
|
||||
return ApiResponse.fail("请重新下单!");
|
||||
}
|
||||
|
||||
// 验证价格是否匹配
|
||||
if (cachedResult.getFinalAmount().compareTo(request.getExpectedFinalAmount()) != 0) {
|
||||
log.warn("移动端下单:价格不匹配, cached={}, expected={}, userId={}, scenicId={}",
|
||||
cachedResult.getFinalAmount(), request.getExpectedFinalAmount(), currentUserId, scenicId);
|
||||
return ApiResponse.fail("价格信息变化,请退出后重新查询价格!");
|
||||
}
|
||||
|
||||
// 验证原价是否匹配(可选)
|
||||
if (request.getExpectedOriginalAmount() != null &&
|
||||
cachedResult.getOriginalAmount().compareTo(request.getExpectedOriginalAmount()) != 0) {
|
||||
log.warn("移动端下单:原价不匹配, cached={}, expected={}, userId={}, scenicId={}",
|
||||
cachedResult.getOriginalAmount(), request.getExpectedOriginalAmount(), currentUserId, scenicId);
|
||||
return ApiResponse.fail("原价信息不匹配,请重新查询价格后再下单");
|
||||
}
|
||||
|
||||
log.info("价格缓存验证通过: userId={}, scenicId={}, finalAmount={}",
|
||||
currentUserId, scenicId, cachedResult.getFinalAmount());
|
||||
|
||||
// 使用旧版创建订单逻辑
|
||||
try {
|
||||
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("订单创建失败,请稍后重试");
|
||||
}
|
||||
|
||||
// 创建订单
|
||||
// try {
|
||||
// Long orderId = orderService.createOrder(request, currentUserId, scenicId, cachedResult);
|
||||
//
|
||||
// log.info("移动端订单创建成功: orderId={}, userId={}, scenicId={}, finalAmount={}",
|
||||
// orderId, currentUserId, scenicId, cachedResult.getFinalAmount());
|
||||
//
|
||||
// return ApiResponse.success(orderId.toString());
|
||||
//
|
||||
// } catch (Exception e) {
|
||||
// log.error("订单创建失败: userId={}, scenicId={}, error={}", currentUserId, scenicId, e.getMessage(), e);
|
||||
// return ApiResponse.fail("订单创建失败,请稍后重试");
|
||||
// }
|
||||
}
|
||||
|
||||
// ====== 新增移动端订单查询功能 ======
|
||||
|
||||
/**
|
||||
* 用户分页查询自己的订单列表
|
||||
*/
|
||||
@PostMapping("/page")
|
||||
public ApiResponse<PageInfo<OrderV2ListResponse>> pageUserOrders(@RequestBody OrderV2PageRequest request) {
|
||||
String currentUserIdStr = BaseContextHandler.getUserId();
|
||||
if (currentUserIdStr == null) {
|
||||
log.warn("用户未登录");
|
||||
return ApiResponse.fail("用户未登录");
|
||||
}
|
||||
|
||||
Long currentUserId = Long.valueOf(currentUserIdStr);
|
||||
request.setMemberId(currentUserId); // 设置当前用户ID,确保只查询自己的订单
|
||||
|
||||
log.info("用户查询订单列表: userId={}, request={}", currentUserId, request);
|
||||
|
||||
try {
|
||||
PageInfo<OrderV2ListResponse> pageInfo = orderService.pageOrdersByUser(request);
|
||||
return ApiResponse.success(pageInfo);
|
||||
} catch (Exception e) {
|
||||
log.error("查询用户订单列表失败: userId={}", currentUserId, e);
|
||||
return ApiResponse.fail("查询失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询订单详情
|
||||
*/
|
||||
@GetMapping("/detail/{orderId}")
|
||||
public ApiResponse<OrderV2DetailResponse> getUserOrderDetail(@PathVariable("orderId") Long orderId) {
|
||||
log.info("查询订单详情: orderId={}", orderId);
|
||||
|
||||
try {
|
||||
OrderV2DetailResponse detail = orderService.getOrderDetail(orderId);
|
||||
if (detail == null) {
|
||||
return ApiResponse.fail("订单不存在");
|
||||
}
|
||||
|
||||
return ApiResponse.success(detail);
|
||||
} catch (Exception e) {
|
||||
log.error("查询订单详情失败: orderId={}", orderId, e);
|
||||
return ApiResponse.fail("查询失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 支付相关接口 ======
|
||||
|
||||
/**
|
||||
* 获取订单支付参数
|
||||
* 用于小程序调起支付
|
||||
*/
|
||||
@PostMapping("/{orderId}/payment-params")
|
||||
public ApiResponse<PaymentParamsResponse> getPaymentParams(
|
||||
@PathVariable("orderId") Long orderId,
|
||||
@RequestBody PaymentParamsRequest request) {
|
||||
|
||||
String currentUserIdStr = BaseContextHandler.getUserId();
|
||||
if (currentUserIdStr == null) {
|
||||
log.warn("用户未登录");
|
||||
return ApiResponse.fail("用户未登录");
|
||||
}
|
||||
|
||||
Long currentUserId = Long.valueOf(currentUserIdStr);
|
||||
|
||||
log.info("获取支付参数: userId={}, orderId={}", currentUserId, orderId);
|
||||
|
||||
return oldOrderService.getPaymentParams(orderId, currentUserId, request);
|
||||
|
||||
//
|
||||
// try {
|
||||
// PaymentParamsResponse response = orderService.getPaymentParams(orderId, currentUserId, request);
|
||||
// return ApiResponse.success(response);
|
||||
// } catch (Exception e) {
|
||||
// log.error("获取支付参数失败: userId={}, orderId={}", currentUserId, orderId, e);
|
||||
// return ApiResponse.fail(e.getMessage());
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付回调处理接口
|
||||
* 供第三方支付平台回调使用
|
||||
*/
|
||||
@PostMapping("/payment/callback/{scenicId}")
|
||||
public String handlePaymentCallback(
|
||||
@PathVariable("scenicId") Long scenicId,
|
||||
HttpServletRequest request) {
|
||||
|
||||
log.info("接收支付回调: scenicId={}", scenicId);
|
||||
|
||||
try {
|
||||
PaymentCallbackResponse response = orderService.handlePaymentCallback(scenicId, request);
|
||||
|
||||
if (response.isSuccess()) {
|
||||
log.info("支付回调处理成功: scenicId={}, orderId={}, statusChangeType={}",
|
||||
scenicId, response.getOrderId(), response.getStatusChangeType());
|
||||
return "SUCCESS"; // 返回给第三方支付平台的成功标识
|
||||
} else {
|
||||
log.error("支付回调处理失败: scenicId={}, message={}", scenicId, response.getMessage());
|
||||
return "FAIL"; // 返回给第三方支付平台的失败标识
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("支付回调异常: scenicId={}", scenicId, e);
|
||||
return "FAIL";
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/downloadable/{orderId}")
|
||||
public ApiResponse<Boolean> getDownloadableOrder(@PathVariable("orderId") Long orderId) {
|
||||
return ApiResponse.success(!redisTemplate.hasKey("order_content_not_downloadable_" + orderId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查商品是否已购买
|
||||
* 使用 DuplicatePurchaseChecker 通过异常捕获判断
|
||||
*
|
||||
* @param strategy 重复检查策略
|
||||
* @param userId 用户ID
|
||||
* @param scenicId 景区ID
|
||||
* @param productType 商品类型
|
||||
* @param productId 商品ID
|
||||
* @param faceId 人脸ID
|
||||
* @return true-已购买, false-未购买
|
||||
*/
|
||||
private boolean checkIfPurchased(DuplicateCheckStrategy strategy, Long userId, String scenicId,
|
||||
String productType, String productId, Long faceId) {
|
||||
// NO_CHECK 策略表示允许重复购买,直接返回 false
|
||||
if (strategy == DuplicateCheckStrategy.NO_CHECK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取对应的检查器
|
||||
IDuplicatePurchaseChecker checker = duplicatePurchaseCheckerFactory.getChecker(strategy);
|
||||
|
||||
// 构建检查上下文
|
||||
DuplicateCheckContext context = new DuplicateCheckContext();
|
||||
context.setUserId(String.valueOf(userId));
|
||||
context.setScenicId(scenicId);
|
||||
context.setProductType(productType);
|
||||
context.setProductId(productId);
|
||||
context.addParam("faceId", faceId);
|
||||
|
||||
// 执行检查,如果抛出异常则表示已购买
|
||||
checker.check(context);
|
||||
|
||||
// 没有抛出异常,表示未购买
|
||||
return false;
|
||||
|
||||
} catch (DuplicatePurchaseException e) {
|
||||
// 捕获到重复购买异常,表示已购买
|
||||
log.debug("检测到已购买: userId={}, scenicId={}, productType={}, productId={}",
|
||||
userId, scenicId, productType, productId);
|
||||
return true;
|
||||
|
||||
} catch (Exception e) {
|
||||
// 其他异常,记录日志并返回 false(保守处理)
|
||||
log.warn("检查是否已购买时发生异常: userId={}, scenicId={}, productType={}, productId={}, error={}",
|
||||
userId, scenicId, productType, productId, e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,336 @@
|
||||
package com.ycwl.basic.controller.mobile;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import com.ycwl.basic.biz.OrderBiz;
|
||||
import com.ycwl.basic.constant.FreeStatus;
|
||||
import com.ycwl.basic.image.watermark.edge.PuzzleDefaultWatermarkTemplateBuilder;
|
||||
import com.ycwl.basic.image.watermark.edge.WatermarkEdgeTaskCreator;
|
||||
import com.ycwl.basic.image.watermark.edge.WatermarkRequest;
|
||||
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.model.pc.puzzle.entity.MemberPuzzleEntity;
|
||||
import com.ycwl.basic.model.pc.scenic.entity.ScenicEntity;
|
||||
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.edge.task.PuzzleEdgeRenderTaskService;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleGenerationRecordEntity;
|
||||
import com.ycwl.basic.puzzle.mapper.MemberPuzzleMapper;
|
||||
import com.ycwl.basic.puzzle.repository.PuzzleRepository;
|
||||
import com.ycwl.basic.repository.FaceRepository;
|
||||
import com.ycwl.basic.repository.ScenicRepository;
|
||||
import com.ycwl.basic.service.pc.FaceService;
|
||||
import com.ycwl.basic.service.printer.PrinterService;
|
||||
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.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/mobile/puzzle/v1")
|
||||
@RequiredArgsConstructor
|
||||
public class AppPuzzleController {
|
||||
|
||||
private final PuzzleRepository puzzleRepository;
|
||||
private final FaceRepository faceRepository;
|
||||
private final IPriceCalculationService iPriceCalculationService;
|
||||
private final PrinterService printerService;
|
||||
private final OrderBiz orderBiz;
|
||||
private final MemberPuzzleMapper memberPuzzleMapper;
|
||||
private final WatermarkEdgeTaskCreator watermarkEdgeTaskCreator;
|
||||
private final FaceService faceService;
|
||||
private final ScenicRepository scenicRepository;
|
||||
|
||||
/**
|
||||
* 根据faceId查询三拼图数量
|
||||
*/
|
||||
@GetMapping("/count/{faceId}")
|
||||
public ApiResponse<Integer> countByFaceId(@PathVariable("faceId") Long faceId) {
|
||||
if (faceId == null) {
|
||||
return ApiResponse.fail("faceId不能为空");
|
||||
}
|
||||
// 通过关联表查询数量
|
||||
List<MemberPuzzleEntity> relations = memberPuzzleMapper.listByFaceId(faceId);
|
||||
return ApiResponse.success(relations.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据faceId查询所有三拼图记录
|
||||
*/
|
||||
@GetMapping("/list/{faceId}")
|
||||
public ApiResponse<List<ContentPageVO>> listByFaceId(@PathVariable("faceId") Long faceId) {
|
||||
if (faceId == null) {
|
||||
return ApiResponse.fail("faceId不能为空");
|
||||
}
|
||||
// 通过关联表查询,获取关联的拼图记录
|
||||
List<MemberPuzzleEntity> relations = memberPuzzleMapper.listByFaceId(faceId);
|
||||
List<ContentPageVO> result = relations.stream()
|
||||
.map(relation -> {
|
||||
PuzzleGenerationRecordEntity record = puzzleRepository.getRecordById(relation.getRecordId());
|
||||
if (record == null) {
|
||||
return null;
|
||||
}
|
||||
return convertToContentPageVO(record, relation);
|
||||
})
|
||||
.filter(vo -> vo != null)
|
||||
.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 = puzzleRepository.getRecordById(recordId);
|
||||
if (record == null) {
|
||||
return ApiResponse.fail("未找到对应的拼图记录");
|
||||
}
|
||||
// 查询关联记录
|
||||
MemberPuzzleEntity relation = memberPuzzleMapper.getByFaceAndRecord(record.getFaceId(), recordId);
|
||||
ContentPageVO result = convertToContentPageVO(record, relation);
|
||||
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 = puzzleRepository.getRecordById(recordId);
|
||||
if (record == null) {
|
||||
return ApiResponse.fail("未找到对应的拼图记录");
|
||||
}
|
||||
String resultImageUrl = record.getResultImageUrl();
|
||||
if (resultImageUrl == null || resultImageUrl.isEmpty()) {
|
||||
return ApiResponse.fail("该拼图记录没有可用的图片URL");
|
||||
}
|
||||
|
||||
// 查询该拼图的关联记录,判断是否为免费赠送
|
||||
Long faceId = record.getFaceId();
|
||||
if (faceId != null) {
|
||||
MemberPuzzleEntity memberPuzzle = memberPuzzleMapper.getByFaceAndRecord(faceId, recordId);
|
||||
if (memberPuzzle != null && FreeStatus.isFree(memberPuzzle.getIsFree())) {
|
||||
// 免费赠送的拼图,需要添加水印
|
||||
String watermarkedUrl = addWatermarkForFreePuzzle(record);
|
||||
if (watermarkedUrl != null) {
|
||||
return ApiResponse.success(Collections.singletonList(watermarkedUrl));
|
||||
}
|
||||
// 如果水印添加失败,记录日志并返回原图
|
||||
log.warn("免费拼图水印添加失败,返回原图: recordId={}", recordId);
|
||||
}
|
||||
}
|
||||
|
||||
return ApiResponse.success(Collections.singletonList(resultImageUrl));
|
||||
}
|
||||
|
||||
/**
|
||||
* 为免费赠送的拼图添加水印
|
||||
*
|
||||
* @param record 拼图记录
|
||||
* @return 带水印的图片URL,失败返回null
|
||||
*/
|
||||
private String addWatermarkForFreePuzzle(PuzzleGenerationRecordEntity record) {
|
||||
try {
|
||||
Long faceId = record.getFaceId();
|
||||
FaceEntity face = faceRepository.getFace(faceId);
|
||||
if (face == null) {
|
||||
log.warn("添加水印失败:未找到人脸信息, faceId={}", faceId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取景区信息
|
||||
ScenicEntity scenic = scenicRepository.getScenic(face.getScenicId());
|
||||
String scenicLine = scenic != null ? scenic.getName() : "";
|
||||
|
||||
// 获取二维码URL
|
||||
String qrcodeUrl = faceService.bindWxaCode(faceId);
|
||||
|
||||
// 格式化日期时间
|
||||
String datetimeLine = record.getCreateTime() != null
|
||||
? DateUtil.format(record.getCreateTime(), "yyyy-MM-dd")
|
||||
: "";
|
||||
|
||||
// 构建水印请求
|
||||
WatermarkRequest request = WatermarkRequest.builder()
|
||||
.originalImageUrl(record.getResultImageUrl())
|
||||
.imageWidth(record.getResultWidth() != null ? record.getResultWidth() : 0)
|
||||
.imageHeight(record.getResultHeight() != null ? record.getResultHeight() : 0)
|
||||
.qrcodeUrl(qrcodeUrl)
|
||||
.faceUrl(face.getFaceUrl())
|
||||
.scenicLine(scenicLine)
|
||||
.datetimeLine(datetimeLine)
|
||||
.outputFormat("JPEG")
|
||||
.outputQuality(90)
|
||||
.build();
|
||||
|
||||
// 创建水印任务并等待结果
|
||||
PuzzleEdgeRenderTaskService.TaskWaitResult result = watermarkEdgeTaskCreator.createAndWait(
|
||||
PuzzleDefaultWatermarkTemplateBuilder.STYLE,
|
||||
request,
|
||||
record.getId(),
|
||||
faceId,
|
||||
"free_puzzle_download",
|
||||
30_000L // 30秒超时
|
||||
);
|
||||
|
||||
if (result.isSuccess()) {
|
||||
log.info("免费拼图水印添加成功: recordId={}, url={}", record.getId(), result.getImageUrl());
|
||||
return result.getImageUrl();
|
||||
} else {
|
||||
log.error("免费拼图水印添加失败: recordId={}, error={}", record.getId(), result.getErrorMessage());
|
||||
return null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("免费拼图水印添加异常: recordId={}", record.getId(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据recordId查询拼图价格
|
||||
*/
|
||||
@GetMapping("/price/{recordId}")
|
||||
public ApiResponse<PriceCalculationResult> getPriceByRecordId(@PathVariable("recordId") Long recordId) {
|
||||
if (recordId == null) {
|
||||
return ApiResponse.fail("recordId不能为空");
|
||||
}
|
||||
PuzzleGenerationRecordEntity record = puzzleRepository.getRecordById(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 = puzzleRepository.getRecordById(recordId);
|
||||
if (record == null) {
|
||||
return ApiResponse.fail("未找到对应的拼图记录");
|
||||
}
|
||||
|
||||
// 检查是否有图片URL
|
||||
String imageUrl = record.getResultImageUrl();
|
||||
if (imageUrl == null || imageUrl.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(),
|
||||
imageUrl,
|
||||
recordId // 拼图记录ID,用于关联 puzzle_record 表
|
||||
);
|
||||
|
||||
if (memberPrintId == null) {
|
||||
return ApiResponse.fail("添加到打印列表失败");
|
||||
}
|
||||
|
||||
return ApiResponse.success(memberPrintId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将PuzzleGenerationRecordEntity转换为ContentPageVO
|
||||
*
|
||||
* @param record 拼图生成记录
|
||||
* @param relation 会员拼图关联记录,用于获取免费状态
|
||||
*/
|
||||
private ContentPageVO convertToContentPageVO(PuzzleGenerationRecordEntity record, MemberPuzzleEntity relation) {
|
||||
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);
|
||||
// 从关联记录读取免费状态
|
||||
if (relation != null && FreeStatus.isFree(relation.getIsFree())) {
|
||||
vo.setFreeCount(1);
|
||||
} else {
|
||||
vo.setFreeCount(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
package com.ycwl.basic.controller.mobile;
|
||||
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.ycwl.basic.annotation.IgnoreToken;
|
||||
import com.ycwl.basic.constant.BaseContextHandler;
|
||||
import com.ycwl.basic.model.jwt.JwtInfo;
|
||||
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.model.pc.scenic.req.ScenicReqQuery;
|
||||
import com.ycwl.basic.model.pc.scenic.resp.ScenicConfigResp;
|
||||
import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO;
|
||||
import com.ycwl.basic.repository.ScenicRepository;
|
||||
import com.ycwl.basic.service.mobile.AppScenicService;
|
||||
import com.ycwl.basic.service.pc.FaceService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import com.ycwl.basic.utils.JwtTokenUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Author:longbinbin
|
||||
* @Date:2024/12/5 10:22
|
||||
*/
|
||||
@Slf4j
|
||||
@Deprecated
|
||||
@RestController
|
||||
@RequestMapping("/api/mobile/scenic/v1")
|
||||
// 景区相关接口
|
||||
public class AppScenicController {
|
||||
|
||||
@Autowired
|
||||
private FaceService faceService;
|
||||
@Autowired
|
||||
private AppScenicService appScenicService;
|
||||
@Autowired
|
||||
private ScenicRepository scenicRepository;
|
||||
private static final List<String> ENABLED_USER_IDs = new ArrayList<>(){{
|
||||
add("3932535453961555968");
|
||||
add("3936121342868459520");
|
||||
add("3936940597855784960");
|
||||
}};
|
||||
|
||||
// 分页查询景区列表
|
||||
@PostMapping("/page")
|
||||
public ApiResponse<PageInfo<ScenicAppVO>> pageQuery(@RequestBody ScenicReqQuery scenicReqQuery){
|
||||
String userId = BaseContextHandler.getUserId();
|
||||
if (ENABLED_USER_IDs.contains(userId)) {
|
||||
return appScenicService.pageQuery(scenicReqQuery);
|
||||
} else {
|
||||
return ApiResponse.success(new PageInfo<>(new ArrayList<>()));
|
||||
}
|
||||
}
|
||||
// 根据id查询景区详情
|
||||
@IgnoreToken
|
||||
@GetMapping("/{id}")
|
||||
public ApiResponse<ScenicRespVO> getDetails(@PathVariable Long id){
|
||||
return appScenicService.getDetails(id);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/config")
|
||||
@IgnoreToken
|
||||
public ApiResponse<ScenicConfigResp> getConfig(@PathVariable Long id){
|
||||
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(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());
|
||||
return ApiResponse.success(resp);
|
||||
}
|
||||
|
||||
// 查询景区设备总数和拍到用户的机位数量
|
||||
@GetMapping("/{scenicId}/deviceCount/")
|
||||
public ApiResponse<ScenicDeviceCountVO> deviceCountByScenicId(@PathVariable Long scenicId){
|
||||
return appScenicService.deviceCountByScenicId(scenicId);
|
||||
}
|
||||
|
||||
// 景区视频源素材列表
|
||||
@GetMapping("/contentList/")
|
||||
public ApiResponse<List<ContentPageVO>> contentList() {
|
||||
return faceService.contentListUseDefaultFace();
|
||||
}
|
||||
|
||||
// 景区视频源素材列表
|
||||
@GetMapping("/face/{faceId}/contentList")
|
||||
public ApiResponse<List<ContentPageVO>> contentList(@PathVariable Long faceId) {
|
||||
List<ContentPageVO> contentPageVOS = faceService.faceContentList(faceId);
|
||||
return ApiResponse.success(contentPageVOS);
|
||||
}
|
||||
|
||||
@PostMapping("/nearby")
|
||||
public ApiResponse<List<ScenicAppVO>> nearby(@RequestBody ScenicIndexVO scenicIndexVO) {
|
||||
List<ScenicAppVO> list = appScenicService.scenicListByLnLa(scenicIndexVO);
|
||||
return ApiResponse.success(list);
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,6 @@ public class AppTaskController {
|
||||
@GetMapping("/face/{faceId}")
|
||||
@IgnoreLogReq
|
||||
public ApiResponse<VideoTaskStatusVO> getTaskStatusByFaceId(@PathVariable("faceId") Long faceId) {
|
||||
JwtInfo worker = JwtTokenUtil.getWorker();
|
||||
return ApiResponse.success(goodsService.getTaskStatusByFaceId(faceId));
|
||||
}
|
||||
@GetMapping("/scenic/{scenicId}")
|
||||
@@ -49,13 +48,12 @@ public class AppTaskController {
|
||||
@GetMapping("/face/{faceId}/template/{templateId}")
|
||||
@IgnoreLogReq
|
||||
public ApiResponse<VideoTaskStatusVO> getTemplateTaskStatus(@PathVariable("faceId") Long faceId, @PathVariable("templateId") Long templateId) {
|
||||
JwtInfo worker = JwtTokenUtil.getWorker();
|
||||
return ApiResponse.success(goodsService.getTaskStatusByTemplateId(faceId, templateId));
|
||||
}
|
||||
|
||||
@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("成功");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package com.ycwl.basic.controller.mobile;
|
||||
|
||||
import com.ycwl.basic.annotation.IgnoreToken;
|
||||
import com.ycwl.basic.model.mobile.scenic.content.ScenicTemplateContentVO;
|
||||
import com.ycwl.basic.model.pc.template.resp.TemplateRespVO;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity;
|
||||
import com.ycwl.basic.puzzle.repository.PuzzleRepository;
|
||||
import com.ycwl.basic.repository.TemplateRepository;
|
||||
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.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 移动端模板接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/mobile/template/v1")
|
||||
@RequiredArgsConstructor
|
||||
public class AppTemplateController {
|
||||
|
||||
private final TemplateRepository templateRepository;
|
||||
private final PuzzleRepository puzzleRepository;
|
||||
|
||||
/**
|
||||
* 根据模板ID获取封面URL
|
||||
*
|
||||
* @param templateId 模板ID
|
||||
* @return 模板封面URL
|
||||
*/
|
||||
@GetMapping("/cover/{templateId}")
|
||||
public ApiResponse<String> getTemplateCoverUrl(@PathVariable("templateId") Long templateId) {
|
||||
if (templateId == null) {
|
||||
return ApiResponse.fail("模板ID不能为空");
|
||||
}
|
||||
|
||||
TemplateRespVO template = templateRepository.getTemplate(templateId);
|
||||
if (template == null) {
|
||||
return ApiResponse.fail("未找到对应的模板");
|
||||
}
|
||||
|
||||
String coverUrl = template.getCoverUrl();
|
||||
if (coverUrl == null || coverUrl.isEmpty()) {
|
||||
return ApiResponse.fail("该模板没有封面地址");
|
||||
}
|
||||
|
||||
return ApiResponse.success(coverUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据景区ID获取所有模板封面URL列表(用于前端预缓存)
|
||||
*
|
||||
* @param scenicId 景区ID
|
||||
* @return 模板封面URL列表
|
||||
*/
|
||||
@GetMapping("/scenic/{scenicId}/covers")
|
||||
@IgnoreToken
|
||||
public ApiResponse<List<String>> getScenicTemplateCoverUrls(@PathVariable("scenicId") Long scenicId) {
|
||||
if (scenicId == null) {
|
||||
return ApiResponse.fail("景区ID不能为空");
|
||||
}
|
||||
|
||||
List<String> coverUrls = new ArrayList<>();
|
||||
|
||||
// 获取普通模板封面
|
||||
List<TemplateRespVO> templateList = templateRepository.getTemplateListByScenicId(scenicId);
|
||||
templateList.stream()
|
||||
.map(TemplateRespVO::getCoverUrl)
|
||||
.filter(Objects::nonNull)
|
||||
.filter(url -> !url.isEmpty())
|
||||
.forEach(coverUrls::add);
|
||||
|
||||
// 获取拼图模板封面(使用缓存)
|
||||
List<PuzzleTemplateEntity> puzzleTemplateList = puzzleRepository.listTemplateByScenic(scenicId);
|
||||
puzzleTemplateList.stream()
|
||||
.map(PuzzleTemplateEntity::getCoverImage)
|
||||
.filter(Objects::nonNull)
|
||||
.filter(url -> !url.isEmpty())
|
||||
.forEach(coverUrls::add);
|
||||
|
||||
return ApiResponse.success(coverUrls);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据景区ID获取所有模板内容列表(返回模板基础信息,与 faceId 无关)
|
||||
*
|
||||
* @param scenicId 景区ID
|
||||
* @return 景区模板内容列表
|
||||
*/
|
||||
@GetMapping("/scenic/{scenicId}/contents")
|
||||
@IgnoreToken
|
||||
public ApiResponse<List<ScenicTemplateContentVO>> getScenicTemplateContents(@PathVariable("scenicId") Long scenicId) {
|
||||
if (scenicId == null) {
|
||||
return ApiResponse.fail("景区ID不能为空");
|
||||
}
|
||||
|
||||
List<ScenicTemplateContentVO> contentList = new ArrayList<>();
|
||||
|
||||
// 获取普通模板
|
||||
List<TemplateRespVO> templateList = templateRepository.getTemplateListByScenicId(scenicId);
|
||||
for (TemplateRespVO template : templateList) {
|
||||
ScenicTemplateContentVO content = new ScenicTemplateContentVO();
|
||||
content.setGoodsType(0); // 普通模板默认商品类型为 0
|
||||
content.setName(template.getName());
|
||||
content.setGroup(template.getGroup());
|
||||
content.setTemplateId(template.getId());
|
||||
content.setTemplateCoverUrl(template.getCoverUrl());
|
||||
contentList.add(content);
|
||||
}
|
||||
|
||||
// 获取拼图模板
|
||||
List<PuzzleTemplateEntity> puzzleTemplateList = puzzleRepository.listTemplateByScenic(scenicId);
|
||||
for (PuzzleTemplateEntity puzzleTemplate : puzzleTemplateList) {
|
||||
ScenicTemplateContentVO content = new ScenicTemplateContentVO();
|
||||
content.setGoodsType(3); // 拼图模板商品类型为 3
|
||||
content.setName(puzzleTemplate.getName());
|
||||
content.setGroup("氛围拼图"); // 拼图模板固定分组
|
||||
content.setTemplateId(puzzleTemplate.getId());
|
||||
content.setTemplateCoverUrl(puzzleTemplate.getCoverImage());
|
||||
contentList.add(content);
|
||||
}
|
||||
|
||||
return ApiResponse.success(contentList);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,124 @@
|
||||
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.pc.video.resp.VideoRespVO;
|
||||
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.RequestParam;
|
||||
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("权限检查失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过faceId和templateId(可选)查询最新的视频记录
|
||||
*
|
||||
* @param faceId 人脸ID
|
||||
* @param templateId 模板ID(可选)
|
||||
* @return 最新的视频记录
|
||||
*/
|
||||
@GetMapping("/latest")
|
||||
public ApiResponse<VideoRespVO> getLatestByFaceId(
|
||||
@RequestParam("faceId") Long faceId,
|
||||
@RequestParam(value = "templateId", required = false) Long templateId) {
|
||||
try {
|
||||
log.debug("查询最新视频记录: faceId={}, templateId={}", faceId, templateId);
|
||||
|
||||
VideoRespVO video = videoRepository.queryLatestByFaceIdAndTemplateId(faceId, templateId);
|
||||
|
||||
if (video == null) {
|
||||
log.info("未找到视频记录: faceId={}, templateId={}", faceId, templateId);
|
||||
return ApiResponse.fail("未找到视频记录");
|
||||
}
|
||||
|
||||
return ApiResponse.success(video);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("查询最新视频记录失败: faceId={}, templateId={}", faceId, templateId, e);
|
||||
return ApiResponse.fail("查询失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.ycwl.basic.controller.mobile;
|
||||
|
||||
import com.ycwl.basic.constant.BaseContextHandler;
|
||||
import com.ycwl.basic.exception.BaseException;
|
||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||
import com.ycwl.basic.pricing.dto.req.VoucherClaimReq;
|
||||
import com.ycwl.basic.pricing.dto.req.VoucherPrintReq;
|
||||
import com.ycwl.basic.pricing.dto.resp.VoucherCodeResp;
|
||||
import com.ycwl.basic.pricing.dto.resp.VoucherPrintResp;
|
||||
import com.ycwl.basic.pricing.service.VoucherCodeService;
|
||||
import com.ycwl.basic.pricing.service.VoucherPrintService;
|
||||
import com.ycwl.basic.repository.FaceRepository;
|
||||
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.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.RestController;
|
||||
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/mobile/voucher/v1")
|
||||
public class AppVoucherController {
|
||||
|
||||
@Autowired
|
||||
private VoucherPrintService voucherPrintService;
|
||||
@Autowired
|
||||
private VoucherCodeService voucherCodeService;
|
||||
@Autowired
|
||||
private FaceRepository faceRepository;
|
||||
|
||||
/**
|
||||
* 打印小票
|
||||
* @param request 打印请求
|
||||
* @return 打印结果
|
||||
*/
|
||||
@PostMapping("/print")
|
||||
public ApiResponse<VoucherPrintResp> printVoucherTicket(@RequestBody VoucherPrintReq request) {
|
||||
log.info("收到打印小票请求: faceId={}, brokerId={}, scenicId={}",
|
||||
request.getFaceId(), request.getBrokerId(), request.getScenicId());
|
||||
|
||||
VoucherPrintResp response = voucherPrintService.printVoucherTicket(request);
|
||||
|
||||
log.info("打印小票完成: code={}, voucherCode={}, status={}",
|
||||
response.getCode(), response.getVoucherCode(), response.getPrintStatus());
|
||||
|
||||
return ApiResponse.success(response);
|
||||
}
|
||||
|
||||
@GetMapping("/printed")
|
||||
public ApiResponse<VoucherPrintResp> queryPrintedVoucher(
|
||||
@RequestParam Long faceId
|
||||
) {
|
||||
return ApiResponse.success(voucherPrintService.queryPrintedVoucher(faceId));
|
||||
}
|
||||
|
||||
@PostMapping("/claim")
|
||||
public ApiResponse<VoucherCodeResp> claimVoucher(@RequestBody VoucherClaimReq req) {
|
||||
FaceEntity face = faceRepository.getFace(req.getFaceId());
|
||||
if (face == null) {
|
||||
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);
|
||||
}
|
||||
}});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,14 +2,18 @@ package com.ycwl.basic.controller.mobile.manage;
|
||||
|
||||
import com.ycwl.basic.annotation.IgnoreToken;
|
||||
import com.ycwl.basic.constant.BaseContextHandler;
|
||||
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
|
||||
import com.ycwl.basic.model.mobile.scenic.account.ScenicLoginOldRespVO;
|
||||
import com.ycwl.basic.model.mobile.scenic.account.ScenicLoginReq;
|
||||
import com.ycwl.basic.model.mobile.scenic.account.ScenicLoginRespVO;
|
||||
import com.ycwl.basic.model.mobile.scenic.account.ScenicRegisterReq;
|
||||
import com.ycwl.basic.model.mobile.scenic.account.ScenicRegisterRespVO;
|
||||
import com.ycwl.basic.model.mobile.weChat.DTO.WeChatUserInfoDTO;
|
||||
import com.ycwl.basic.model.pc.device.resp.DeviceRespVO;
|
||||
import com.ycwl.basic.model.pc.scenic.entity.ScenicAccountEntity;
|
||||
import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery;
|
||||
import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO;
|
||||
import com.ycwl.basic.repository.ScenicRepository;
|
||||
import com.ycwl.basic.service.mobile.AppScenicService;
|
||||
import com.ycwl.basic.service.pc.ScenicAccountService;
|
||||
import com.ycwl.basic.service.pc.ScenicService;
|
||||
@@ -23,10 +27,10 @@ import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static com.ycwl.basic.constant.JwtRoleConstant.ADMIN;
|
||||
import static com.ycwl.basic.constant.JwtRoleConstant.MERCHANT;
|
||||
|
||||
/**
|
||||
@@ -43,6 +47,8 @@ public class AppScenicAccountController {
|
||||
private AppScenicService scenicService;
|
||||
@Autowired
|
||||
private ScenicService adminScenicService;
|
||||
@Autowired
|
||||
private ScenicRepository scenicRepository;
|
||||
|
||||
// 登录
|
||||
@PostMapping("/login")
|
||||
@@ -63,9 +69,16 @@ public class AppScenicAccountController {
|
||||
return ApiResponse.success(vo);
|
||||
}
|
||||
|
||||
// 注册
|
||||
@PostMapping("/register")
|
||||
@IgnoreToken
|
||||
public ApiResponse<ScenicRegisterRespVO> register(@RequestBody ScenicRegisterReq scenicRegisterReq) {
|
||||
return scenicService.register(scenicRegisterReq);
|
||||
}
|
||||
|
||||
@GetMapping("/myScenicList")
|
||||
public ApiResponse<List<ScenicRespVO>> myScenicList() {
|
||||
List<ScenicRespVO> list;
|
||||
public ApiResponse<List<ScenicV2DTO>> myScenicList() {
|
||||
List<ScenicV2DTO> list = Collections.emptyList();
|
||||
if (Strings.CS.equals(BaseContextHandler.getRoleId(), MERCHANT.type)) {
|
||||
String userId = BaseContextHandler.getUserId();
|
||||
ScenicAccountEntity account = accountService.getScenicAccountById(Long.valueOf(userId));
|
||||
@@ -73,10 +86,12 @@ public class AppScenicAccountController {
|
||||
return ApiResponse.fail("景区账号未绑定景区");
|
||||
}
|
||||
list = account.getScenicId().stream()
|
||||
.map(id -> scenicService.getDetails(id).getData())
|
||||
.map(id -> scenicRepository.getScenicBasic(id))
|
||||
.toList();
|
||||
} else {
|
||||
list = adminScenicService.list(new ScenicReqQuery()).getData();
|
||||
} else if (Strings.CS.equals(BaseContextHandler.getRoleId(), ADMIN.type)) {
|
||||
ScenicReqQuery query = new ScenicReqQuery();
|
||||
query.setPageSize(1000);
|
||||
list = scenicRepository.list(query);
|
||||
}
|
||||
return ApiResponse.success(list);
|
||||
}
|
||||
|
||||
@@ -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,150 @@
|
||||
package com.ycwl.basic.controller.mobile.manage;
|
||||
|
||||
import com.ycwl.basic.annotation.IgnoreToken;
|
||||
import com.ycwl.basic.model.mobile.video.dto.HlsStreamRequest;
|
||||
import com.ycwl.basic.model.mobile.video.dto.HlsStreamResponse;
|
||||
import com.ycwl.basic.service.mobile.HlsStreamService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* 移动端视频流控制器
|
||||
* 提供HLS视频流播放列表生成功能
|
||||
*
|
||||
* @author Claude Code
|
||||
* @date 2025-12-26
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/mobile/video-stream/v1")
|
||||
@RequiredArgsConstructor
|
||||
public class AppVideoStreamController {
|
||||
|
||||
private final HlsStreamService hlsStreamService;
|
||||
|
||||
/**
|
||||
* 生成设备视频的HLS播放列表(JSON格式)
|
||||
* 返回包含m3u8内容和视频片段信息的JSON对象
|
||||
*
|
||||
* @param request HLS流请求参数
|
||||
* @return HLS播放列表响应
|
||||
*/
|
||||
@PostMapping("/hls/playlist")
|
||||
public ApiResponse<HlsStreamResponse> generateHlsPlaylist(@Validated @RequestBody HlsStreamRequest request) {
|
||||
log.info("收到HLS播放列表生成请求: deviceId={}, durationMinutes={}",
|
||||
request.getDeviceId(), request.getDurationMinutes());
|
||||
|
||||
try {
|
||||
HlsStreamResponse response = hlsStreamService.generateHlsPlaylist(request);
|
||||
log.info("HLS播放列表生成成功: deviceId={}, segmentCount={}, totalDuration={}s",
|
||||
response.getDeviceId(), response.getSegmentCount(), response.getTotalDurationSeconds());
|
||||
|
||||
return ApiResponse.success(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("生成HLS播放列表失败: deviceId={}", request.getDeviceId(), e);
|
||||
return ApiResponse.buildResponse(500, null, "生成失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成设备视频的HLS播放列表(m3u8文件格式)
|
||||
* 直接返回m3u8文件内容,可被视频播放器直接使用
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @param durationMinutes 视频时长(分钟),默认2分钟
|
||||
* @param eventPlaylist 是否为Event播放列表,默认true
|
||||
* @param response HTTP响应对象
|
||||
*/
|
||||
@GetMapping("/hls/playlist.m3u8")
|
||||
@IgnoreToken
|
||||
public void generateHlsPlaylistFile(
|
||||
@RequestParam Long deviceId,
|
||||
@RequestParam(defaultValue = "2") Integer durationMinutes,
|
||||
@RequestParam(defaultValue = "true") Boolean eventPlaylist,
|
||||
HttpServletResponse response) {
|
||||
|
||||
log.info("收到m3u8文件生成请求: deviceId={}, durationMinutes={}",
|
||||
deviceId, durationMinutes);
|
||||
|
||||
try {
|
||||
// 构建请求参数
|
||||
HlsStreamRequest request = new HlsStreamRequest();
|
||||
request.setDeviceId(deviceId);
|
||||
request.setDurationMinutes(durationMinutes);
|
||||
request.setEventPlaylist(eventPlaylist);
|
||||
|
||||
// 生成播放列表
|
||||
HlsStreamResponse hlsResponse = hlsStreamService.generateHlsPlaylist(request);
|
||||
|
||||
log.info("m3u8文件生成成功: deviceId={}, segmentCount={}, totalDuration={}s",
|
||||
deviceId, hlsResponse.getSegmentCount(), hlsResponse.getTotalDurationSeconds());
|
||||
|
||||
// 设置响应头
|
||||
response.setContentType("application/vnd.apple.mpegurl");
|
||||
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||
response.setHeader("Content-Disposition", "inline; filename=\"playlist.m3u8\"");
|
||||
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
response.setHeader("Pragma", "no-cache");
|
||||
response.setHeader("Expires", "0");
|
||||
|
||||
// 写入m3u8内容
|
||||
response.getWriter().write(hlsResponse.getPlaylistContent());
|
||||
response.getWriter().flush();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("生成m3u8文件失败: deviceId={}", deviceId, e);
|
||||
try {
|
||||
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||
response.getWriter().write("{\"code\":500,\"message\":\"生成失败: " + e.getMessage() + "\"}");
|
||||
response.getWriter().flush();
|
||||
} catch (IOException ioException) {
|
||||
log.error("写入错误响应失败", ioException);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备最近的视频片段信息
|
||||
* 仅返回视频片段列表,不包含m3u8内容
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @param durationMinutes 视频时长(分钟),默认2分钟
|
||||
* @return 视频片段列表
|
||||
*/
|
||||
@GetMapping("/segments")
|
||||
public ApiResponse<HlsStreamResponse> getVideoSegments(
|
||||
@RequestParam Long deviceId,
|
||||
@RequestParam(defaultValue = "2") Integer durationMinutes) {
|
||||
|
||||
log.info("收到视频片段查询请求: deviceId={}, durationMinutes={}",
|
||||
deviceId, durationMinutes);
|
||||
|
||||
try {
|
||||
HlsStreamRequest request = new HlsStreamRequest();
|
||||
request.setDeviceId(deviceId);
|
||||
request.setDurationMinutes(durationMinutes);
|
||||
request.setEventPlaylist(true);
|
||||
|
||||
HlsStreamResponse response = hlsStreamService.generateHlsPlaylist(request);
|
||||
log.info("视频片段查询成功: deviceId={}, segmentCount={}",
|
||||
deviceId, response.getSegmentCount());
|
||||
|
||||
return ApiResponse.success(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("查询视频片段失败: deviceId={}", deviceId, e);
|
||||
return ApiResponse.buildResponse(500, null, "查询失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package com.ycwl.basic.controller.mobile.notify;
|
||||
|
||||
import com.ycwl.basic.model.mobile.notify.req.BatchRemainingCountReq;
|
||||
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.model.pc.notify.entity.UserNotificationAuthorizationEntity;
|
||||
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.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 用户通知授权记录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={}, requestId={}",
|
||||
req.getTemplateIds(), req.getScenicId(), req.getRequestId());
|
||||
|
||||
try {
|
||||
// 获取当前用户ID
|
||||
Long memberId = JwtTokenUtil.getWorker().getUserId();
|
||||
|
||||
// 调用批量授权记录方法
|
||||
List<UserNotificationAuthorizationService.AuthorizationRecord> records =
|
||||
userNotificationAuthorizationService.batchRecordAuthorization(
|
||||
memberId, req.getTemplateIds(), req.getScenicId(), req.getRequestId());
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量查询用户授权余额
|
||||
* 返回 Map<wechatTemplateId, remainingCount>
|
||||
*/
|
||||
@PostMapping("/batch-remaining")
|
||||
public ApiResponse<Map<String, Integer>> batchGetRemainingCount(
|
||||
@RequestBody BatchRemainingCountReq req) {
|
||||
log.debug("批量查询用户授权余额: templateIds={}, scenicId={}",
|
||||
req.getTemplateIds(), req.getScenicId());
|
||||
|
||||
try {
|
||||
Long memberId = JwtTokenUtil.getWorker().getUserId();
|
||||
if (memberId == null) {
|
||||
return ApiResponse.fail("用户未登录");
|
||||
}
|
||||
|
||||
if (CollectionUtils.isEmpty(req.getTemplateIds())) {
|
||||
return ApiResponse.success(new HashMap<>());
|
||||
}
|
||||
|
||||
Map<String, UserNotificationAuthorizationEntity> authMap =
|
||||
userNotificationAuthorizationService.batchCheckAuthorization(
|
||||
memberId, req.getTemplateIds(), req.getScenicId());
|
||||
|
||||
// 转换为 templateId -> remainingCount
|
||||
Map<String, Integer> result = new HashMap<>();
|
||||
for (String templateId : req.getTemplateIds()) {
|
||||
UserNotificationAuthorizationEntity entity = authMap.get(templateId);
|
||||
int remaining = (entity != null && entity.getRemainingCount() != null)
|
||||
? entity.getRemainingCount() : 0;
|
||||
result.put(templateId, remaining);
|
||||
}
|
||||
|
||||
return ApiResponse.success(result);
|
||||
} catch (Exception e) {
|
||||
log.error("批量查询用户授权余额失败", e);
|
||||
return ApiResponse.fail("查询失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.ycwl.basic.controller.mobile.notify;
|
||||
|
||||
import com.ycwl.basic.annotation.IgnoreToken;
|
||||
import com.ycwl.basic.model.mobile.notify.resp.WechatSubscribeAllScenesResp;
|
||||
import com.ycwl.basic.model.mobile.notify.resp.WechatSubscribeSceneTemplatesResp;
|
||||
import com.ycwl.basic.model.pc.notify.entity.WechatSubscribeTemplateConfigEntity;
|
||||
import com.ycwl.basic.service.notify.WechatSubscribeNotifyConfigService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import com.ycwl.basic.utils.JwtTokenUtil;
|
||||
import com.ycwl.basic.utils.NotificationAuthUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
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;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 微信小程序订阅消息:场景模板查询(移动端API)
|
||||
*
|
||||
* @Author: System
|
||||
* @Date: 2025/12/31
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/mobile/notify/subscribe")
|
||||
@Slf4j
|
||||
public class WechatSubscribeNotifyController {
|
||||
|
||||
private final WechatSubscribeNotifyConfigService configService;
|
||||
private final NotificationAuthUtils notificationAuthUtils;
|
||||
|
||||
public WechatSubscribeNotifyController(WechatSubscribeNotifyConfigService configService,
|
||||
NotificationAuthUtils notificationAuthUtils) {
|
||||
this.configService = configService;
|
||||
this.notificationAuthUtils = notificationAuthUtils;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取“场景”下可申请授权的模板列表(支持按 scenicId 覆盖模板ID/开关/文案)
|
||||
*/
|
||||
@GetMapping("/scenic/{scenicId}/scenes/{sceneKey}/templates")
|
||||
@IgnoreToken
|
||||
public ApiResponse<WechatSubscribeSceneTemplatesResp> listSceneTemplates(@PathVariable("scenicId") Long scenicId,
|
||||
@PathVariable("sceneKey") String sceneKey) {
|
||||
if (scenicId == null) {
|
||||
return ApiResponse.fail("scenicId不能为空");
|
||||
}
|
||||
if (StringUtils.isBlank(sceneKey)) {
|
||||
return ApiResponse.fail("sceneKey不能为空");
|
||||
}
|
||||
|
||||
Long memberId = JwtTokenUtil.getWorker().getUserId();
|
||||
List<WechatSubscribeTemplateConfigEntity> configs = configService.listSceneTemplateConfigs(scenicId, sceneKey);
|
||||
|
||||
WechatSubscribeSceneTemplatesResp resp = new WechatSubscribeSceneTemplatesResp();
|
||||
resp.setScenicId(scenicId);
|
||||
resp.setSceneKey(sceneKey);
|
||||
if (memberId == null) {
|
||||
return ApiResponse.success(resp);
|
||||
}
|
||||
|
||||
List<WechatSubscribeSceneTemplatesResp.TemplateInfo> templates = new ArrayList<>();
|
||||
for (WechatSubscribeTemplateConfigEntity cfg : configs) {
|
||||
if (cfg == null || StringUtils.isBlank(cfg.getWechatTemplateId())) {
|
||||
continue;
|
||||
}
|
||||
String title = StringUtils.isNotBlank(cfg.getTitleTemplate())
|
||||
? cfg.getTitleTemplate()
|
||||
: cfg.getTemplateKey();
|
||||
int remaining = notificationAuthUtils.getRemainingCount(memberId, cfg.getWechatTemplateId(), scenicId);
|
||||
|
||||
WechatSubscribeSceneTemplatesResp.TemplateInfo info = new WechatSubscribeSceneTemplatesResp.TemplateInfo();
|
||||
info.setTemplateKey(cfg.getTemplateKey());
|
||||
info.setWechatTemplateId(cfg.getWechatTemplateId());
|
||||
info.setTitle(title);
|
||||
info.setDescription(cfg.getDescription());
|
||||
info.setRemainingCount(remaining);
|
||||
info.setHasAuthorization(remaining > 0);
|
||||
templates.add(info);
|
||||
}
|
||||
resp.setTemplates(templates);
|
||||
|
||||
log.debug("场景模板查询: scenicId={}, sceneKey={}, memberId={}, templateCount={}",
|
||||
scenicId, sceneKey, memberId, Objects.requireNonNullElse(templates.size(), 0));
|
||||
return ApiResponse.success(resp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取景区下所有场景及其模板列表(静态配置,带缓存)
|
||||
* 不含用户授权信息,用户授权信息通过 /api/mobile/notify/auth/batch-remaining 接口获取
|
||||
*/
|
||||
@GetMapping("/scenic/{scenicId}/scenes")
|
||||
@IgnoreToken
|
||||
public ApiResponse<WechatSubscribeAllScenesResp> listAllSceneTemplates(@PathVariable("scenicId") Long scenicId) {
|
||||
if (scenicId == null) {
|
||||
return ApiResponse.fail("scenicId不能为空");
|
||||
}
|
||||
|
||||
WechatSubscribeAllScenesResp resp = configService.getAllScenesWithTemplatesCached(scenicId);
|
||||
log.debug("所有场景模板查询: scenicId={}, sceneCount={}",
|
||||
scenicId, resp.getScenes() != null ? resp.getScenes().size() : 0);
|
||||
return ApiResponse.success(resp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package com.ycwl.basic.controller.pc;
|
||||
import com.github.pagehelper.PageHelper;
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.ycwl.basic.biz.PriceBiz;
|
||||
import com.ycwl.basic.model.pc.coupon.entity.CouponEntity;
|
||||
import com.ycwl.basic.model.pc.coupon.req.CouponQueryReq;
|
||||
import com.ycwl.basic.model.pc.coupon.resp.CouponRespVO;
|
||||
import com.ycwl.basic.model.pc.price.resp.GoodsListRespVO;
|
||||
import com.ycwl.basic.service.pc.CouponService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/coupon/v1")
|
||||
// 优惠券管理
|
||||
public class CouponController {
|
||||
@Autowired
|
||||
private CouponService couponService;
|
||||
@Autowired
|
||||
private PriceBiz priceBiz;
|
||||
|
||||
@GetMapping("/{scenicId}/goodsList")
|
||||
public ApiResponse<List<GoodsListRespVO>> scenicGoodsList(@PathVariable Long scenicId) {
|
||||
List<GoodsListRespVO> data = priceBiz.listGoodsByScenic(scenicId);
|
||||
data.add(new GoodsListRespVO(-1L, "一口价"));
|
||||
return ApiResponse.success(data);
|
||||
}
|
||||
|
||||
// 新增优惠券
|
||||
@PostMapping("/add")
|
||||
public ApiResponse<Integer> add(@RequestBody CouponEntity coupon) {
|
||||
return ApiResponse.success(couponService.add(coupon));
|
||||
}
|
||||
|
||||
// 更新优惠券
|
||||
@PostMapping("/update/{id}")
|
||||
public ApiResponse<Boolean> update(@PathVariable Integer id, @RequestBody CouponEntity coupon) {
|
||||
coupon.setId(id);
|
||||
return ApiResponse.success(couponService.update(coupon));
|
||||
}
|
||||
|
||||
@PutMapping("/updateStatus/{id}")
|
||||
public ApiResponse<Boolean> updateStatus(@PathVariable Integer id) {
|
||||
return ApiResponse.success(couponService.updateStatus(id));
|
||||
}
|
||||
|
||||
// 删除优惠券
|
||||
@DeleteMapping("/delete/{id}")
|
||||
public ApiResponse<Boolean> delete(@PathVariable Integer id) {
|
||||
return ApiResponse.success(couponService.delete(id));
|
||||
}
|
||||
|
||||
// 根据ID查询优惠券
|
||||
@GetMapping("/get/{id}")
|
||||
public ApiResponse<CouponEntity> getById(@PathVariable Integer id) {
|
||||
return ApiResponse.success(couponService.getById(id));
|
||||
}
|
||||
|
||||
// 分页查询优惠券列表
|
||||
@PostMapping("/page")
|
||||
public ApiResponse<PageInfo<CouponRespVO>> list(@RequestBody CouponQueryReq couponQuery) {
|
||||
PageHelper.startPage(couponQuery.getPageNum(), couponQuery.getPageSize());
|
||||
List<CouponRespVO> list = couponService.list(couponQuery);
|
||||
PageInfo<CouponRespVO> pageInfo = new PageInfo<>(list);
|
||||
return ApiResponse.success(pageInfo);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package com.ycwl.basic.controller.pc;
|
||||
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.ycwl.basic.model.pc.couponRecord.req.CouponRecordPageQueryReq;
|
||||
import com.ycwl.basic.model.pc.couponRecord.resp.CouponRecordPageResp;
|
||||
import com.ycwl.basic.service.pc.CouponRecordService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/coupon/record/v1")
|
||||
public class CouponRecordController {
|
||||
|
||||
@Autowired
|
||||
private CouponRecordService couponRecordService;
|
||||
|
||||
@PostMapping("/page")
|
||||
public ApiResponse<PageInfo<CouponRecordPageResp>> pageQuery(@RequestBody CouponRecordPageQueryReq query) {
|
||||
return couponRecordService.pageQuery(query);
|
||||
}
|
||||
}
|
||||
@@ -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,176 @@
|
||||
package com.ycwl.basic.controller.pc;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ycwl.basic.annotation.IgnoreToken;
|
||||
import com.ycwl.basic.device.entity.common.DeviceVideoContinuityCache;
|
||||
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
|
||||
import com.ycwl.basic.model.pc.device.req.VideoContinuityReportReq;
|
||||
import com.ycwl.basic.repository.DeviceRepository;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 设备视频连续性检查控制器
|
||||
* 提供查询设备视频连续性检查结果的接口
|
||||
*
|
||||
* @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 static final int CACHE_TTL_HOURS = 24; // 缓存24小时
|
||||
|
||||
private final RedisTemplate<String, String> redisTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final DeviceRepository deviceRepository;
|
||||
|
||||
/**
|
||||
* 查询设备最近的视频连续性检查结果
|
||||
*
|
||||
* @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);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除设备的视频连续性检查缓存
|
||||
* 用于清理过期或错误的缓存数据
|
||||
*
|
||||
* @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());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 外部工具上报视频连续性检查结果
|
||||
* 通过设备编号(deviceNo)上报检查结果,无需认证
|
||||
*
|
||||
* @param reportReq 上报请求
|
||||
* @return 上报结果
|
||||
*/
|
||||
@PostMapping("/report")
|
||||
@IgnoreToken
|
||||
public ApiResponse<DeviceVideoContinuityCache> reportContinuityResult(
|
||||
@Validated @RequestBody VideoContinuityReportReq reportReq) {
|
||||
log.info("外部工具上报设备 {} 的视频连续性检查结果", reportReq.getDeviceNo());
|
||||
|
||||
try {
|
||||
// 1. 根据设备编号查询设备ID
|
||||
DeviceEntity device = deviceRepository.getDeviceByDeviceNo(reportReq.getDeviceNo());
|
||||
if (device == null) {
|
||||
log.warn("设备编号 {} 不存在", reportReq.getDeviceNo());
|
||||
return ApiResponse.buildResponse(404, null, "设备不存在: " + reportReq.getDeviceNo());
|
||||
}
|
||||
|
||||
Long deviceId = device.getId();
|
||||
|
||||
// 2. 构建缓存对象
|
||||
DeviceVideoContinuityCache cache = new DeviceVideoContinuityCache();
|
||||
cache.setDeviceId(deviceId);
|
||||
cache.setCheckTime(new Date());
|
||||
cache.setStartTime(reportReq.getStartTime());
|
||||
cache.setEndTime(reportReq.getEndTime());
|
||||
cache.setSupport(reportReq.getSupport());
|
||||
cache.setContinuous(reportReq.getContinuous());
|
||||
cache.setTotalVideos(reportReq.getTotalVideos());
|
||||
cache.setTotalDurationMs(reportReq.getTotalDurationMs());
|
||||
cache.setMaxAllowedGapMs(reportReq.getMaxAllowedGapMs() != null
|
||||
? reportReq.getMaxAllowedGapMs() : 2000L);
|
||||
cache.setGapCount(reportReq.getGaps() != null ? reportReq.getGaps().size() : 0);
|
||||
|
||||
// 3. 转换间隙信息
|
||||
if (reportReq.getGaps() != null && !reportReq.getGaps().isEmpty()) {
|
||||
List<DeviceVideoContinuityCache.GapInfo> gapInfos = reportReq.getGaps().stream()
|
||||
.map(gap -> new DeviceVideoContinuityCache.GapInfo(
|
||||
gap.getBeforeFileName(),
|
||||
gap.getAfterFileName(),
|
||||
gap.getGapMs(),
|
||||
gap.getGapStartTime(),
|
||||
gap.getGapEndTime()
|
||||
))
|
||||
.collect(Collectors.toList());
|
||||
cache.setGaps(gapInfos);
|
||||
}
|
||||
|
||||
// 4. 存储到Redis
|
||||
String redisKey = REDIS_KEY_PREFIX + deviceId;
|
||||
String cacheJson = objectMapper.writeValueAsString(cache);
|
||||
redisTemplate.opsForValue().set(redisKey, cacheJson, CACHE_TTL_HOURS, TimeUnit.HOURS);
|
||||
|
||||
log.info("设备 {} (ID: {}) 视频连续性检查结果上报成功: continuous={}, videos={}, gaps={}",
|
||||
reportReq.getDeviceNo(), deviceId, cache.getContinuous(),
|
||||
cache.getTotalVideos(), cache.getGapCount());
|
||||
|
||||
return ApiResponse.success(cache);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("外部工具上报设备 {} 视频连续性检查结果失败", reportReq.getDeviceNo(), e);
|
||||
return ApiResponse.buildResponse(500, null, "上报失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.ycwl.basic.controller.pc;
|
||||
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.ycwl.basic.model.pc.extraDevice.req.ExtraDevicePageQueryReq;
|
||||
import com.ycwl.basic.model.pc.extraDevice.resp.ExtraDeviceRespVO;
|
||||
import com.ycwl.basic.service.pc.ExtraDeviceService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 外部设备管理控制器
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/extra_device/v1")
|
||||
@RequiredArgsConstructor
|
||||
public class ExtraDeviceController {
|
||||
|
||||
private final ExtraDeviceService extraDeviceService;
|
||||
|
||||
/**
|
||||
* 分页查询外部设备列表
|
||||
*
|
||||
* @param req 查询请求参数,包含scenicId(可选)、pageNum、pageSize
|
||||
* @return 分页查询结果,包含设备ID、景区ID、景区名称、设备名称、标识、状态、心跳时间、在线状态
|
||||
*/
|
||||
@PostMapping("/page")
|
||||
public ApiResponse<PageInfo<ExtraDeviceRespVO>> page(@RequestBody ExtraDevicePageQueryReq req) {
|
||||
log.info("分页查询外部设备列表, scenicId: {}, pageNum: {}, pageSize: {}",
|
||||
req.getScenicId(), req.getPageNum(), req.getPageSize());
|
||||
|
||||
PageInfo<ExtraDeviceRespVO> pageInfo = extraDeviceService.pageQuery(req);
|
||||
|
||||
log.info("外部设备列表查询完成, total: {}, pages: {}",
|
||||
pageInfo.getTotal(), pageInfo.getPages());
|
||||
|
||||
return ApiResponse.success(pageInfo);
|
||||
}
|
||||
}
|
||||
@@ -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,257 @@
|
||||
package com.ycwl.basic.controller.pc;
|
||||
|
||||
import com.ycwl.basic.integration.profitshare.dto.CalculateResultVO;
|
||||
import com.ycwl.basic.integration.profitshare.dto.CalculateShareRequest;
|
||||
import com.ycwl.basic.integration.profitshare.dto.ManualShareRequest;
|
||||
import com.ycwl.basic.integration.profitshare.dto.TypesVO;
|
||||
import com.ycwl.basic.integration.profitshare.dto.record.RecordDetailVO;
|
||||
import com.ycwl.basic.integration.profitshare.dto.record.RecordVO;
|
||||
import com.ycwl.basic.integration.profitshare.dto.rule.CreateRuleRequest;
|
||||
import com.ycwl.basic.integration.profitshare.dto.rule.RuleVO;
|
||||
import com.ycwl.basic.integration.profitshare.service.ProfitShareIntegrationService;
|
||||
import com.ycwl.basic.integration.common.response.PageResponse;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 分账管理 V2 版本控制器 - 基于 zt-profitshare 集成服务
|
||||
*
|
||||
* @author Claude Code
|
||||
* @date 2025-01-11
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/profit-share/v2")
|
||||
@RequiredArgsConstructor
|
||||
public class ProfitShareV2Controller {
|
||||
|
||||
private final ProfitShareIntegrationService profitShareIntegrationService;
|
||||
|
||||
// ========== 分账规则管理 ==========
|
||||
|
||||
/**
|
||||
* 创建分账规则
|
||||
*/
|
||||
@PostMapping("/rules")
|
||||
public ApiResponse<RuleVO> createRule(@Valid @RequestBody CreateRuleRequest request) {
|
||||
log.info("创建分账规则, scenicId: {}, ruleName: {}, ruleType: {}",
|
||||
request.getScenicId(), request.getRuleName(), request.getRuleType());
|
||||
try {
|
||||
RuleVO rule = profitShareIntegrationService.createRule(request);
|
||||
return ApiResponse.success(rule);
|
||||
} catch (Exception e) {
|
||||
log.error("创建分账规则失败", e);
|
||||
return ApiResponse.fail("创建分账规则失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询分账规则列表
|
||||
*/
|
||||
@GetMapping("/rules")
|
||||
public ApiResponse<PageResponse<RuleVO>> listRules(
|
||||
@RequestParam(required = false) Long scenicId,
|
||||
@RequestParam(required = false) String status,
|
||||
@RequestParam(required = false) String ruleType,
|
||||
@RequestParam(defaultValue = "1") Integer page,
|
||||
@RequestParam(defaultValue = "10") Integer pageSize) {
|
||||
log.info("查询分账规则列表, scenicId: {}, status: {}, ruleType: {}, page: {}, pageSize: {}",
|
||||
scenicId, status, ruleType, page, pageSize);
|
||||
|
||||
// 参数验证:限制pageSize最大值为100
|
||||
if (pageSize > 100) {
|
||||
pageSize = 100;
|
||||
}
|
||||
|
||||
try {
|
||||
PageResponse<RuleVO> response = profitShareIntegrationService.listRules(scenicId, status, ruleType, page, pageSize);
|
||||
return ApiResponse.success(response);
|
||||
} catch (Exception e) {
|
||||
log.error("查询分账规则列表失败", e);
|
||||
return ApiResponse.fail("查询分账规则列表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分账规则详情
|
||||
*/
|
||||
@GetMapping("/rules/{id}")
|
||||
public ApiResponse<RuleVO> getRule(@PathVariable Long id) {
|
||||
log.info("获取分账规则详情, id: {}", id);
|
||||
try {
|
||||
RuleVO rule = profitShareIntegrationService.getRule(id);
|
||||
return ApiResponse.success(rule);
|
||||
} catch (Exception e) {
|
||||
log.error("获取分账规则详情失败, id: {}", id, e);
|
||||
return ApiResponse.fail("获取分账规则详情失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新分账规则
|
||||
*/
|
||||
@PutMapping("/rules/{id}")
|
||||
public ApiResponse<RuleVO> updateRule(@PathVariable Long id, @Valid @RequestBody CreateRuleRequest request) {
|
||||
log.info("更新分账规则, id: {}", id);
|
||||
try {
|
||||
RuleVO rule = profitShareIntegrationService.updateRule(id, request);
|
||||
return ApiResponse.success(rule);
|
||||
} catch (Exception e) {
|
||||
log.error("更新分账规则失败, id: {}", id, e);
|
||||
return ApiResponse.fail("更新分账规则失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用分账规则
|
||||
*/
|
||||
@PutMapping("/rules/{id}/enable")
|
||||
public ApiResponse<String> enableRule(@PathVariable Long id) {
|
||||
log.info("启用分账规则, id: {}", id);
|
||||
try {
|
||||
profitShareIntegrationService.enableRule(id);
|
||||
return ApiResponse.success("规则已启用");
|
||||
} catch (Exception e) {
|
||||
log.error("启用分账规则失败, id: {}", id, e);
|
||||
return ApiResponse.fail("启用分账规则失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用分账规则
|
||||
*/
|
||||
@PutMapping("/rules/{id}/disable")
|
||||
public ApiResponse<String> disableRule(@PathVariable Long id) {
|
||||
log.info("禁用分账规则, id: {}", id);
|
||||
try {
|
||||
profitShareIntegrationService.disableRule(id);
|
||||
return ApiResponse.success("规则已禁用");
|
||||
} catch (Exception e) {
|
||||
log.error("禁用分账规则失败, id: {}", id, e);
|
||||
return ApiResponse.fail("禁用分账规则失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除分账规则
|
||||
*/
|
||||
@DeleteMapping("/rules/{id}")
|
||||
public ApiResponse<String> deleteRule(@PathVariable Long id) {
|
||||
log.info("删除分账规则, id: {}", id);
|
||||
try {
|
||||
profitShareIntegrationService.deleteRule(id);
|
||||
return ApiResponse.success("规则已删除");
|
||||
} catch (Exception e) {
|
||||
log.error("删除分账规则失败, id: {}", id, e);
|
||||
return ApiResponse.fail("删除分账规则失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 分账记录查询 ==========
|
||||
|
||||
/**
|
||||
* 查询景区分账记录
|
||||
*/
|
||||
@GetMapping("/records/scenic/{scenicId}")
|
||||
public ApiResponse<PageResponse<RecordVO>> getRecordsByScenic(
|
||||
@PathVariable Long scenicId,
|
||||
@RequestParam(defaultValue = "1") Integer page,
|
||||
@RequestParam(defaultValue = "10") Integer pageSize) {
|
||||
log.info("查询景区分账记录, scenicId: {}, page: {}, pageSize: {}", scenicId, page, pageSize);
|
||||
|
||||
// 参数验证:限制pageSize最大值为100
|
||||
if (pageSize > 100) {
|
||||
pageSize = 100;
|
||||
}
|
||||
|
||||
try {
|
||||
PageResponse<RecordVO> response = profitShareIntegrationService.getRecordsByScenic(scenicId, page, pageSize);
|
||||
return ApiResponse.success(response);
|
||||
} catch (Exception e) {
|
||||
log.error("查询景区分账记录失败, scenicId: {}", scenicId, e);
|
||||
return ApiResponse.fail("查询景区分账记录失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询分账记录详情
|
||||
*/
|
||||
@GetMapping("/records/{id}")
|
||||
public ApiResponse<RecordDetailVO> getRecordById(@PathVariable Long id) {
|
||||
log.info("查询分账记录详情, id: {}", id);
|
||||
try {
|
||||
RecordDetailVO record = profitShareIntegrationService.getRecordById(id);
|
||||
return ApiResponse.success(record);
|
||||
} catch (Exception e) {
|
||||
log.error("查询分账记录详情失败, id: {}", id, e);
|
||||
return ApiResponse.fail("查询分账记录详情失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按订单ID查询分账记录
|
||||
*/
|
||||
@GetMapping("/records/order/{orderId}")
|
||||
public ApiResponse<RecordDetailVO> getRecordByOrderId(@PathVariable String orderId) {
|
||||
log.info("按订单ID查询分账记录, orderId: {}", orderId);
|
||||
try {
|
||||
RecordDetailVO record = profitShareIntegrationService.getRecordByOrderId(orderId);
|
||||
return ApiResponse.success(record);
|
||||
} catch (Exception e) {
|
||||
log.error("按订单ID查询分账记录失败, orderId: {}", orderId, e);
|
||||
return ApiResponse.fail("按订单ID查询分账记录失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 手动分账与计算 ==========
|
||||
|
||||
/**
|
||||
* 手动触发分账
|
||||
*/
|
||||
@PostMapping("/manual")
|
||||
public ApiResponse<String> manualShare(@Valid @RequestBody ManualShareRequest request) {
|
||||
log.info("手动触发分账, orderId: {}", request.getOrderId());
|
||||
try {
|
||||
profitShareIntegrationService.manualShare(request.getOrderId());
|
||||
return ApiResponse.success("手动分账触发成功");
|
||||
} catch (Exception e) {
|
||||
log.error("手动触发分账失败, orderId: {}", request.getOrderId(), e);
|
||||
return ApiResponse.fail("手动触发分账失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算分账结果(不执行)
|
||||
*/
|
||||
@PostMapping("/calculate")
|
||||
public ApiResponse<CalculateResultVO> calculateShare(@Valid @RequestBody CalculateShareRequest request) {
|
||||
log.info("计算分账结果, scenicId: {}, totalAmount: {}", request.getScenicId(), request.getTotalAmount());
|
||||
try {
|
||||
CalculateResultVO result = profitShareIntegrationService.calculateShare(request);
|
||||
return ApiResponse.success(result);
|
||||
} catch (Exception e) {
|
||||
log.error("计算分账结果失败", e);
|
||||
return ApiResponse.fail("计算分账结果失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 类型查询 ==========
|
||||
|
||||
/**
|
||||
* 获取支持的类型列表
|
||||
*/
|
||||
@GetMapping("/types")
|
||||
public ApiResponse<TypesVO> getSupportedTypes() {
|
||||
log.info("获取支持的类型列表");
|
||||
try {
|
||||
TypesVO types = profitShareIntegrationService.getSupportedTypes();
|
||||
return ApiResponse.success(types);
|
||||
} catch (Exception e) {
|
||||
log.error("获取支持的类型列表失败", e);
|
||||
return ApiResponse.fail("获取支持的类型列表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,13 @@ public class ScenicAccountController {
|
||||
return result > 0 ? ApiResponse.success("更新成功") : ApiResponse.fail("更新失败");
|
||||
}
|
||||
|
||||
// 激活/停用景区账号
|
||||
@PostMapping("/updateActiveStatus/{id}")
|
||||
public ApiResponse updateActiveStatus(@PathVariable Long id) {
|
||||
int result = service.updateActiveStatus(id);
|
||||
return result > 0 ? ApiResponse.success("操作成功") : ApiResponse.fail("操作失败");
|
||||
}
|
||||
|
||||
// 更新景区账号
|
||||
@PostMapping("/update")
|
||||
public ApiResponse updateScenicAccount(@RequestBody ScenicAccountEntity entity) {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package com.ycwl.basic.controller.pc;
|
||||
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.ycwl.basic.constant.BaseContextHandler;
|
||||
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
|
||||
import com.ycwl.basic.model.mobile.statistic.req.CommonQueryReq;
|
||||
import com.ycwl.basic.model.pc.mp.MpConfigEntity;
|
||||
import com.ycwl.basic.model.pc.scenic.entity.ScenicAccountEntity;
|
||||
import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity;
|
||||
import com.ycwl.basic.model.pc.scenic.req.ScenicAddOrUpdateReq;
|
||||
import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery;
|
||||
import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO;
|
||||
import com.ycwl.basic.repository.ScenicRepository;
|
||||
import com.ycwl.basic.service.mobile.AppScenicService;
|
||||
import com.ycwl.basic.service.mobile.AppStatisticsService;
|
||||
import com.ycwl.basic.service.pc.ScenicAccountService;
|
||||
@@ -17,16 +16,20 @@ 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 com.ycwl.basic.repository.ScenicRepository;
|
||||
import com.ycwl.basic.model.pc.mp.MpConfigEntity;
|
||||
import org.apache.commons.lang3.Strings;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
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;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static com.ycwl.basic.constant.JwtRoleConstant.ADMIN;
|
||||
import static com.ycwl.basic.constant.JwtRoleConstant.MERCHANT;
|
||||
|
||||
/**
|
||||
@@ -50,68 +53,6 @@ public class ScenicController {
|
||||
@Autowired
|
||||
private ScenicAccountService accountService;
|
||||
|
||||
// 分页查询景区
|
||||
@PostMapping("/page")
|
||||
public ApiResponse<PageInfo<ScenicRespVO>> pageQuery(@RequestBody ScenicReqQuery scenicReqQuery) {
|
||||
return scenicService.pageQuery(scenicReqQuery);
|
||||
}
|
||||
// 查询景区列表
|
||||
@PostMapping("/list")
|
||||
public ApiResponse<List<ScenicRespVO>> list(@RequestBody ScenicReqQuery scenicReqQuery) {
|
||||
return scenicService.list(scenicReqQuery);
|
||||
}
|
||||
// 查询景区详情
|
||||
@GetMapping("/getDetail/{id}")
|
||||
public ApiResponse<ScenicRespVO> getDetail(@PathVariable Long id) {
|
||||
return scenicService.getById(id);
|
||||
}
|
||||
// 新增景区
|
||||
@PostMapping("/add")
|
||||
public ApiResponse<Boolean> add(@RequestBody ScenicAddOrUpdateReq scenicAddReq) {
|
||||
return scenicService.add(scenicAddReq);
|
||||
}
|
||||
// 删除景区
|
||||
@GetMapping("/delete/{id}")
|
||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
||||
return scenicService.deleteById(id);
|
||||
}
|
||||
// 修改景区
|
||||
@PostMapping("/update")
|
||||
public ApiResponse<Boolean> update(@RequestBody ScenicAddOrUpdateReq scenicAddReq) {
|
||||
return scenicService.update(scenicAddReq);
|
||||
}
|
||||
// 修改景区状态
|
||||
@GetMapping("/updateStatus/{id}")
|
||||
public ApiResponse<Boolean> updateStatus(@PathVariable Long id) {
|
||||
return scenicService.updateStatus(id);
|
||||
}
|
||||
// 新增景区配置
|
||||
@PostMapping("/addConfig")
|
||||
public ApiResponse<Boolean> addConfig(@RequestBody ScenicConfigEntity scenicConfig) {
|
||||
return scenicService.addConfig(scenicConfig);
|
||||
}
|
||||
// 修改景区配置
|
||||
@PostMapping("/updateConfig")
|
||||
public ApiResponse<Boolean> updateConfig(@RequestBody ScenicConfigEntity scenicConfig) {
|
||||
return scenicService.updateConfigById(scenicConfig);
|
||||
}
|
||||
|
||||
// 查询景区配置
|
||||
@GetMapping("/config/{id}")
|
||||
public ApiResponse<ScenicConfigEntity> getConfig(@PathVariable("id") Long id) {
|
||||
return ApiResponse.success(scenicService.getConfig(id));
|
||||
}
|
||||
@PostMapping("/saveConfig/{id}")
|
||||
public ApiResponse saveConfig(@PathVariable("id") Long id, @RequestBody ScenicConfigEntity config) {
|
||||
scenicService.saveConfig(id, config);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
@PostMapping("/saveConfig/undefined")
|
||||
public ApiResponse saveConfig(@RequestBody ScenicConfigEntity config) {
|
||||
scenicService.addConfig(config);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
// 根据景区ID下载小程序二维码
|
||||
@GetMapping("/{id}/QRCode")
|
||||
public ApiResponse<String> downloadQrCode(@PathVariable Long id) {
|
||||
@@ -167,19 +108,19 @@ public class ScenicController {
|
||||
}
|
||||
|
||||
@GetMapping("/myScenicList")
|
||||
public ApiResponse<List<ScenicRespVO>> myScenicList() {
|
||||
List<ScenicRespVO> list = Collections.emptyList();
|
||||
public ApiResponse<List<ScenicV2DTO>> myScenicList() {
|
||||
List<ScenicV2DTO> list = Collections.emptyList();
|
||||
if (Strings.CS.equals(BaseContextHandler.getRoleId(), MERCHANT.type)) {
|
||||
String userId = BaseContextHandler.getUserId();
|
||||
ScenicAccountEntity account = accountService.getScenicAccountById(Long.valueOf(userId));
|
||||
if (account == null || account.getScenicId().isEmpty()) {
|
||||
return ApiResponse.fail("景区账号未绑定景区");
|
||||
}
|
||||
list = account.getScenicId().stream().map(id -> {
|
||||
return appScenicService.getDetails(id).getData();
|
||||
}).toList();
|
||||
} else {
|
||||
list = scenicService.list(new ScenicReqQuery()).getData();
|
||||
list = account.getScenicId().stream().map(id -> scenicRepository.getScenicBasic(id)).toList();
|
||||
} else if (Strings.CS.equals(BaseContextHandler.getRoleId(), ADMIN.type)) {
|
||||
ScenicReqQuery query = new ScenicReqQuery();
|
||||
query.setPageSize(1000);
|
||||
list = scenicRepository.list(query);
|
||||
}
|
||||
return ApiResponse.success(list);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
package com.ycwl.basic.controller.pc;
|
||||
|
||||
import com.ycwl.basic.integration.scenic.dto.config.BatchConfigRequest;
|
||||
import com.ycwl.basic.integration.scenic.dto.config.BatchUpdateResponse;
|
||||
import com.ycwl.basic.integration.scenic.dto.config.CreateConfigRequest;
|
||||
import com.ycwl.basic.integration.scenic.dto.config.ScenicConfigV2DTO;
|
||||
import com.ycwl.basic.integration.scenic.dto.config.UpdateConfigRequest;
|
||||
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.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;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
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.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;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Author:longbinbin
|
||||
* @Date:2024/12/26
|
||||
* 景区管理 V2 版本控制器 - 基于 zt-scenic 集成服务
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/scenic/v2")
|
||||
@RequiredArgsConstructor
|
||||
public class ScenicV2Controller {
|
||||
|
||||
private final ScenicIntegrationService scenicIntegrationService;
|
||||
private final ScenicConfigIntegrationService scenicConfigIntegrationService;
|
||||
|
||||
// ========== 景区基础 CRUD 操作 ==========
|
||||
|
||||
/**
|
||||
* 景区V2核心信息分页列表
|
||||
*/
|
||||
@GetMapping("/")
|
||||
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) {
|
||||
log.info("分页查询景区核心信息列表, page: {}, pageSize: {}, status: {}, name: {}", page, pageSize, status, name);
|
||||
|
||||
// 参数验证:限制pageSize最大值为100
|
||||
if (pageSize > 100) {
|
||||
pageSize = 100;
|
||||
}
|
||||
|
||||
try {
|
||||
PageResponse<ScenicV2DTO> response = scenicIntegrationService.listScenics(page, pageSize, status, name, null);
|
||||
return ApiResponse.success(response);
|
||||
} catch (Exception e) {
|
||||
log.error("分页查询景区核心信息列表失败", e);
|
||||
return ApiResponse.fail("分页查询景区列表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询单个景区详情
|
||||
*/
|
||||
@GetMapping("/{scenicId}")
|
||||
public ApiResponse<ScenicV2DTO> getScenic(@PathVariable Long scenicId) {
|
||||
log.info("查询景区详情, scenicId: {}", scenicId);
|
||||
try {
|
||||
ScenicV2DTO scenic = scenicIntegrationService.getScenic(scenicId);
|
||||
return ApiResponse.success(scenic);
|
||||
} catch (Exception e) {
|
||||
log.error("查询景区详情失败, scenicId: {}", scenicId, e);
|
||||
return ApiResponse.fail("查询景区详情失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询景区列表(支持筛选和分页)- 高级筛选
|
||||
*/
|
||||
@PostMapping("/filter")
|
||||
public ApiResponse<ScenicFilterPageResponse> filterScenics(@RequestBody @Valid ScenicFilterRequest request) {
|
||||
log.info("高级筛选景区列表, 筛选条件数: {}, 页码: {}, 页大小: {}",
|
||||
request.getFilters().size(), request.getPage(), request.getPageSize());
|
||||
try {
|
||||
ScenicFilterPageResponse response = scenicIntegrationService.filterScenics(request);
|
||||
return ApiResponse.success(response);
|
||||
} catch (Exception e) {
|
||||
log.error("高级筛选景区列表失败", e);
|
||||
return ApiResponse.fail("高级筛选景区列表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增景区
|
||||
*/
|
||||
@PostMapping("/create")
|
||||
public ApiResponse<ScenicV2DTO> createScenic(@RequestBody @Valid CreateScenicRequest request) {
|
||||
log.info("新增景区, name: {}, mpId: {}", request.getName(), request.getMpId());
|
||||
try {
|
||||
ScenicV2DTO scenic = scenicIntegrationService.createScenic(request);
|
||||
return ApiResponse.success(scenic);
|
||||
} catch (Exception e) {
|
||||
log.error("新增景区失败, name: {}", request.getName(), e);
|
||||
return ApiResponse.fail("新增景区失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改景区
|
||||
*/
|
||||
@PutMapping("/{scenicId}")
|
||||
public ApiResponse<ScenicV2DTO> updateScenic(@PathVariable Long scenicId,
|
||||
@RequestBody @Valid UpdateScenicRequest request) {
|
||||
log.info("修改景区, scenicId: {}", scenicId);
|
||||
try {
|
||||
ScenicV2DTO scenic = scenicIntegrationService.updateScenic(scenicId, request);
|
||||
return ApiResponse.success(scenic);
|
||||
} catch (Exception e) {
|
||||
log.error("修改景区失败, scenicId: {}", scenicId, e);
|
||||
return ApiResponse.fail("修改景区失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除景区
|
||||
*/
|
||||
@DeleteMapping("/{scenicId}")
|
||||
public ApiResponse<Void> deleteScenic(@PathVariable Long scenicId) {
|
||||
log.info("删除景区, scenicId: {}", scenicId);
|
||||
try {
|
||||
scenicIntegrationService.deleteScenic(scenicId);
|
||||
return ApiResponse.success(null);
|
||||
} catch (Exception e) {
|
||||
log.error("删除景区失败, scenicId: {}", scenicId, e);
|
||||
return ApiResponse.fail("删除景区失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 景区列表查询(默认1000条)
|
||||
* 只支持根据状态筛选
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
public ApiResponse<PageResponse<ScenicV2DTO>> listScenicsByStatus(@RequestParam(required = false) Integer status) {
|
||||
log.info("查询景区列表, status: {}", status);
|
||||
try {
|
||||
// 默认查询1000条数据,第1页
|
||||
PageResponse<ScenicV2DTO> scenics = scenicIntegrationService.listScenics(1, 1000, status, null, null);
|
||||
return ApiResponse.success(scenics);
|
||||
} catch (Exception e) {
|
||||
log.error("查询景区列表失败, status: {}", status, e);
|
||||
return ApiResponse.fail("查询景区列表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ========== 景区配置管理 ==========
|
||||
|
||||
/**
|
||||
* 获取景区配置列表
|
||||
*/
|
||||
@GetMapping("/{scenicId}/config")
|
||||
public ApiResponse<List<ScenicConfigV2DTO>> listConfigs(@PathVariable Long scenicId) {
|
||||
log.info("获取景区配置列表, scenicId: {}", scenicId);
|
||||
try {
|
||||
List<ScenicConfigV2DTO> configs = scenicConfigIntegrationService.listConfigs(scenicId);
|
||||
return ApiResponse.success(configs);
|
||||
} catch (Exception e) {
|
||||
log.error("获取景区配置列表失败, scenicId: {}", scenicId, e);
|
||||
return ApiResponse.fail("获取景区配置列表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据配置键获取配置
|
||||
*/
|
||||
@GetMapping("/{scenicId}/config/{configKey}")
|
||||
public ApiResponse<ScenicConfigV2DTO> getConfigByKey(@PathVariable Long scenicId,
|
||||
@PathVariable String configKey) {
|
||||
log.info("根据键获取景区配置, scenicId: {}, configKey: {}", scenicId, configKey);
|
||||
try {
|
||||
ScenicConfigV2DTO config = scenicConfigIntegrationService.getConfigByKey(scenicId, configKey);
|
||||
return ApiResponse.success(config);
|
||||
} catch (Exception e) {
|
||||
log.error("根据键获取景区配置失败, scenicId: {}, configKey: {}", scenicId, configKey, e);
|
||||
return ApiResponse.fail("获取配置失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建景区配置
|
||||
*/
|
||||
@PostMapping("/{scenicId}/config")
|
||||
public ApiResponse<ScenicConfigV2DTO> createConfig(@PathVariable Long scenicId,
|
||||
@RequestBody @Valid CreateConfigRequest request) {
|
||||
log.info("创建景区配置, scenicId: {}, configKey: {}", scenicId, request.getConfigKey());
|
||||
try {
|
||||
ScenicConfigV2DTO config = scenicConfigIntegrationService.createConfig(scenicId, request);
|
||||
return ApiResponse.success(config);
|
||||
} catch (Exception e) {
|
||||
log.error("创建景区配置失败, scenicId: {}, configKey: {}", scenicId, request.getConfigKey(), e);
|
||||
return ApiResponse.fail("创建配置失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新景区配置
|
||||
*/
|
||||
@PutMapping("/{scenicId}/config/{configId}")
|
||||
public ApiResponse<ScenicConfigV2DTO> updateConfig(@PathVariable Long scenicId,
|
||||
@PathVariable String configId,
|
||||
@RequestBody @Valid UpdateConfigRequest request) {
|
||||
log.info("更新景区配置, scenicId: {}, configId: {}", scenicId, configId);
|
||||
try {
|
||||
ScenicConfigV2DTO config = scenicConfigIntegrationService.updateConfig(scenicId, configId, request);
|
||||
return ApiResponse.success(config);
|
||||
} catch (Exception e) {
|
||||
log.error("更新景区配置失败, scenicId: {}, configId: {}", scenicId, configId, e);
|
||||
return ApiResponse.fail("更新配置失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除景区配置
|
||||
*/
|
||||
@DeleteMapping("/{scenicId}/config/{configId}")
|
||||
public ApiResponse<Void> deleteConfig(@PathVariable Long scenicId, @PathVariable String configId) {
|
||||
log.info("删除景区配置, scenicId: {}, configId: {}", scenicId, configId);
|
||||
try {
|
||||
scenicConfigIntegrationService.deleteConfig(scenicId, configId);
|
||||
return ApiResponse.success(null);
|
||||
} catch (Exception e) {
|
||||
log.error("删除景区配置失败, scenicId: {}, configId: {}", scenicId, configId, e);
|
||||
return ApiResponse.fail("删除配置失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新景区配置
|
||||
*/
|
||||
@PutMapping("/{scenicId}/config/batch")
|
||||
public ApiResponse<BatchUpdateResponse> batchUpdateConfigs(@PathVariable Long scenicId,
|
||||
@RequestBody @Valid BatchConfigRequest request) {
|
||||
log.info("批量更新景区配置, scenicId: {}, configs count: {}", scenicId, request.getConfigs().size());
|
||||
try {
|
||||
BatchUpdateResponse response = scenicConfigIntegrationService.batchUpdateConfigs(scenicId, request);
|
||||
return ApiResponse.success(response);
|
||||
} catch (Exception e) {
|
||||
log.error("批量更新景区配置失败, scenicId: {}", scenicId, e);
|
||||
return ApiResponse.fail("批量更新配置失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,16 @@ import com.ycwl.basic.annotation.IgnoreToken;
|
||||
import com.ycwl.basic.model.jwt.JwtInfo;
|
||||
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
|
||||
import com.ycwl.basic.model.pc.source.req.SourceReqQuery;
|
||||
import com.ycwl.basic.model.printer.req.CreateVirtualOrderRequest;
|
||||
import com.ycwl.basic.service.pc.SourceService;
|
||||
import com.ycwl.basic.service.printer.PrinterService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import com.ycwl.basic.utils.JwtTokenUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Author:longbinbin
|
||||
* @Date:2024/12/3 15:45
|
||||
@@ -21,8 +25,9 @@ public class SourceController {
|
||||
|
||||
@Autowired
|
||||
private SourceService sourceService;
|
||||
@Autowired
|
||||
private PrinterService printerService;
|
||||
|
||||
@Deprecated
|
||||
// 分页查询视频源
|
||||
@PostMapping("/page")
|
||||
public ApiResponse pageQuery(@RequestBody SourceReqQuery sourceReqQuery) {
|
||||
@@ -46,5 +51,28 @@ public class SourceController {
|
||||
return sourceService.deleteById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建虚拟用户0元订单
|
||||
* 用于后台直接从source创建订单,不需要真实用户
|
||||
*
|
||||
* @param request 请求参数
|
||||
* @return 订单信息
|
||||
*/
|
||||
@PostMapping("/createVirtualOrder")
|
||||
public ApiResponse<Map<String, Object>> createVirtualOrder(@RequestBody CreateVirtualOrderRequest request) {
|
||||
try {
|
||||
Map<String, Object> result = printerService.createVirtualOrder(
|
||||
request.getSourceId(),
|
||||
request.getScenicId(),
|
||||
request.getPrinterId(),
|
||||
request.getNeedEnhance(),
|
||||
request.getPrintImgUrl()
|
||||
);
|
||||
return ApiResponse.success(result);
|
||||
} catch (Exception e) {
|
||||
return ApiResponse.fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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,122 @@
|
||||
package com.ycwl.basic.controller.pc;
|
||||
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.ycwl.basic.model.pc.notify.entity.WechatSubscribeEventTemplateEntity;
|
||||
import com.ycwl.basic.model.pc.notify.entity.WechatSubscribeSceneTemplateEntity;
|
||||
import com.ycwl.basic.model.pc.notify.entity.WechatSubscribeSendLogEntity;
|
||||
import com.ycwl.basic.model.pc.notify.entity.WechatSubscribeTemplateConfigEntity;
|
||||
import com.ycwl.basic.model.pc.notify.req.WechatSubscribeEventTemplatePageReq;
|
||||
import com.ycwl.basic.model.pc.notify.req.WechatSubscribeEventTemplateSaveReq;
|
||||
import com.ycwl.basic.model.pc.notify.req.WechatSubscribeSceneTemplatePageReq;
|
||||
import com.ycwl.basic.model.pc.notify.req.WechatSubscribeSceneTemplateSaveReq;
|
||||
import com.ycwl.basic.model.pc.notify.req.WechatSubscribeSendLogPageReq;
|
||||
import com.ycwl.basic.model.pc.notify.req.WechatSubscribeTemplateConfigPageReq;
|
||||
import com.ycwl.basic.model.pc.notify.req.WechatSubscribeTemplateConfigSaveReq;
|
||||
import com.ycwl.basic.service.pc.WechatSubscribeNotifyAdminService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 微信小程序订阅消息:配置管理(管理后台)
|
||||
*
|
||||
* @Author: System
|
||||
* @Date: 2025/12/31
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/wechatSubscribeNotify/v1")
|
||||
@RequiredArgsConstructor
|
||||
public class WechatSubscribeNotifyAdminController {
|
||||
|
||||
private final WechatSubscribeNotifyAdminService adminService;
|
||||
|
||||
// ========================= 模板配置 =========================
|
||||
|
||||
@PostMapping("/templateConfig/page")
|
||||
public ApiResponse<PageInfo<WechatSubscribeTemplateConfigEntity>> pageTemplateConfig(
|
||||
@RequestBody WechatSubscribeTemplateConfigPageReq req) {
|
||||
return adminService.pageTemplateConfig(req);
|
||||
}
|
||||
|
||||
@GetMapping("/templateConfig/detail/{id}")
|
||||
public ApiResponse<WechatSubscribeTemplateConfigEntity> getTemplateConfig(@PathVariable("id") Long id) {
|
||||
return adminService.getTemplateConfig(id);
|
||||
}
|
||||
|
||||
@PostMapping("/templateConfig/save")
|
||||
public ApiResponse<Boolean> saveTemplateConfig(@RequestBody WechatSubscribeTemplateConfigSaveReq req) {
|
||||
return adminService.saveTemplateConfig(req);
|
||||
}
|
||||
|
||||
@DeleteMapping("/templateConfig/delete/{id}")
|
||||
public ApiResponse<Boolean> deleteTemplateConfig(@PathVariable("id") Long id) {
|
||||
return adminService.deleteTemplateConfig(id);
|
||||
}
|
||||
|
||||
// ========================= 场景映射 =========================
|
||||
|
||||
@PostMapping("/sceneTemplate/page")
|
||||
public ApiResponse<PageInfo<WechatSubscribeSceneTemplateEntity>> pageSceneTemplate(
|
||||
@RequestBody WechatSubscribeSceneTemplatePageReq req) {
|
||||
return adminService.pageSceneTemplate(req);
|
||||
}
|
||||
|
||||
@GetMapping("/sceneTemplate/detail/{id}")
|
||||
public ApiResponse<WechatSubscribeSceneTemplateEntity> getSceneTemplate(@PathVariable("id") Long id) {
|
||||
return adminService.getSceneTemplate(id);
|
||||
}
|
||||
|
||||
@PostMapping("/sceneTemplate/save")
|
||||
public ApiResponse<Boolean> saveSceneTemplate(@RequestBody WechatSubscribeSceneTemplateSaveReq req) {
|
||||
return adminService.saveSceneTemplate(req);
|
||||
}
|
||||
|
||||
@DeleteMapping("/sceneTemplate/delete/{id}")
|
||||
public ApiResponse<Boolean> deleteSceneTemplate(@PathVariable("id") Long id) {
|
||||
return adminService.deleteSceneTemplate(id);
|
||||
}
|
||||
|
||||
// ========================= 事件映射 =========================
|
||||
|
||||
@PostMapping("/eventTemplate/page")
|
||||
public ApiResponse<PageInfo<WechatSubscribeEventTemplateEntity>> pageEventTemplate(
|
||||
@RequestBody WechatSubscribeEventTemplatePageReq req) {
|
||||
return adminService.pageEventTemplate(req);
|
||||
}
|
||||
|
||||
@GetMapping("/eventTemplate/detail/{id}")
|
||||
public ApiResponse<WechatSubscribeEventTemplateEntity> getEventTemplate(@PathVariable("id") Long id) {
|
||||
return adminService.getEventTemplate(id);
|
||||
}
|
||||
|
||||
@PostMapping("/eventTemplate/save")
|
||||
public ApiResponse<Boolean> saveEventTemplate(@RequestBody WechatSubscribeEventTemplateSaveReq req) {
|
||||
return adminService.saveEventTemplate(req);
|
||||
}
|
||||
|
||||
@DeleteMapping("/eventTemplate/delete/{id}")
|
||||
public ApiResponse<Boolean> deleteEventTemplate(@PathVariable("id") Long id) {
|
||||
return adminService.deleteEventTemplate(id);
|
||||
}
|
||||
|
||||
// ========================= 发送日志 =========================
|
||||
|
||||
@PostMapping("/sendLog/page")
|
||||
public ApiResponse<PageInfo<WechatSubscribeSendLogEntity>> pageSendLog(@RequestBody WechatSubscribeSendLogPageReq req) {
|
||||
return adminService.pageSendLog(req);
|
||||
}
|
||||
|
||||
@GetMapping("/sendLog/detail/{id}")
|
||||
public ApiResponse<WechatSubscribeSendLogEntity> getSendLog(@PathVariable("id") Long id) {
|
||||
return adminService.getSendLog(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
package com.ycwl.basic.controller.printer;
|
||||
|
||||
|
||||
import com.ycwl.basic.annotation.IgnoreToken;
|
||||
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.mobile.face.FaceRecognizeResp;
|
||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||
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.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.utils.ApiResponse;
|
||||
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.List;
|
||||
|
||||
@IgnoreToken
|
||||
// 打印机大屏对接接口
|
||||
@RestController
|
||||
@RequestMapping("/printer/v1/tv")
|
||||
@RequiredArgsConstructor
|
||||
public class PrinterTvController {
|
||||
|
||||
private final DeviceRepository deviceRepository;
|
||||
private final ScenicRepository scenicRepository;
|
||||
private final FaceRepository faceRepository;
|
||||
private final FaceService pcFaceService;
|
||||
private final SourceMapper sourceMapper;
|
||||
|
||||
/**
|
||||
* 获取景区列表
|
||||
*
|
||||
* @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 {
|
||||
String url = pcFaceService.bindWxaCode(faceId);
|
||||
response.sendRedirect(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据人脸ID查询图像素材
|
||||
*
|
||||
* @param faceId 人脸ID
|
||||
* @param type 素材类型(默认为2-图片)
|
||||
* @return 匹配的source记录
|
||||
*/
|
||||
@GetMapping("/{faceId}/source")
|
||||
public ApiResponse<List<SourceEntity>> getSourceByFaceId(@PathVariable Long faceId, @RequestParam(name = "type", required = false, defaultValue = "2") Integer type) {
|
||||
List<SourceEntity> sources = sourceMapper.listSourceByFaceRelation(faceId, type);
|
||||
return ApiResponse.success(sources);
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印机大屏人脸识别
|
||||
* 上传照片,在景区人脸库中搜索匹配的人脸样本,返回识别结果
|
||||
*
|
||||
* 使用 USER_FACE_DB_NAME+scenicId 对人脸进行去重检测:
|
||||
* - 如果已存在相同人脸(打印机大屏用户,memberId=0),则返回已存在的 faceId
|
||||
* - 否则创建新的人脸记录并添加到人脸库
|
||||
*
|
||||
* @param file 人脸照片文件
|
||||
* @param scenicId 景区ID
|
||||
* @return 人脸识别结果
|
||||
*/
|
||||
@PostMapping("/{scenicId}/faceRecognize")
|
||||
public ApiResponse<FaceRecognizeResp> faceRecognize(
|
||||
@RequestParam("file") MultipartFile file,
|
||||
@PathVariable Long scenicId) {
|
||||
// 复用 faceUpload 方法的去重逻辑
|
||||
// memberId=0L 表示打印机大屏用户,scene="tv" 为试点场景:仅执行识别/补救/落库/建关系
|
||||
FaceRecognizeResp resp = pcFaceService.faceUpload(file, scenicId, 0L, "tv");
|
||||
return ApiResponse.success(resp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过人脸样本ID重定向到人脸图片URL
|
||||
*
|
||||
* @param faceSampleId 人脸样本ID
|
||||
* @param response HTTP响应
|
||||
*/
|
||||
@GetMapping("/faceSample/{faceSampleId}/url")
|
||||
public void redirectToFaceSampleUrl(@PathVariable Long faceSampleId, HttpServletResponse response) throws Exception {
|
||||
FaceSampleEntity faceSample = faceRepository.getFaceSample(faceSampleId);
|
||||
if (faceSample == null || faceSample.getFaceUrl() == null) {
|
||||
response.setStatus(404);
|
||||
return;
|
||||
}
|
||||
response.sendRedirect(faceSample.getFaceUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过人脸ID重定向到人脸图片URL
|
||||
*
|
||||
* @param faceId 人脸ID
|
||||
* @param response HTTP响应
|
||||
*/
|
||||
@GetMapping("/face/{faceId}/url")
|
||||
public void redirectToFaceUrl(@PathVariable Long faceId, HttpServletResponse response) throws Exception {
|
||||
FaceEntity face = faceRepository.getFace(faceId);
|
||||
if (face == null || face.getFaceUrl() == null) {
|
||||
response.setStatus(404);
|
||||
return;
|
||||
}
|
||||
response.sendRedirect(face.getFaceUrl());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
54
src/main/java/com/ycwl/basic/dto/MobileOrderRequest.java
Normal file
54
src/main/java/com/ycwl/basic/dto/MobileOrderRequest.java
Normal file
@@ -0,0 +1,54 @@
|
||||
package com.ycwl.basic.dto;
|
||||
|
||||
import com.ycwl.basic.pricing.dto.ProductItem;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 移动端下单请求DTO
|
||||
*/
|
||||
@Data
|
||||
public class MobileOrderRequest {
|
||||
|
||||
/**
|
||||
* 商品列表
|
||||
*/
|
||||
private List<ProductItem> products;
|
||||
|
||||
/**
|
||||
* 人脸ID(必填,用于权限验证)
|
||||
*/
|
||||
private Long faceId;
|
||||
|
||||
/**
|
||||
* 预期原价(用于价格验证)
|
||||
*/
|
||||
private BigDecimal expectedOriginalAmount;
|
||||
|
||||
/**
|
||||
* 预期最终价格(用于价格验证)
|
||||
*/
|
||||
private BigDecimal expectedFinalAmount;
|
||||
|
||||
/**
|
||||
* 是否自动使用优惠券
|
||||
*/
|
||||
private Boolean autoUseCoupon = true;
|
||||
|
||||
/**
|
||||
* 用户输入的券码
|
||||
*/
|
||||
private String voucherCode;
|
||||
|
||||
/**
|
||||
* 是否自动使用券码优惠
|
||||
*/
|
||||
private Boolean autoUseVoucher = true;
|
||||
|
||||
/**
|
||||
* 订单备注
|
||||
*/
|
||||
private String remarks;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
65
src/main/java/com/ycwl/basic/enums/FaceCutStatus.java
Normal file
65
src/main/java/com/ycwl/basic/enums/FaceCutStatus.java
Normal file
@@ -0,0 +1,65 @@
|
||||
package com.ycwl.basic.enums;
|
||||
|
||||
/**
|
||||
* 人脸视频切片状态枚举
|
||||
*/
|
||||
public enum FaceCutStatus {
|
||||
|
||||
/**
|
||||
* 正在切片中
|
||||
*/
|
||||
CUTTING(0, "正在切片中"),
|
||||
|
||||
/**
|
||||
* 切片已完成
|
||||
*/
|
||||
COMPLETED(1, "切片已完成"),
|
||||
|
||||
/**
|
||||
* 等待用户选择模板
|
||||
*/
|
||||
WAITING_USER_SELECT(2, "等待用户选择模板");
|
||||
|
||||
private final int code;
|
||||
private final String description;
|
||||
|
||||
FaceCutStatus(int code, String description) {
|
||||
this.code = code;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据code获取枚举
|
||||
*/
|
||||
public static FaceCutStatus fromCode(int code) {
|
||||
for (FaceCutStatus status : values()) {
|
||||
if (status.code == code) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown FaceCutStatus code: " + code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据code获取枚举,如果不存在则返回默认值
|
||||
* @param code 状态码
|
||||
* @param defaultStatus 默认状态
|
||||
* @return 枚举值
|
||||
*/
|
||||
public static FaceCutStatus fromCodeOrDefault(int code, FaceCutStatus defaultStatus) {
|
||||
for (FaceCutStatus status : values()) {
|
||||
if (status.code == code) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
return defaultStatus;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user