mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-06-17 11:14:23 +00:00
Compare commits
901 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7701d359dc | |||
| ffd9ef8a32 | |||
| 36cd5ab171 | |||
| 58d86fa065 | |||
| df71cf901b | |||
| 02d366e2c7 | |||
| 6409531a64 | |||
| 175ab79b27 | |||
| b543953d22 | |||
| b4fef68af5 | |||
| 6c48a38212 | |||
| 8a3e4d8e98 | |||
| cd73747164 | |||
| 0ee58df868 | |||
| 6fed8131f6 | |||
| d75c08396a | |||
| 3a742fbb00 | |||
| 9c2b1f6e98 | |||
| 28b33d8c44 | |||
| 1f99a10322 | |||
| 743c3dbc72 | |||
| d46a446f2b | |||
| 75a785f612 | |||
| e79b75f340 | |||
| 1f6f470ab5 | |||
| 583d4042f5 | |||
| 8437c1c714 | |||
| 2613fe6cf1 | |||
| a15d541b72 | |||
| 8657a06f52 | |||
| 09900b34aa | |||
| 4e1c3f5839 | |||
| d97cc7df5e | |||
| 151242f3ba | |||
| b2783a0168 | |||
| c79bcac217 | |||
| 9a06da3bc1 | |||
| 98bbc36453 | |||
| 4a04f4ec17 | |||
| 77be2bfebb | |||
| cf4ca89e19 | |||
| 094fc78d92 | |||
| da3d2a6a71 | |||
| 15cba0c96e | |||
| 98a79acad9 | |||
| 4947978f81 | |||
| 24cc479a56 | |||
| 8ee1347b17 | |||
| 7e95120341 | |||
| 2f261983ac | |||
| e8e7b9a245 | |||
| d2bd949ac8 | |||
| 605654ec99 | |||
| 88127fcf34 | |||
| 0a82f0036b | |||
| 3a979277e4 | |||
| 1c582fbbf1 | |||
| 92452da19a | |||
| a2ccaae7aa | |||
| 23371d4433 | |||
| e05d65cf49 | |||
| 809a78fee3 | |||
| aaf9b35a45 | |||
| 61c0336a78 | |||
| 69c913394f | |||
| 0ed9ad2f2a | |||
| 67e912381b | |||
| a6a1c72a37 | |||
| d77baa8a93 | |||
| fce4832407 | |||
| 91c8cc9607 | |||
| 02273e018f | |||
| 4af10ecf79 | |||
| d34ed3c058 | |||
| 8372e06949 | |||
| a74cb0c272 | |||
| 5388767a2f | |||
| 97dda9601e | |||
| cddbb602bf | |||
| f21ed1f062 | |||
| c009616f74 | |||
| 84ab87878a | |||
| c53f91913c | |||
| feed97154a | |||
| f69562516d | |||
| 0b8e550097 | |||
| cf722f5707 | |||
| 67e54c5106 | |||
| d3dcc98122 | |||
| c648d4cf39 | |||
| 41a5eda311 | |||
| c6e0dc6a1d | |||
| 92e630df22 | |||
| c6b0f32b09 | |||
| 5f5b6f84a8 | |||
| cd57d478f2 | |||
| da07313df8 | |||
| c08ecb5e33 | |||
| 0a722c81c5 | |||
| 15071471b2 | |||
| 98a9939738 | |||
| 9554030054 | |||
| 72e9a77373 | |||
| ed7dcd9414 | |||
| 79fe8b5997 | |||
| cfce4d7d1d | |||
| b7f5d8485c | |||
| d4677a5799 | |||
| 99644046fc | |||
| 22c9bd7d39 | |||
| 3fc6929075 | |||
| 4eed6889d6 | |||
| 959b0f6a48 | |||
| 91ffacc335 | |||
| 4969a759aa | |||
| 81e3a80d35 | |||
| d717ce03c1 | |||
| 66035447a8 | |||
| fa1148bc4e | |||
| f60f184c84 | |||
| 06ffb180fe | |||
| 1b892828f1 | |||
| 1aa88ab0fe | |||
| 86126699d3 | |||
| a6a07b9bda | |||
| d8b3c68150 | |||
| 318b481c4b | |||
| 7338b891db | |||
| eb18dc8e94 | |||
| aec5321f89 | |||
| 2e658f37a4 | |||
| 7ccb2a44e3 | |||
| 0fa08e2260 | |||
| 38a49f7414 | |||
| fb89c9fb25 | |||
| e9458f5419 | |||
| 2d87c8f23d | |||
| cb281e4030 | |||
| 9b85d77158 | |||
| a3b28eafe4 | |||
| 805a8df7d3 | |||
| 02be045f55 | |||
| ac3c7b7bec | |||
| c344b5b4ae | |||
| e7a1e1d159 | |||
| 30b56e54cf | |||
| cc525c1c27 | |||
| 3f2829cd37 | |||
| 3258a42b44 | |||
| a64fd71bbe | |||
| 1a12bec7b1 | |||
| fbca19791a | |||
| 10b6fdd1cb | |||
| 7dd1f1b3d0 | |||
| df92ff664c | |||
| 73168c1859 | |||
| 77856ff9af | |||
| df49b21620 | |||
| fbe2c66955 | |||
| df7ca77652 | |||
| fe84e3f2fa | |||
| c307732696 | |||
| 35bcd2cdc2 | |||
| a29bf7d860 | |||
| 27393c15f2 | |||
| c91a787f29 | |||
| 6096712291 | |||
| 6d20addcde | |||
| d8f9fd670c | |||
| 5ebe739917 | |||
| 022edc866a | |||
| b06815cc59 | |||
| f1b223a3c9 | |||
| e65273daa6 | |||
| d3a2acb605 | |||
| bced24e47d | |||
| 425ad03e6f | |||
| 4462d8f711 | |||
| 1b31977ec6 | |||
| 42693cb1ff | |||
| 6b500466fc | |||
| c84262eb88 | |||
| fa2ffaa64a | |||
| 3037c856d0 | |||
| 7b1ce3f070 | |||
| f4a15b3448 | |||
| 239f1f8c84 | |||
| ac303184c4 | |||
| 7f16bbdb94 | |||
| f1c83b0f68 | |||
| 22c2b1564d | |||
| 628d28c12d | |||
| 2577992ee3 | |||
| 5b837c9d7f | |||
| 017ad5bf54 | |||
| f076b70e9b | |||
| 62d12ad2a7 | |||
| 923854bbc6 | |||
| 9ca5d7b167 | |||
| 9c3e1d17f0 | |||
| 7906062945 | |||
| 785c36d339 | |||
| 197cbca99c | |||
| b1076d7256 | |||
| ce94cd7e73 | |||
| 90147d6cd9 | |||
| 2c187cf2cd | |||
| 0b6d4f9709 | |||
| cf3b6d8fc7 | |||
| 8d98c876d2 | |||
| df4df1933a | |||
| 7507f1bb03 | |||
| 9b4c36c76a | |||
| edfc81aeb0 | |||
| 7bd1225b27 | |||
| 2dd56e27af | |||
| c3ecef3609 | |||
| efc74d0f77 | |||
| f27cb5c703 | |||
| a756c2fab3 | |||
| 4e2171a8a6 | |||
| bcbdff8768 | |||
| b976a1f46f | |||
| b9fd9711de | |||
| 642a527dcf | |||
| 88afcc5a8e | |||
| 2c5462cd97 | |||
| 2f29946b11 | |||
| e27aa34cfd | |||
| 2322b2da15 | |||
| 79261054f9 | |||
| 86633e1f21 | |||
| 784598a6f0 | |||
| fdad0e5d34 | |||
| ebf63c4072 | |||
| 354d6bdaf9 | |||
| d9aebdebdc | |||
| d6f6495b35 | |||
| 300f8705ef | |||
| 1f74a29dce | |||
| 27ef792b11 | |||
| 8dd2d59617 | |||
| 077ba448d7 | |||
| 9ce85f2769 | |||
| f5557cbf08 | |||
| e042c499e1 | |||
| e01afb168c | |||
| c1d81eb1d1 | |||
| 2b0b429866 | |||
| 8ea85d78ee | |||
| 3b506fe8a8 | |||
| 3cc7a4c01a | |||
| 2e749a5672 | |||
| 7d553d7750 | |||
| 16105cef54 | |||
| 2b824d94f2 | |||
| 00d3c563e2 | |||
| b26891261c | |||
| c1d19b854b | |||
| 72e7ccf262 | |||
| 84ca6fd28c | |||
| d1c148c5c4 | |||
| ef58630dae | |||
| f025e82e7c | |||
| 4380a988f7 | |||
| 2899f7af48 | |||
| d4b05256a3 | |||
| 57a26e375d | |||
| 8a202c4fba | |||
| 089b2a3f5f | |||
| 0b3d7a21d5 | |||
| fe8a705a28 | |||
| 974c7ba83e | |||
| f2937d735d | |||
| 423248c574 | |||
| 5126cfda8c | |||
| e009875797 | |||
| 04ff17f796 | |||
| e9c9fbd742 | |||
| b385945c2d | |||
| 24cbed2eda | |||
| ba073b71a6 | |||
| 5ff098ea21 | |||
| f6713b956e | |||
| b8ea12646f | |||
| e573e54c2b | |||
| 8ec005d392 | |||
| b1f92f61a6 | |||
| 824b4dd8aa | |||
| 6b08db7e58 | |||
| 6f3830b3f7 | |||
| d70dad723f | |||
| 2cf89e4802 | |||
| 1fc6460ae0 | |||
| a04e5c2f6f | |||
| 77b26937f5 | |||
| a1134b9d4b | |||
| 600f6ac1d1 | |||
| 9ad50b35c9 | |||
| 867ee3907b | |||
| 58fcd42745 | |||
| 0ee62a3a04 | |||
| f0bc7a22a0 | |||
| f6c0c8e226 | |||
| 8f3c0d6710 | |||
| 4f738778db | |||
| 84b45f785d | |||
| df56d7e885 | |||
| 76176e135c | |||
| ab87e0e51c | |||
| 5346a063bf | |||
| e53f2130b8 | |||
| 1e87e9252d | |||
| 3fc4d29dce | |||
| bcdac9d9b2 | |||
| ea9710d16f | |||
| 47134cadc2 | |||
| 1a1b20b9cf | |||
| b63ebb8fae | |||
| e0f7299a86 | |||
| 1f9ae8d057 | |||
| da1ad73cf6 | |||
| 53c603f33a | |||
| 06f86f2b21 | |||
| 22693bfdd9 | |||
| 0058f20b1e | |||
| 304d941d68 | |||
| 3dbcd2ac4d | |||
| 2efe4e733a | |||
| 08239a16b8 | |||
| cb49dc9b73 | |||
| 43d4c9be43 | |||
| 1dc13698ad | |||
| d58432dcd9 | |||
| e7ff73c7f9 | |||
| 4ee9532d5f | |||
| 80c3fd8ea2 | |||
| 7e277d06d5 | |||
| d2b68119bd | |||
| f7b0d7edd5 | |||
| cdea1ab911 | |||
| ada6bfb5cf | |||
| 928dbd73b5 | |||
| 8c1a7afc6e | |||
| 87453f7198 | |||
| 48e3593ef9 | |||
| 655e8f2a65 | |||
| 7a0afedc7c | |||
| 902fce5174 | |||
| 0034839e8d | |||
| 148fd36fd1 | |||
| 06cd663eaf | |||
| 0edbeabac2 | |||
| 65cc3ee58b | |||
| 6965fcfb7f | |||
| 40520c30ec | |||
| 5d7ca3d29a | |||
| a3aec1133b | |||
| 8fa715477b | |||
| 9209ebea4c | |||
| 47a9ce5843 | |||
| dfef13e2be | |||
| 2f4d6e68da | |||
| 414872f61e | |||
| 82475f71db | |||
| a6874e9be3 | |||
| 720031770d | |||
| eb7a25434f | |||
| bda4b24cf0 | |||
| 4dedb70d54 | |||
| aea4f59af7 | |||
| 84ed778dc0 | |||
| 6ca1862034 | |||
| b3ea41ad1e | |||
| 210d3dfa6f | |||
| 80ecb1620d | |||
| b094f2f287 | |||
| 02076e24e5 | |||
| d195d2f624 | |||
| 8b12402e89 | |||
| d72709ca4d | |||
| 9878c12e33 | |||
| 84c1833923 | |||
| 08a2678bd5 | |||
| ae46cbf216 | |||
| 5fee90dfae | |||
| 5a5d5add23 | |||
| cf4c427335 | |||
| 85fb1b8a27 | |||
| 72282b1a2f | |||
| 499488c22a | |||
| e2d812246a | |||
| c7d99885dc | |||
| f977f96407 | |||
| fb7d134b27 | |||
| 98d85a0573 | |||
| 6c2a7f7957 | |||
| 2ebccb40f5 | |||
| 6342b8f3a6 | |||
| 20585201dd | |||
| 1c4df40f12 | |||
| 31cff70f63 | |||
| 678626a3d7 | |||
| 05bf33dacd | |||
| 4cf7f1dacf | |||
| 5871d74c4f | |||
| 1e3e1a78a5 | |||
| 4b987f894d | |||
| 3ab1e92a37 | |||
| 2c334a26b6 | |||
| 2a25c6edfa | |||
| 3c6553d7f8 | |||
| 908df079e0 | |||
| a3f30e9444 | |||
| aff3997687 | |||
| 5fa0bd792b | |||
| 2280a16a83 | |||
| 86ef6f9ce7 | |||
| bcd499b4bc | |||
| dcc7c3ebcc | |||
| fd2676ef04 | |||
| ca49407bf9 | |||
| ff64f13765 | |||
| 29e061c885 | |||
| 36bc86da7f | |||
| 2811a89e3b | |||
| a774c9cc97 | |||
| e27f57fab2 | |||
| 692fc6d1d0 | |||
| 5360529327 | |||
| e7d0f7fb0e | |||
| 6b1aeb82c1 | |||
| 8320a84ba0 | |||
| 7aef126181 | |||
| e0291868bc | |||
| 0cf1bf187a | |||
| 6fb16e91dc | |||
| 6d40e6e5e8 | |||
| 398226b9bc | |||
| c1ad6b499f | |||
| 71e0b1379c | |||
| 594d8bf994 | |||
| 7616a2d0e0 | |||
| c4ca1465ee | |||
| eb32e4bad7 | |||
| ae1a8daa22 | |||
| 0fdb4c234a | |||
| 23815fbd0a | |||
| dbeaefe9ba | |||
| 582873e505 | |||
| 6b35b43fd6 | |||
| 06fd7e893e | |||
| ef2bf7f32b | |||
| f312cf7d1c | |||
| 351b33bb3c | |||
| 113cec1705 | |||
| 454f2b4a0b | |||
| 6ab4879968 | |||
| 57acb37e84 | |||
| 4441c697b3 | |||
| bda2b54ce9 | |||
| f6f4f198b0 | |||
| f50541bff7 | |||
| 2a5bff1086 | |||
| 8c1386a2d0 | |||
| ce5bcb9dca | |||
| 1177a778ee | |||
| 1a8e216aa9 | |||
| 7189977a2f | |||
| d5f52529f7 | |||
| 1e85e489a7 | |||
| 5ec85519c7 | |||
| 9462c284d6 | |||
| c6e88792a3 | |||
| 2dfbf0d904 | |||
| 68c7b12cb0 | |||
| 33b2734ba5 | |||
| b58a5d975c | |||
| d0df698aa9 | |||
| 6b80f2386b | |||
| 8e475103f8 | |||
| 4516f77727 | |||
| 594068bd5e | |||
| 041496cf98 | |||
| 6aedec7a9b | |||
| 6d08d10f19 | |||
| cdc35878a2 | |||
| 57c0aa5899 | |||
| b196be59a2 | |||
| 760bc4fc4b | |||
| 6b5b6b8c81 | |||
| 411d24194b | |||
| 2560bf45a7 | |||
| 4207886dce | |||
| 987fe0d885 | |||
| 9c1cedd172 | |||
| ff767970a1 | |||
| 6d277b5809 | |||
| fddfd07836 | |||
| 27a2591904 | |||
| 70eece7a83 | |||
| 637f5316e8 | |||
| 25a64d7666 | |||
| 4df96f8aa2 | |||
| 3e4834f0fd | |||
| 10a63d3659 | |||
| cc1a414df4 | |||
| 8ceae90962 | |||
| 12b7c68cae | |||
| d9099c7281 | |||
| 7d793ede6e | |||
| 6233508442 | |||
| 91298c6922 | |||
| 8dcb61dfed | |||
| 2b33253182 | |||
| 30919212f3 | |||
| 1a496dc0ee | |||
| 7aafe30b46 | |||
| 688d29f445 | |||
| 14dd5a8fc2 | |||
| 0ec5db5d91 | |||
| 26622a4ad2 | |||
| f36532d45f | |||
| af2cf99041 | |||
| af9028190d | |||
| 9120ea511b | |||
| 37e0f9dbc5 | |||
| 98bcbf52ba | |||
| 3a23cd8e19 | |||
| e1a1bc8a69 | |||
| 44c3091951 | |||
| 1840284b48 | |||
| 9de5f4ce8d | |||
| a794872ab6 | |||
| 963f2167eb | |||
| 35bdf58cd6 | |||
| 65ae660486 | |||
| 6554e66a4e | |||
| 5e839be3af | |||
| 44daa255c8 | |||
| 2b1958a603 | |||
| 51e958799d | |||
| 676e959d4b | |||
| f9a89ae9ef | |||
| af85e7eee4 | |||
| a9d104735c | |||
| 752d288e3b | |||
| 9c59277023 | |||
| d19cfc0797 | |||
| 565678f79a | |||
| 73b9dcf0cd | |||
| a65e051af8 | |||
| f2a034f299 | |||
| b42cdcf640 | |||
| cfdd257b9a | |||
| b4ac496b55 | |||
| 105f7781b3 | |||
| 925973b134 | |||
| 4a88685e81 | |||
| d121bb08b9 | |||
| b4f85989d0 | |||
| 21d8984bfb | |||
| 3de6b89cc4 | |||
| 9621efd282 | |||
| fbaa05f146 | |||
| 05089761b6 | |||
| fdf51be5f5 | |||
| 05dbeccdd7 | |||
| 25b8ac97d7 | |||
| c2fe5649e2 | |||
| 2235612070 | |||
| 6a1b71de0f | |||
| b9819252d3 | |||
| 5709b0d6fd | |||
| 5ef104df46 | |||
| c838caf9e1 | |||
| 597f682b75 | |||
| 2a72345943 | |||
| 73066522e3 | |||
| f5a3206f36 | |||
| 6a6d743b96 | |||
| 2241cfc9da | |||
| 597bc09c57 | |||
| fd024cf65d | |||
| 393e60c6e9 | |||
| 9a36b7651b | |||
| 5a2ef02ce7 | |||
| 227269c639 | |||
| beb1bf70bf | |||
| 3167aad6d8 | |||
| 79ccc45c95 | |||
| 48ee45a560 | |||
| 5f75f34289 | |||
| f0d1caf5f3 | |||
| 004924815b | |||
| 472db0174b | |||
| 26ed082f93 | |||
| e491a00c57 | |||
| 4e0dda3a24 | |||
| 91862713e7 | |||
| 009d139549 | |||
| 74fa970902 | |||
| eb933b8f78 | |||
| 7fe37d0131 | |||
| 82c0aa240a | |||
| a543a7bcf2 | |||
| f0caf930c5 | |||
| 05e28123ed | |||
| 7292834700 | |||
| 3e4b94f1f2 | |||
| 0957a5c132 | |||
| 1edaa50732 | |||
| fb3eb2646d | |||
| 1ed76f7687 | |||
| dc73a74e1c | |||
| 2c0d39a6b8 | |||
| 9b31df28aa | |||
| caba1c6658 | |||
| dd0beb1955 | |||
| c65dfbcbf9 | |||
| 2e8bc012fa | |||
| 8ac1794853 | |||
| d047e3d17a | |||
| 3673f5b904 | |||
| ea1588d9e9 | |||
| 2b9a4d35b6 | |||
| 0cfcd5588f | |||
| 043b369695 | |||
| 730d5b1d10 | |||
| 42c3ef3377 | |||
| eedde0fd28 | |||
| ae054f76de | |||
| e5c746fe27 | |||
| cce0560cb3 | |||
| 7368fa77ae | |||
| 90c26c5e7f | |||
| ed7319d61b | |||
| 232ce26246 | |||
| 14b4f67bcb | |||
| c62c4b2942 | |||
| 7ab247f58d | |||
| 33e1e92af3 | |||
| ac6bafb708 | |||
| cfe7a44b98 | |||
| 49ae2c1fa2 | |||
| 3b107ffbd2 | |||
| eac582ee74 | |||
| ee5e0f38dc | |||
| 88c3933540 | |||
| 6f32de633a | |||
| 58bed963c5 | |||
| 62c6699adc | |||
| 4d78877849 | |||
| 63a30127b7 | |||
| a6a302760a | |||
| 93a3dd421a | |||
| 1a1a610b29 | |||
| 053c05ea26 | |||
| f04c4b27d9 | |||
| e73479feec | |||
| c99839d75b | |||
| 8452ffbd03 | |||
| 9985884534 | |||
| c795bdad28 | |||
| 5171f89182 | |||
| ccc87f3f10 | |||
| 7f06964eb2 | |||
| e0fcc0d1f7 | |||
| e3964c1b3d | |||
| 0f5d5653f5 | |||
| e886142b39 | |||
| d11403209d | |||
| e2e5942941 | |||
| 2db998a9d9 | |||
| a22967fc0c | |||
| b41835d9c8 | |||
| 93d3023276 | |||
| 3b82a4aba0 | |||
| 6fa5978613 | |||
| 0584cdb42c | |||
| 147598275a | |||
| f6d7020165 | |||
| 44e262a636 | |||
| 3ae27e6216 | |||
| 4da16cd071 | |||
| 7eb6ffde1d | |||
| d50e8c0863 | |||
| 797c39b1e4 | |||
| a5899565b1 | |||
| b30939dd7d | |||
| a1b5597944 | |||
| 9f1080eeb0 | |||
| a8187b7d38 | |||
| 11b839b21a | |||
| 957a07309a | |||
| afe01bc6ad | |||
| ee0aceeab7 | |||
| 8a2adc5632 | |||
| 9b840ca769 | |||
| 8bd27af592 | |||
| f5f09cddcc | |||
| c154967564 | |||
| ba8babd68a | |||
| 3c7915e672 | |||
| f78a3ae149 | |||
| b8632acebd | |||
| 365bcb86dd | |||
| 089e38f577 | |||
| 1b206af28c | |||
| dc5fdf4857 | |||
| d7b0aca4e6 | |||
| 354cc7cd17 | |||
| d70433ff93 | |||
| 3a0cea8cd4 | |||
| 5aeedb07fe | |||
| 61e2122ca9 | |||
| 1b39289dad | |||
| d58d5014da | |||
| c3e377ca3c | |||
| 67910317e8 | |||
| ff09df03e3 | |||
| a34e213647 | |||
| b393eaadea | |||
| 80998c71b0 | |||
| 9fd67c6c71 | |||
| 4d2b616aed | |||
| 41f603e349 | |||
| ca9b2cb14d | |||
| c08723574d | |||
| d63081955e | |||
| 561a622c26 | |||
| 50fcdbd12f | |||
| 06eb9e2c7e | |||
| 2ad3275753 | |||
| a128bfa960 | |||
| 454c7fb8f3 | |||
| ac08fc96ce | |||
| 6642a248fd | |||
| 9df8a9d123 | |||
| 486787084a | |||
| 49f5f36630 | |||
| 624729ae9e | |||
| 5b8f3a1284 | |||
| d056bc9120 | |||
| f73f0cd45a | |||
| d9d4597e13 | |||
| 873db304d1 | |||
| a239e29c32 | |||
| 847426a507 | |||
| d4d8245671 | |||
| 14bd9d86c0 | |||
| db31b5d6c1 | |||
| e8e77b6467 | |||
| ba1432d945 | |||
| 2fc22e7580 | |||
| ba2299e882 | |||
| cd020c7e49 | |||
| caa255b882 | |||
| f900a45c81 | |||
| ce1a94dbaf | |||
| 6a3644bca1 | |||
| 67db6579e9 | |||
| ebc39a6388 | |||
| a97ca0cec9 | |||
| 97118e7bc8 | |||
| 22c2b41ac5 | |||
| 3cd49c64f2 | |||
| cc371fad85 | |||
| a182b0b8da | |||
| 913ffa8a5a | |||
| 296b0a737f | |||
| 69b5e96695 | |||
| c6a133978b | |||
| ea17240cf1 | |||
| 68e3daa736 | |||
| 8d3bc728c5 | |||
| 5486b9a6fe | |||
| d4c1ad54fc | |||
| 4fcc47aa40 | |||
| 954eb9afc4 | |||
| c94aeb9984 | |||
| aa70fc3273 | |||
| c541306494 | |||
| ef2aa43f03 | |||
| a02129b8f9 | |||
| c4ecb80524 | |||
| 3ff94d0a5c | |||
| d385ce8c54 | |||
| e2053c0428 | |||
| 62096ba34d | |||
| d4bfef36f7 | |||
| c242c89690 | |||
| a322d94f42 | |||
| aa38c2e5e1 | |||
| c16709a36d | |||
| c52ac92549 | |||
| 33432a10f4 | |||
| cb5411c091 | |||
| dc4159b308 | |||
| 86aff7aeb2 | |||
| 43c1f67b33 | |||
| 2a7727e446 | |||
| b65bf7cc82 | |||
| cab4b5c57d | |||
| 1834636e01 | |||
| 1aa39b8f91 | |||
| a80ffbf85b | |||
| 18f3e31251 | |||
| 7129514a9a | |||
| 8711237cf8 | |||
| 980e2e6d1e | |||
| 1b98f09115 | |||
| e27c821d80 | |||
| 8efdd5a50d | |||
| 1fcdff6203 | |||
| b2779759c0 | |||
| 36652d3a39 | |||
| 23ce2f7d1f | |||
| 18c52c18e3 | |||
| 03704b62a7 | |||
| 2a64e3ed14 | |||
| 3b69b1b3ee | |||
| c7685f9b92 | |||
| 9347d423e2 | |||
| 854401ca8d | |||
| 995cfdf87e | |||
| 748f7f9709 | |||
| fb010607f1 | |||
| 40c919348f | |||
| fe79a5481a | |||
| 02b0628bfc | |||
| 3464137511 | |||
| aa138afe61 | |||
| dccf8f9d0c | |||
| 69f5745fe8 | |||
| df8c5376f6 | |||
| 2ec13ed2c9 | |||
| 75a5be2d3c | |||
| 143ceebc00 | |||
| 24a46384b0 | |||
| d248be21e2 | |||
| 27ec900780 | |||
| 393128b326 | |||
| 80a86cec3b | |||
| d6a1e53646 | |||
| 275360983f | |||
| 89241ced04 | |||
| 3dbeb25a09 | |||
| 9aec1afcf5 | |||
| 1f18c2b000 | |||
| 74433b507d | |||
| 045693db21 | |||
| b84cc19ee5 | |||
| 846cb4a2de | |||
| 20b0eb0df5 | |||
| 08c78c8666 | |||
| 23dc6a971c | |||
| 9ed47c727b | |||
| e4dd121bb6 | |||
| 92b7099a44 | |||
| 9589b10e2e | |||
| 2226577b68 | |||
| 3bc530cbde | |||
| 2a1c19697a | |||
| 1ed65ff184 | |||
| 2bc64ecc87 | |||
| cdeed1fcb0 | |||
| 816eacde42 | |||
| 8f76ee80f4 | |||
| 9f3e5df9ca | |||
| 9255b37533 | |||
| b48114a34c | |||
| b900e7c620 | |||
| 03164bc87a | |||
| cbc3819697 | |||
| 5193df6289 | |||
| 8d6a118e97 | |||
| de7a2c5b80 | |||
| 54bf778977 | |||
| 96827afd41 | |||
| 29c584f059 | |||
| 9a01a5daf6 | |||
| 5a3456b878 | |||
| 51a3a7b8f8 | |||
| bcd34e1019 | |||
| 69dbcd2572 | |||
| b953b4c8fe | |||
| 62299d17f0 | |||
| 38405f89b2 | |||
| 10080e6d74 | |||
| 46531b5461 | |||
| 56c89c3228 | |||
| 55b545c058 | |||
| 2f41da9486 | |||
| d1843fc58d | |||
| 14008292d9 | |||
| 0c784dc5cc | |||
| ef6ad46d12 | |||
| 1c2751422d | |||
| a5900aa60d | |||
| 0028a501fb |
@@ -0,0 +1,23 @@
|
|||||||
|
name: Staging CI & CD
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: Deploy
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Deploy to Server
|
||||||
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_HOST }}
|
||||||
|
username: root
|
||||||
|
key: ${{ secrets.SSH_KEY }}
|
||||||
|
script: bash /opt/openisle/deploy-staging.sh
|
||||||
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
name: CI & CD
|
name: CI & CD
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 19 * * *" # 每天 UTC 19:00,相当于北京时间凌晨3点
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
@@ -11,29 +11,12 @@ jobs:
|
|||||||
environment: Deploy
|
environment: Deploy
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
# - uses: actions/setup-java@v4
|
|
||||||
# with:
|
|
||||||
# java-version: '17'
|
|
||||||
# distribution: 'temurin'
|
|
||||||
|
|
||||||
# - run: mvn -B clean package -DskipTests
|
|
||||||
|
|
||||||
# - uses: actions/setup-node@v4
|
|
||||||
# with:
|
|
||||||
# node-version: '20'
|
|
||||||
|
|
||||||
# - run: |
|
|
||||||
# cd open-isle-cli
|
|
||||||
# npm ci
|
|
||||||
# npm run build
|
|
||||||
|
|
||||||
- name: Deploy to Server
|
|
||||||
uses: appleboy/ssh-action@v1.0.3
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.SSH_HOST }}
|
|
||||||
username: root
|
|
||||||
key: ${{ secrets.SSH_KEY }}
|
|
||||||
script: bash /opt/openisle/deploy.sh
|
|
||||||
|
|
||||||
|
- name: Deploy to Server
|
||||||
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_HOST }}
|
||||||
|
username: root
|
||||||
|
key: ${{ secrets.SSH_KEY }}
|
||||||
|
script: bash /opt/openisle/deploy.sh
|
||||||
|
|||||||
+3
-1
@@ -2,4 +2,6 @@
|
|||||||
target
|
target
|
||||||
openisle.iml
|
openisle.iml
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
|
open-isle.env
|
||||||
|
logs
|
||||||
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
npx lint-staged
|
||||||
+116
@@ -0,0 +1,116 @@
|
|||||||
|
#### **⚠️注意:仅想修改前端的朋友可不用部署后端服务**
|
||||||
|
|
||||||
|
## 如何部署
|
||||||
|
|
||||||
|
> Step1 先克隆仓库
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git clone https://github.com/nagisa77/OpenIsle.git
|
||||||
|
cd OpenIsle
|
||||||
|
```
|
||||||
|
|
||||||
|
> Step2 后端部署
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cd backend
|
||||||
|
```
|
||||||
|
|
||||||
|
以IDEA编辑器为例,IDEA打开backend文件夹。
|
||||||
|
|
||||||
|
- 设置VM Option,最好运行在其他端口,非8080,这里设置8081
|
||||||
|
|
||||||
|
```shell
|
||||||
|
-Dserver.port=8081
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- 设置jdk版本为java 17
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- 本机配置MySQL服务(网上很多教程,忽略)
|
||||||
|
- 设置环境变量.env 文件 或.properties 文件(二选一)
|
||||||
|
|
||||||
|
1. 环境变量文件生成
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cp open-isle.env.example open-isle.env
|
||||||
|
```
|
||||||
|
|
||||||
|
修改环境变量,留下需要的,比如你要开发Google登录业务,就需要谷歌相关的变量,数据库是一定要的
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
应用环境文件, 选择刚刚的`open-isle.env`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
2. 直接修改 .properities 文件
|
||||||
|
|
||||||
|
位置src/main/application.properties, 数据库需要修改标红处,其他按需修改
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
处理完环境问题直接跑起来就能通了
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> Step3 前端部署
|
||||||
|
|
||||||
|
前端可以依赖本机部署的后端,也可以直接调用线上的后端接口
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cd ../frontend_nuxt/
|
||||||
|
```
|
||||||
|
|
||||||
|
copy环境.env文件
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cp .env.staging.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
1. 依赖本机部署的后端:打开本文件夹,修改.env 修改为瞄准本机后端端口
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
; 本地部署后端
|
||||||
|
NUXT_PUBLIC_API_BASE_URL=https://127.0.0.1:8081
|
||||||
|
; 预发环境后端
|
||||||
|
; NUXT_PUBLIC_API_BASE_URL=https://staging.open-isle.com
|
||||||
|
; 生产环境后端
|
||||||
|
; NUXT_PUBLIC_API_BASE_URL=https://www.open-isle.com
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 依赖预发环境后台环境
|
||||||
|
|
||||||
|
**(⚠️强烈推荐只部署前端的朋友使用该环境)**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
; 本地部署后端
|
||||||
|
; NUXT_PUBLIC_API_BASE_URL=https://127.0.0.1:8081
|
||||||
|
; 预发环境后端
|
||||||
|
NUXT_PUBLIC_API_BASE_URL=https://staging.open-isle.com
|
||||||
|
; 生产环境后端
|
||||||
|
; NUXT_PUBLIC_API_BASE_URL=https://www.open-isle.com
|
||||||
|
```
|
||||||
|
|
||||||
|
4. 依赖线上后台环境
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
; 本地部署后端
|
||||||
|
; NUXT_PUBLIC_API_BASE_URL=https://127.0.0.1:8081
|
||||||
|
; 预发环境后端
|
||||||
|
; NUXT_PUBLIC_API_BASE_URL=https://staging.open-isle.com
|
||||||
|
; 生产环境后端
|
||||||
|
NUXT_PUBLIC_API_BASE_URL=https://www.open-isle.com
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# 安装依赖
|
||||||
|
npm install --verbose
|
||||||
|
|
||||||
|
# 运行前端服务
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
如此一来,浏览器访问 http://127.0.0.1:3000 即可访问前端页面
|
||||||
@@ -1,28 +1,21 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img alt="OpenIsle" src="https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/image.png" width="200">
|
<img alt="OpenIsle" src="https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/image.png" width="200">
|
||||||
<br><br>
|
<br>
|
||||||
高效的开源社区前后端端平台
|
高效的开源社区前后端平台
|
||||||
<br><br>
|
<br><br><br>
|
||||||
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square"></a>
|
<img alt="Image" src="https://openisle-1307107697.cos.accelerate.myqcloud.com/dynamic_assert/22752cfac5a04a9c90c41995b9f55fed.png" width="1200">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## 💡 简介
|
## 💡 简介
|
||||||
|
|
||||||
OpenIsle 是一个使用 Spring Boot 和 Vue 3 构建的全栈开源社区平台,提供用户注册、登录、贴文发布、评论交互等完整功能,可用于项目社区或直接打造自主社区站点。
|
OpenIsle 是一个使用 Spring Boot 和 Vue 3 构建的全栈开源社区平台,提供用户注册、登录、贴文发布、评论交互等完整功能,可用于项目社区或直接打造自主社区站点。
|
||||||
|
|
||||||
## 🚀 部署
|
## 🚧 开发 & 部署
|
||||||
|
|
||||||
### 后端
|
详细见 [Contributing](https://github.com/nagisa77/OpenIsle?tab=contributing-ov-file)
|
||||||
1. 确保安装 JDK 17 及 Maven
|
|
||||||
2. 信息配置修改 `src/main/resources/application.properties`,或通过环境变量设置数据库等参数
|
|
||||||
3. 执行 `mvn clean package` 生成包,之后使用 `java -jar target/openisle-0.0.1-SNAPSHOT.jar`启动,或在开发时直接使用 `mvn spring-boot:run`
|
|
||||||
|
|
||||||
### 前端
|
|
||||||
1. `cd open-isle-cli`
|
|
||||||
2. 执行 `npm install`
|
|
||||||
3. `npm run serve`可在本地启动开发服务,产品环境使用 `npm run build`生成 `dist/` 文件,配合线上网站方式部署
|
|
||||||
|
|
||||||
## ✨ 项目特点
|
## ✨ 项目特点
|
||||||
|
|
||||||
- JWT 认证以及 Google、GitHub、Discord、Twitter 等多种 OAuth 登录
|
- JWT 认证以及 Google、GitHub、Discord、Twitter 等多种 OAuth 登录
|
||||||
- 支持分类、标签的贴文管理以及草稿保存功能
|
- 支持分类、标签的贴文管理以及草稿保存功能
|
||||||
- 嵌套评论、指定贴文或评论的点赞/抖弹系统
|
- 嵌套评论、指定贴文或评论的点赞/抖弹系统
|
||||||
@@ -31,14 +24,18 @@ OpenIsle 是一个使用 Spring Boot 和 Vue 3 构建的全栈开源社区平台
|
|||||||
- 集成 OpenAI 提供的 Markdown 格式化功能
|
- 集成 OpenAI 提供的 Markdown 格式化功能
|
||||||
- 通过环境变量可调整密码强度、登录方式、保护码等多种配置
|
- 通过环境变量可调整密码强度、登录方式、保护码等多种配置
|
||||||
- 支持图片上传,默认使用腾讯云 COS 扩展
|
- 支持图片上传,默认使用腾讯云 COS 扩展
|
||||||
|
- 默认头像使用 DiceBear Avatars,可通过 `AVATAR_STYLE` 和 `AVATAR_SIZE` 环境变量自定义主题和大小
|
||||||
|
- 浏览器推送通知,离开网站也能及时收到提醒
|
||||||
|
|
||||||
## 🌟 项目优势
|
## 🌟 项目优势
|
||||||
|
|
||||||
- 全面开源,便于二次开发和自定义扩展
|
- 全面开源,便于二次开发和自定义扩展
|
||||||
- Spring Boot + Vue 3 成熟技术栈,学习起点低,社区资源丰富
|
- Spring Boot + Vue 3 成熟技术栈,学习起点低,社区资源丰富
|
||||||
- 支持多种登录方式和角色权限,容易展展到不同场景
|
- 支持多种登录方式和角色权限,容易展展到不同场景
|
||||||
- 模块化设计,代码结构清晰,维护成本低
|
- 模块化设计,代码结构清晰,维护成本低
|
||||||
- REST API 可接入任意前端框架,兼容多端平台
|
- REST API 可接入任意前端框架,兼容多端平台
|
||||||
- 配置简单,通过环境变量快速调整和部署
|
- 配置简单,通过环境变量快速调整和部署
|
||||||
|
- 如需推送通知,请设置 `WEBPUSH_PUBLIC_KEY` 和 `WEBPUSH_PRIVATE_KEY` 环境变量
|
||||||
|
|
||||||
## 🏘️ 社区
|
## 🏘️ 社区
|
||||||
|
|
||||||
@@ -49,6 +46,7 @@ OpenIsle 是一个使用 Spring Boot 和 Vue 3 构建的全栈开源社区平台
|
|||||||
本项目以 MIT License 发布,欢迎自由使用与修改。
|
本项目以 MIT License 发布,欢迎自由使用与修改。
|
||||||
|
|
||||||
## 🙏 鼓赞
|
## 🙏 鼓赞
|
||||||
|
|
||||||
- [Spring Boot](https://spring.io/projects/spring-boot)
|
- [Spring Boot](https://spring.io/projects/spring-boot)
|
||||||
- [JJWT](https://github.com/jwtk/jjwt)
|
- [JJWT](https://github.com/jwtk/jjwt)
|
||||||
- [Lombok](https://github.com/projectlombok/lombok)
|
- [Lombok](https://github.com/projectlombok/lombok)
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# === Database ===
|
||||||
|
MYSQL_URL=jdbc:mysql://<数据库地址>:<端口>/<数据库名>?useUnicode=yes&characterEncoding=UTF-8&useInformationSchema=true&useSSL=false&serverTimezone=UTC
|
||||||
|
MYSQL_USER=<数据库用户名>
|
||||||
|
MYSQL_PASSWORD=<数据库密码>
|
||||||
|
|
||||||
|
# === JWT ===
|
||||||
|
JWT_SECRET=<jwt secret>
|
||||||
|
JWT_REASON_SECRET=<jwt reason secret>
|
||||||
|
JWT_RESET_SECRET=<jwt reset secret>
|
||||||
|
JWT_INVITE_SECRET=<jwt invite secret>
|
||||||
|
JWT_EXPIRATION=2592000000
|
||||||
|
|
||||||
|
# === Resend ===
|
||||||
|
RESEND_API_KEY=<你的resend-api-key>
|
||||||
|
|
||||||
|
# === COS ===
|
||||||
|
# COS_BASE_URL=https://<你的cos>.cos.ap-guangzhou.myqcloud.com
|
||||||
|
COS_BASE_URL=https://<你的cos>.cos.accelerate.myqcloud.com
|
||||||
|
COS_SECRET_ID=<你的cos-secret-id>
|
||||||
|
COS_SECRET_KEY=<你的cos-secret-key>
|
||||||
|
COS_BUCKET_NAME=<你的cos-bucket-name>
|
||||||
|
|
||||||
|
# === OAuth ===
|
||||||
|
GOOGLE_CLIENT_ID=<你的google-client-id>
|
||||||
|
GITHUB_CLIENT_ID=<你的github-client-id>
|
||||||
|
GITHUB_CLIENT_SECRET=<你的github-client-secret>
|
||||||
|
TWITTER_CLIENT_ID=<你的twitter-client-id>
|
||||||
|
TWITTER_CLIENT_SECRET=<你的-twitter-client-secret>
|
||||||
|
DISCORD_CLIENT_ID=<你的discord-client-id>
|
||||||
|
DISCORD_CLIENT_SECRET=<你的discord-client-secret>
|
||||||
|
|
||||||
|
# === OPENAI ===
|
||||||
|
OPENAI_API_KEY=<你的openai-api-key>
|
||||||
|
|
||||||
|
# === Webpush ===
|
||||||
|
WEBPUSH_PUBLIC_KEY=<你的webpush-public-key>
|
||||||
|
WEBPUSH_PRIVATE_KEY=<你的webpush-private-key>
|
||||||
|
|
||||||
|
# LOG_LEVEL=DEBUG
|
||||||
@@ -26,6 +26,10 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.slf4j</groupId>
|
<groupId>org.slf4j</groupId>
|
||||||
<artifactId>slf4j-api</artifactId>
|
<artifactId>slf4j-api</artifactId>
|
||||||
@@ -38,6 +42,16 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-security</artifactId>
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.vladsch.flexmark</groupId>
|
||||||
|
<artifactId>flexmark-all</artifactId>
|
||||||
|
<version>0.64.8</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jsoup</groupId>
|
||||||
|
<artifactId>jsoup</artifactId>
|
||||||
|
<version>1.17.2</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.mysql</groupId>
|
<groupId>com.mysql</groupId>
|
||||||
<artifactId>mysql-connector-j</artifactId>
|
<artifactId>mysql-connector-j</artifactId>
|
||||||
@@ -90,6 +104,16 @@
|
|||||||
<artifactId>spring-boot-starter-test</artifactId>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>nl.martijndwars</groupId>
|
||||||
|
<artifactId>web-push</artifactId>
|
||||||
|
<version>5.1.1</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bouncycastle</groupId>
|
||||||
|
<artifactId>bcprov-jdk15on</artifactId>
|
||||||
|
<version>1.70</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
+2
@@ -2,8 +2,10 @@ package com.openisle;
|
|||||||
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
|
@EnableScheduling
|
||||||
public class OpenIsleApplication {
|
public class OpenIsleApplication {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(OpenIsleApplication.class, args);
|
SpringApplication.run(OpenIsleApplication.class, args);
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.openisle.config;
|
||||||
|
|
||||||
|
import com.openisle.model.Activity;
|
||||||
|
import com.openisle.model.ActivityType;
|
||||||
|
import com.openisle.repository.ActivityRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.boot.CommandLineRunner;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ActivityInitializer implements CommandLineRunner {
|
||||||
|
private final ActivityRepository activityRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run(String... args) {
|
||||||
|
if (activityRepository.findByType(ActivityType.MILK_TEA) == null) {
|
||||||
|
Activity a = new Activity();
|
||||||
|
a.setTitle("🎡建站送奶茶活动");
|
||||||
|
a.setType(ActivityType.MILK_TEA);
|
||||||
|
a.setIcon("https://icons.veryicon.com/png/o/food--drinks/delicious-food-1/coffee-36.png");
|
||||||
|
a.setContent("为了有利于建站推广以及激励发布内容,我们推出了建站送奶茶的活动,前50名达到level 1的用户,可以联系站长获取奶茶/咖啡一杯");
|
||||||
|
activityRepository.save(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activityRepository.findByType(ActivityType.INVITE_POINTS) == null) {
|
||||||
|
Activity a = new Activity();
|
||||||
|
a.setTitle("🎁邀请码送积分活动");
|
||||||
|
a.setType(ActivityType.INVITE_POINTS);
|
||||||
|
a.setIcon("https://img.icons8.com/color/96/gift.png");
|
||||||
|
a.setContent("使用邀请码注册或邀请好友即可获得积分奖励,快来参与吧!");
|
||||||
|
a.setStartTime(LocalDateTime.now());
|
||||||
|
a.setEndTime(LocalDate.of(LocalDate.now().getYear(), 10, 1).atStartOfDay());
|
||||||
|
activityRepository.save(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.openisle.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
|
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableAsync
|
||||||
|
public class AsyncConfig {
|
||||||
|
@Bean(name = "notificationExecutor")
|
||||||
|
public Executor notificationExecutor() {
|
||||||
|
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||||
|
executor.setCorePoolSize(2);
|
||||||
|
executor.setMaxPoolSize(10);
|
||||||
|
executor.setQueueCapacity(100);
|
||||||
|
executor.setThreadNamePrefix("notification-");
|
||||||
|
executor.initialize();
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.openisle.config;
|
||||||
|
|
||||||
|
import com.openisle.model.MessageConversation;
|
||||||
|
import com.openisle.repository.MessageConversationRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.boot.CommandLineRunner;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ChannelInitializer implements CommandLineRunner {
|
||||||
|
private final MessageConversationRepository conversationRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run(String... args) {
|
||||||
|
if (conversationRepository.countByChannelTrue() == 0) {
|
||||||
|
MessageConversation chat = new MessageConversation();
|
||||||
|
chat.setChannel(true);
|
||||||
|
chat.setName("吹水群");
|
||||||
|
chat.setDescription("吹水聊天");
|
||||||
|
chat.setAvatar("https://openisle-1307107697.cos.accelerate.myqcloud.com/dynamic_assert/32647273e2334d14adfd4a6ce9db0643.jpeg");
|
||||||
|
conversationRepository.save(chat);
|
||||||
|
|
||||||
|
MessageConversation tech = new MessageConversation();
|
||||||
|
tech.setChannel(true);
|
||||||
|
tech.setName("技术讨论群");
|
||||||
|
tech.setDescription("讨论技术相关话题");
|
||||||
|
tech.setAvatar("https://openisle-1307107697.cos.accelerate.myqcloud.com/dynamic_assert/5edde9a5864e471caa32491dbcdaa8b2.png");
|
||||||
|
conversationRepository.save(tech);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.openisle.config;
|
||||||
|
|
||||||
|
import com.openisle.model.PointGood;
|
||||||
|
import com.openisle.repository.PointGoodRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.boot.CommandLineRunner;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/** Initialize default point mall goods. */
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PointGoodInitializer implements CommandLineRunner {
|
||||||
|
private final PointGoodRepository pointGoodRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run(String... args) {
|
||||||
|
if (pointGoodRepository.count() == 0) {
|
||||||
|
PointGood g1 = new PointGood();
|
||||||
|
g1.setName("GPT Plus 1 个月");
|
||||||
|
g1.setCost(20000);
|
||||||
|
g1.setImage("https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/icons/chatgpt.png");
|
||||||
|
pointGoodRepository.save(g1);
|
||||||
|
|
||||||
|
PointGood g2 = new PointGood();
|
||||||
|
g2.setName("奶茶");
|
||||||
|
g2.setCost(5000);
|
||||||
|
g2.setImage("https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/icons/coffee.png");
|
||||||
|
pointGoodRepository.save(g2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.openisle.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||||
|
import org.springframework.scheduling.TaskScheduler;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableScheduling
|
||||||
|
public class SchedulerConfig {
|
||||||
|
@Bean
|
||||||
|
public TaskScheduler taskScheduler() {
|
||||||
|
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
||||||
|
scheduler.setPoolSize(2);
|
||||||
|
scheduler.setThreadNamePrefix("lottery-");
|
||||||
|
scheduler.initialize();
|
||||||
|
return scheduler;
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
-7
@@ -74,11 +74,17 @@ public class SecurityConfig {
|
|||||||
CorsConfiguration cfg = new CorsConfiguration();
|
CorsConfiguration cfg = new CorsConfiguration();
|
||||||
cfg.setAllowedOrigins(List.of(
|
cfg.setAllowedOrigins(List.of(
|
||||||
"http://127.0.0.1:8080",
|
"http://127.0.0.1:8080",
|
||||||
|
"http://127.0.0.1:3000",
|
||||||
|
"http://127.0.0.1:3001",
|
||||||
"http://127.0.0.1",
|
"http://127.0.0.1",
|
||||||
"http://localhost:8080",
|
"http://localhost:8080",
|
||||||
|
"http://localhost:3000",
|
||||||
|
"http://localhost:3001",
|
||||||
"http://localhost",
|
"http://localhost",
|
||||||
"http://30.211.97.254:8080",
|
"http://30.211.97.238:3000",
|
||||||
"http://30.211.97.254",
|
"http://30.211.97.238",
|
||||||
|
"http://192.168.7.98",
|
||||||
|
"http://192.168.7.98:3000",
|
||||||
websiteUrl,
|
websiteUrl,
|
||||||
websiteUrl.replace("://www.", "://")
|
websiteUrl.replace("://www.", "://")
|
||||||
));
|
));
|
||||||
@@ -93,11 +99,13 @@ public class SecurityConfig {
|
|||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
http.csrf(csrf -> csrf.disable())
|
http.csrf(csrf -> csrf.disable())
|
||||||
.cors(Customizer.withDefaults()) // 让 Spring 自带 CorsFilter 处理预检
|
.cors(Customizer.withDefaults())
|
||||||
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
.headers(h -> h.frameOptions(f -> f.sameOrigin()))
|
||||||
|
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
.exceptionHandling(eh -> eh.accessDeniedHandler(customAccessDeniedHandler))
|
.exceptionHandling(eh -> eh.accessDeniedHandler(customAccessDeniedHandler))
|
||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> auth
|
||||||
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
|
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
|
||||||
|
.requestMatchers("/api/ws/**", "/api/sockjs/**").permitAll()
|
||||||
.requestMatchers(HttpMethod.POST, "/api/auth/**").permitAll()
|
.requestMatchers(HttpMethod.POST, "/api/auth/**").permitAll()
|
||||||
.requestMatchers(HttpMethod.GET, "/api/posts/**").permitAll()
|
.requestMatchers(HttpMethod.GET, "/api/posts/**").permitAll()
|
||||||
.requestMatchers(HttpMethod.GET, "/api/comments/**").permitAll()
|
.requestMatchers(HttpMethod.GET, "/api/comments/**").permitAll()
|
||||||
@@ -108,11 +116,20 @@ public class SecurityConfig {
|
|||||||
.requestMatchers(HttpMethod.POST,"/api/auth/reason").permitAll()
|
.requestMatchers(HttpMethod.POST,"/api/auth/reason").permitAll()
|
||||||
.requestMatchers(HttpMethod.GET, "/api/search/**").permitAll()
|
.requestMatchers(HttpMethod.GET, "/api/search/**").permitAll()
|
||||||
.requestMatchers(HttpMethod.GET, "/api/users/**").permitAll()
|
.requestMatchers(HttpMethod.GET, "/api/users/**").permitAll()
|
||||||
|
.requestMatchers(HttpMethod.GET, "/api/medals/**").permitAll()
|
||||||
|
.requestMatchers(HttpMethod.GET, "/api/push/public-key").permitAll()
|
||||||
.requestMatchers(HttpMethod.GET, "/api/reaction-types").permitAll()
|
.requestMatchers(HttpMethod.GET, "/api/reaction-types").permitAll()
|
||||||
|
.requestMatchers(HttpMethod.GET, "/api/activities/**").permitAll()
|
||||||
|
.requestMatchers(HttpMethod.GET, "/api/sitemap.xml").permitAll()
|
||||||
|
.requestMatchers(HttpMethod.GET, "/api/channels").permitAll()
|
||||||
|
.requestMatchers(HttpMethod.GET, "/api/rss").permitAll()
|
||||||
|
.requestMatchers(HttpMethod.GET, "/api/point-goods").permitAll()
|
||||||
|
.requestMatchers(HttpMethod.POST, "/api/point-goods").permitAll()
|
||||||
.requestMatchers(HttpMethod.POST, "/api/categories/**").hasAuthority("ADMIN")
|
.requestMatchers(HttpMethod.POST, "/api/categories/**").hasAuthority("ADMIN")
|
||||||
.requestMatchers(HttpMethod.POST, "/api/tags/**").authenticated()
|
.requestMatchers(HttpMethod.POST, "/api/tags/**").authenticated()
|
||||||
.requestMatchers(HttpMethod.DELETE, "/api/categories/**").hasAuthority("ADMIN")
|
.requestMatchers(HttpMethod.DELETE, "/api/categories/**").hasAuthority("ADMIN")
|
||||||
.requestMatchers(HttpMethod.DELETE, "/api/tags/**").hasAuthority("ADMIN")
|
.requestMatchers(HttpMethod.DELETE, "/api/tags/**").hasAuthority("ADMIN")
|
||||||
|
.requestMatchers(HttpMethod.GET, "/api/stats/**").hasAuthority("ADMIN")
|
||||||
.requestMatchers("/api/admin/**").hasAuthority("ADMIN")
|
.requestMatchers("/api/admin/**").hasAuthority("ADMIN")
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
)
|
)
|
||||||
@@ -137,8 +154,12 @@ public class SecurityConfig {
|
|||||||
boolean publicGet = "GET".equalsIgnoreCase(request.getMethod()) &&
|
boolean publicGet = "GET".equalsIgnoreCase(request.getMethod()) &&
|
||||||
(uri.startsWith("/api/posts") || uri.startsWith("/api/comments") ||
|
(uri.startsWith("/api/posts") || uri.startsWith("/api/comments") ||
|
||||||
uri.startsWith("/api/categories") || uri.startsWith("/api/tags") ||
|
uri.startsWith("/api/categories") || uri.startsWith("/api/tags") ||
|
||||||
uri.startsWith("/api/search") || uri.startsWith("/api/users") ||
|
uri.startsWith("/api/search") || uri.startsWith("/api/users") ||
|
||||||
uri.startsWith("/api/reaction-types") || uri.startsWith("/api/config"));
|
uri.startsWith("/api/reaction-types") || uri.startsWith("/api/config") ||
|
||||||
|
uri.startsWith("/api/activities") || uri.startsWith("/api/push/public-key") ||
|
||||||
|
uri.startsWith("/api/point-goods") || uri.startsWith("/api/channels") ||
|
||||||
|
uri.startsWith("/api/sitemap.xml") || uri.startsWith("/api/medals") ||
|
||||||
|
uri.startsWith("/api/rss"));
|
||||||
|
|
||||||
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
||||||
String token = authHeader.substring(7);
|
String token = authHeader.substring(7);
|
||||||
@@ -154,7 +175,8 @@ public class SecurityConfig {
|
|||||||
response.getWriter().write("{\"error\": \"Invalid or expired token\"}");
|
response.getWriter().write("{\"error\": \"Invalid or expired token\"}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (!uri.startsWith("/api/auth") && !publicGet) {
|
} else if (!uri.startsWith("/api/auth") && !publicGet
|
||||||
|
&& !uri.startsWith("/api/ws") && !uri.startsWith("/api/sockjs")) {
|
||||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
response.setContentType("application/json");
|
response.setContentType("application/json");
|
||||||
response.getWriter().write("{\"error\": \"Missing token\"}");
|
response.getWriter().write("{\"error\": \"Missing token\"}");
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
package com.openisle.config;
|
||||||
|
|
||||||
|
import com.openisle.service.JwtService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.messaging.Message;
|
||||||
|
import org.springframework.messaging.MessageChannel;
|
||||||
|
import org.springframework.messaging.simp.config.ChannelRegistration;
|
||||||
|
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||||
|
import org.springframework.messaging.simp.stomp.StompCommand;
|
||||||
|
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
|
||||||
|
import org.springframework.messaging.support.ChannelInterceptor;
|
||||||
|
import org.springframework.messaging.support.MessageHeaderAccessor;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||||
|
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||||
|
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSocketMessageBroker
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||||
|
|
||||||
|
private final JwtService jwtService;
|
||||||
|
private final UserDetailsService userDetailsService;
|
||||||
|
@Value("${app.website-url}")
|
||||||
|
private String websiteUrl;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureMessageBroker(MessageBrokerRegistry config) {
|
||||||
|
// Enable a simple memory-based message broker to carry the messages back to the client on destinations prefixed with "/topic" and "/queue"
|
||||||
|
config.enableSimpleBroker("/topic", "/queue");
|
||||||
|
// Set user destination prefix for personal messages
|
||||||
|
config.setUserDestinationPrefix("/user");
|
||||||
|
// Designates the "/app" prefix for messages that are bound for @MessageMapping-annotated methods.
|
||||||
|
config.setApplicationDestinationPrefixes("/app");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||||
|
// 1) 原生 WebSocket(不带 SockJS)
|
||||||
|
registry.addEndpoint("/api/ws")
|
||||||
|
.setAllowedOriginPatterns(
|
||||||
|
"https://staging.open-isle.com",
|
||||||
|
"https://www.staging.open-isle.com",
|
||||||
|
websiteUrl,
|
||||||
|
websiteUrl.replace("://www.", "://"),
|
||||||
|
"http://localhost:*",
|
||||||
|
"http://127.0.0.1:*",
|
||||||
|
"http://192.168.7.98:*",
|
||||||
|
"http://30.211.97.238:*"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2) SockJS 回退:单独路径
|
||||||
|
registry.addEndpoint("/api/sockjs")
|
||||||
|
.setAllowedOriginPatterns(
|
||||||
|
"https://staging.open-isle.com",
|
||||||
|
"https://www.staging.open-isle.com",
|
||||||
|
websiteUrl,
|
||||||
|
websiteUrl.replace("://www.", "://"),
|
||||||
|
"http://localhost:*",
|
||||||
|
"http://127.0.0.1:*",
|
||||||
|
"http://192.168.7.98:*",
|
||||||
|
"http://30.211.97.238:*"
|
||||||
|
)
|
||||||
|
.withSockJS()
|
||||||
|
.setWebSocketEnabled(true)
|
||||||
|
.setSessionCookieNeeded(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureClientInboundChannel(ChannelRegistration registration) {
|
||||||
|
registration.interceptors(new ChannelInterceptor() {
|
||||||
|
@Override
|
||||||
|
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||||
|
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
|
||||||
|
|
||||||
|
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
|
||||||
|
System.out.println("WebSocket CONNECT command received");
|
||||||
|
String authHeader = accessor.getFirstNativeHeader("Authorization");
|
||||||
|
System.out.println("Authorization header: " + (authHeader != null ? "present" : "missing"));
|
||||||
|
|
||||||
|
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
||||||
|
String token = authHeader.substring(7);
|
||||||
|
try {
|
||||||
|
String username = jwtService.validateAndGetSubject(token);
|
||||||
|
System.out.println("JWT validated for user: " + username);
|
||||||
|
var userDetails = userDetailsService.loadUserByUsername(username);
|
||||||
|
Authentication auth = new UsernamePasswordAuthenticationToken(
|
||||||
|
userDetails, null, userDetails.getAuthorities());
|
||||||
|
accessor.setUser(auth);
|
||||||
|
System.out.println("WebSocket user set: " + username);
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("JWT validation failed: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (StompCommand.SUBSCRIBE.equals(accessor.getCommand())) {
|
||||||
|
System.out.println("WebSocket SUBSCRIBE to: " + accessor.getDestination());
|
||||||
|
System.out.println("WebSocket user during subscribe: " + (accessor.getUser() != null ? accessor.getUser().getName() : "null"));
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package com.openisle.controller;
|
||||||
|
|
||||||
|
import com.openisle.dto.ActivityDto;
|
||||||
|
import com.openisle.dto.MilkTeaInfoDto;
|
||||||
|
import com.openisle.dto.MilkTeaRedeemRequest;
|
||||||
|
import com.openisle.mapper.ActivityMapper;
|
||||||
|
import com.openisle.model.Activity;
|
||||||
|
import com.openisle.model.ActivityType;
|
||||||
|
import com.openisle.model.User;
|
||||||
|
import com.openisle.service.ActivityService;
|
||||||
|
import com.openisle.service.UserService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/activities")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ActivityController {
|
||||||
|
private final ActivityService activityService;
|
||||||
|
private final UserService userService;
|
||||||
|
private final ActivityMapper activityMapper;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public List<ActivityDto> list() {
|
||||||
|
return activityService.list().stream()
|
||||||
|
.map(activityMapper::toDto)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/milk-tea")
|
||||||
|
public MilkTeaInfoDto milkTea() {
|
||||||
|
Activity a = activityService.getByType(ActivityType.MILK_TEA);
|
||||||
|
long count = activityService.countParticipants(a);
|
||||||
|
if (!a.isEnded() && count >= 50) {
|
||||||
|
activityService.end(a);
|
||||||
|
}
|
||||||
|
MilkTeaInfoDto info = new MilkTeaInfoDto();
|
||||||
|
info.setRedeemCount(count);
|
||||||
|
info.setEnded(a.isEnded());
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/milk-tea/redeem")
|
||||||
|
public java.util.Map<String, String> redeemMilkTea(@RequestBody MilkTeaRedeemRequest req, Authentication auth) {
|
||||||
|
User user = userService.findByIdentifier(auth.getName()).orElseThrow();
|
||||||
|
Activity a = activityService.getByType(ActivityType.MILK_TEA);
|
||||||
|
boolean first = activityService.redeem(a, user, req.getContact());
|
||||||
|
if (first) {
|
||||||
|
return java.util.Map.of("message", "redeemed");
|
||||||
|
}
|
||||||
|
return java.util.Map.of("message", "updated");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.openisle.controller;
|
||||||
|
|
||||||
|
import com.openisle.dto.CommentDto;
|
||||||
|
import com.openisle.mapper.CommentMapper;
|
||||||
|
import com.openisle.service.CommentService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoints for administrators to manage comments.
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/admin/comments")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AdminCommentController {
|
||||||
|
private final CommentService commentService;
|
||||||
|
private final CommentMapper commentMapper;
|
||||||
|
|
||||||
|
@PostMapping("/{id}/pin")
|
||||||
|
public CommentDto pin(@PathVariable Long id, Authentication auth) {
|
||||||
|
return commentMapper.toDto(commentService.pinComment(auth.getName(), id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/unpin")
|
||||||
|
public CommentDto unpin(@PathVariable Long id, Authentication auth) {
|
||||||
|
return commentMapper.toDto(commentService.unpinComment(auth.getName(), id));
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-12
@@ -1,13 +1,10 @@
|
|||||||
package com.openisle.controller;
|
package com.openisle.controller;
|
||||||
|
|
||||||
import com.openisle.model.PasswordStrength;
|
import com.openisle.dto.ConfigDto;
|
||||||
import com.openisle.model.PublishMode;
|
import com.openisle.service.AiUsageService;
|
||||||
import com.openisle.service.PasswordValidator;
|
import com.openisle.service.PasswordValidator;
|
||||||
import com.openisle.service.PostService;
|
import com.openisle.service.PostService;
|
||||||
import com.openisle.service.AiUsageService;
|
|
||||||
import com.openisle.service.RegisterModeService;
|
import com.openisle.service.RegisterModeService;
|
||||||
import com.openisle.model.RegisterMode;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@@ -47,11 +44,4 @@ public class AdminConfigController {
|
|||||||
return getConfig();
|
return getConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
|
||||||
public static class ConfigDto {
|
|
||||||
private PublishMode publishMode;
|
|
||||||
private PasswordStrength passwordStrength;
|
|
||||||
private Integer aiFormatLimit;
|
|
||||||
private RegisterMode registerMode;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package com.openisle.controller;
|
||||||
|
|
||||||
|
import com.openisle.dto.PostSummaryDto;
|
||||||
|
import com.openisle.mapper.PostMapper;
|
||||||
|
import com.openisle.service.PostService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoints for administrators to manage posts.
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/admin/posts")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AdminPostController {
|
||||||
|
private final PostService postService;
|
||||||
|
private final PostMapper postMapper;
|
||||||
|
|
||||||
|
@GetMapping("/pending")
|
||||||
|
public List<PostSummaryDto> pendingPosts() {
|
||||||
|
return postService.listPendingPosts().stream()
|
||||||
|
.map(postMapper::toSummaryDto)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/approve")
|
||||||
|
public PostSummaryDto approve(@PathVariable Long id) {
|
||||||
|
return postMapper.toSummaryDto(postService.approvePost(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/reject")
|
||||||
|
public PostSummaryDto reject(@PathVariable Long id) {
|
||||||
|
return postMapper.toSummaryDto(postService.rejectPost(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/pin")
|
||||||
|
public PostSummaryDto pin(@PathVariable Long id) {
|
||||||
|
return postMapper.toSummaryDto(postService.pinPost(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/unpin")
|
||||||
|
public PostSummaryDto unpin(@PathVariable Long id) {
|
||||||
|
return postMapper.toSummaryDto(postService.unpinPost(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/rss-exclude")
|
||||||
|
public PostSummaryDto excludeFromRss(@PathVariable Long id) {
|
||||||
|
return postMapper.toSummaryDto(postService.excludeFromRss(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/rss-include")
|
||||||
|
public PostSummaryDto includeInRss(@PathVariable Long id) {
|
||||||
|
return postMapper.toSummaryDto(postService.includeInRss(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-25
@@ -1,9 +1,10 @@
|
|||||||
package com.openisle.controller;
|
package com.openisle.controller;
|
||||||
|
|
||||||
|
import com.openisle.dto.TagDto;
|
||||||
|
import com.openisle.mapper.TagMapper;
|
||||||
import com.openisle.model.Tag;
|
import com.openisle.model.Tag;
|
||||||
import com.openisle.service.TagService;
|
|
||||||
import com.openisle.service.PostService;
|
import com.openisle.service.PostService;
|
||||||
import lombok.Data;
|
import com.openisle.service.TagService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@@ -16,11 +17,12 @@ import java.util.stream.Collectors;
|
|||||||
public class AdminTagController {
|
public class AdminTagController {
|
||||||
private final TagService tagService;
|
private final TagService tagService;
|
||||||
private final PostService postService;
|
private final PostService postService;
|
||||||
|
private final TagMapper tagMapper;
|
||||||
|
|
||||||
@GetMapping("/pending")
|
@GetMapping("/pending")
|
||||||
public List<TagDto> pendingTags() {
|
public List<TagDto> pendingTags() {
|
||||||
return tagService.listPendingTags().stream()
|
return tagService.listPendingTags().stream()
|
||||||
.map(t -> toDto(t, postService.countPostsByTag(t.getId())))
|
.map(t -> tagMapper.toDto(t, postService.countPostsByTag(t.getId())))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,27 +30,6 @@ public class AdminTagController {
|
|||||||
public TagDto approve(@PathVariable Long id) {
|
public TagDto approve(@PathVariable Long id) {
|
||||||
Tag tag = tagService.approveTag(id);
|
Tag tag = tagService.approveTag(id);
|
||||||
long count = postService.countPostsByTag(tag.getId());
|
long count = postService.countPostsByTag(tag.getId());
|
||||||
return toDto(tag, count);
|
return tagMapper.toDto(tag, count);
|
||||||
}
|
|
||||||
|
|
||||||
private TagDto toDto(Tag tag, long count) {
|
|
||||||
TagDto dto = new TagDto();
|
|
||||||
dto.setId(tag.getId());
|
|
||||||
dto.setName(tag.getName());
|
|
||||||
dto.setDescription(tag.getDescription());
|
|
||||||
dto.setIcon(tag.getIcon());
|
|
||||||
dto.setSmallIcon(tag.getSmallIcon());
|
|
||||||
dto.setCount(count);
|
|
||||||
return dto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
private static class TagDto {
|
|
||||||
private Long id;
|
|
||||||
private String name;
|
|
||||||
private String description;
|
|
||||||
private String icon;
|
|
||||||
private String smallIcon;
|
|
||||||
private Long count;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+19
-4
@@ -1,7 +1,10 @@
|
|||||||
package com.openisle.controller;
|
package com.openisle.controller;
|
||||||
|
|
||||||
|
import com.openisle.model.Notification;
|
||||||
|
import com.openisle.model.NotificationType;
|
||||||
import com.openisle.model.User;
|
import com.openisle.model.User;
|
||||||
import com.openisle.service.EmailSender;
|
import com.openisle.service.EmailSender;
|
||||||
|
import com.openisle.repository.NotificationRepository;
|
||||||
import com.openisle.repository.UserRepository;
|
import com.openisle.repository.UserRepository;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
@@ -13,6 +16,7 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AdminUserController {
|
public class AdminUserController {
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
|
private final NotificationRepository notificationRepository;
|
||||||
private final EmailSender emailSender;
|
private final EmailSender emailSender;
|
||||||
@Value("${app.website-url}")
|
@Value("${app.website-url}")
|
||||||
private String websiteUrl;
|
private String websiteUrl;
|
||||||
@@ -22,8 +26,9 @@ public class AdminUserController {
|
|||||||
User user = userRepository.findById(id).orElseThrow();
|
User user = userRepository.findById(id).orElseThrow();
|
||||||
user.setApproved(true);
|
user.setApproved(true);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
emailSender.sendEmail(user.getEmail(), "Registration Approved",
|
markRegisterRequestNotificationsRead(user);
|
||||||
"Your account has been approved. Visit: " + websiteUrl);
|
emailSender.sendEmail(user.getEmail(), "您的注册已审核通过",
|
||||||
|
"🎉您的注册已经审核通过, 点击以访问网站: " + websiteUrl);
|
||||||
return ResponseEntity.ok().build();
|
return ResponseEntity.ok().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,8 +37,18 @@ public class AdminUserController {
|
|||||||
User user = userRepository.findById(id).orElseThrow();
|
User user = userRepository.findById(id).orElseThrow();
|
||||||
user.setApproved(false);
|
user.setApproved(false);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
emailSender.sendEmail(user.getEmail(), "Registration Rejected",
|
markRegisterRequestNotificationsRead(user);
|
||||||
"Your account request was rejected. Visit: " + websiteUrl);
|
emailSender.sendEmail(user.getEmail(), "您的注册已被管理员拒绝",
|
||||||
|
"您的注册被管理员拒绝, 点击链接可以重新填写理由申请: " + websiteUrl);
|
||||||
return ResponseEntity.ok().build();
|
return ResponseEntity.ok().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void markRegisterRequestNotificationsRead(User applicant) {
|
||||||
|
java.util.List<Notification> notifs =
|
||||||
|
notificationRepository.findByTypeAndFromUser(NotificationType.REGISTER_REQUEST, applicant);
|
||||||
|
for (Notification n : notifs) {
|
||||||
|
n.setRead(true);
|
||||||
|
}
|
||||||
|
notificationRepository.saveAll(notifs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+185
-104
@@ -1,24 +1,16 @@
|
|||||||
package com.openisle.controller;
|
package com.openisle.controller;
|
||||||
|
|
||||||
import com.openisle.model.User;
|
import com.openisle.dto.*;
|
||||||
import com.openisle.service.EmailSender;
|
import com.openisle.exception.FieldException;
|
||||||
import com.openisle.service.JwtService;
|
|
||||||
import com.openisle.service.UserService;
|
|
||||||
import com.openisle.service.CaptchaService;
|
|
||||||
import com.openisle.service.GoogleAuthService;
|
|
||||||
import com.openisle.service.GithubAuthService;
|
|
||||||
import com.openisle.service.DiscordAuthService;
|
|
||||||
import com.openisle.service.TwitterAuthService;
|
|
||||||
import com.openisle.service.RegisterModeService;
|
|
||||||
import com.openisle.service.NotificationService;
|
|
||||||
import com.openisle.model.RegisterMode;
|
import com.openisle.model.RegisterMode;
|
||||||
|
import com.openisle.model.User;
|
||||||
import com.openisle.repository.UserRepository;
|
import com.openisle.repository.UserRepository;
|
||||||
import lombok.Data;
|
import com.openisle.service.*;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@@ -37,6 +29,8 @@ public class AuthController {
|
|||||||
private final RegisterModeService registerModeService;
|
private final RegisterModeService registerModeService;
|
||||||
private final NotificationService notificationService;
|
private final NotificationService notificationService;
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
|
private final InviteService inviteService;
|
||||||
|
|
||||||
|
|
||||||
@Value("${app.captcha.enabled:false}")
|
@Value("${app.captcha.enabled:false}")
|
||||||
private boolean captchaEnabled;
|
private boolean captchaEnabled;
|
||||||
@@ -52,9 +46,30 @@ public class AuthController {
|
|||||||
if (captchaEnabled && registerCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
|
if (captchaEnabled && registerCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", "Invalid captcha"));
|
return ResponseEntity.badRequest().body(Map.of("error", "Invalid captcha"));
|
||||||
}
|
}
|
||||||
|
if (req.getInviteToken() != null && !req.getInviteToken().isEmpty()) {
|
||||||
|
InviteService.InviteValidateResult result = inviteService.validate(req.getInviteToken());
|
||||||
|
if (!result.isValidate()) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", "邀请码使用次数过多"));
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
User user = userService.registerWithInvite(
|
||||||
|
req.getUsername(), req.getEmail(), req.getPassword());
|
||||||
|
inviteService.consume(req.getInviteToken(), user.getUsername());
|
||||||
|
emailService.sendEmail(user.getEmail(), "在网站填写验证码以验证", "您的验证码是 " + user.getVerificationCode());
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"token", jwtService.generateToken(user.getUsername()),
|
||||||
|
"reason_code", "INVITE_APPROVED"
|
||||||
|
));
|
||||||
|
} catch (FieldException e) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of(
|
||||||
|
"field", e.getField(),
|
||||||
|
"error", e.getMessage()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
User user = userService.register(
|
User user = userService.register(
|
||||||
req.getUsername(), req.getEmail(), req.getPassword(), "", registerModeService.getRegisterMode());
|
req.getUsername(), req.getEmail(), req.getPassword(), "", registerModeService.getRegisterMode());
|
||||||
emailService.sendEmail(user.getEmail(), "Verification Code", "Your verification code is " + user.getVerificationCode());
|
emailService.sendEmail(user.getEmail(), "在网站填写验证码以验证", "您的验证码是 " + user.getVerificationCode());
|
||||||
if (!user.isApproved()) {
|
if (!user.isApproved()) {
|
||||||
notificationService.createRegisterRequestNotifications(user, user.getRegisterReason());
|
notificationService.createRegisterRequestNotifications(user, user.getRegisterReason());
|
||||||
}
|
}
|
||||||
@@ -65,10 +80,26 @@ public class AuthController {
|
|||||||
public ResponseEntity<?> verify(@RequestBody VerifyRequest req) {
|
public ResponseEntity<?> verify(@RequestBody VerifyRequest req) {
|
||||||
boolean ok = userService.verifyCode(req.getUsername(), req.getCode());
|
boolean ok = userService.verifyCode(req.getUsername(), req.getCode());
|
||||||
if (ok) {
|
if (ok) {
|
||||||
return ResponseEntity.ok(Map.of(
|
Optional<User> userOpt = userService.findByUsername(req.getUsername());
|
||||||
"message", "Verified",
|
if (userOpt.isEmpty()) {
|
||||||
"token", jwtService.generateReasonToken(req.getUsername())
|
return ResponseEntity.badRequest().body(Map.of("error", "Invalid credentials"));
|
||||||
));
|
}
|
||||||
|
|
||||||
|
User user = userOpt.get();
|
||||||
|
|
||||||
|
if (user.isApproved()) {
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"message", "Verified and isApproved",
|
||||||
|
"reason_code", "VERIFIED_AND_APPROVED",
|
||||||
|
"token", jwtService.generateToken(req.getUsername())
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"message", "Verified",
|
||||||
|
"reason_code", "VERIFIED",
|
||||||
|
"token", jwtService.generateReasonToken(req.getUsername())
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", "Invalid verification code"));
|
return ResponseEntity.badRequest().body(Map.of("error", "Invalid verification code"));
|
||||||
}
|
}
|
||||||
@@ -90,7 +121,7 @@ public class AuthController {
|
|||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
if (!user.isVerified()) {
|
if (!user.isVerified()) {
|
||||||
user = userService.register(user.getUsername(), user.getEmail(), user.getPassword(), user.getRegisterReason(), registerModeService.getRegisterMode());
|
user = userService.register(user.getUsername(), user.getEmail(), user.getPassword(), user.getRegisterReason(), registerModeService.getRegisterMode());
|
||||||
emailService.sendEmail(user.getEmail(), "Verification Code", "Your verification code is " + user.getVerificationCode());
|
emailService.sendEmail(user.getEmail(), "在网站填写验证码以验证", "您的验证码是 " + user.getVerificationCode());
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(Map.of(
|
||||||
"error", "User not verified",
|
"error", "User not verified",
|
||||||
"reason_code", "NOT_VERIFIED",
|
"reason_code", "NOT_VERIFIED",
|
||||||
@@ -113,27 +144,43 @@ public class AuthController {
|
|||||||
|
|
||||||
@PostMapping("/google")
|
@PostMapping("/google")
|
||||||
public ResponseEntity<?> loginWithGoogle(@RequestBody GoogleLoginRequest req) {
|
public ResponseEntity<?> loginWithGoogle(@RequestBody GoogleLoginRequest req) {
|
||||||
Optional<User> user = googleAuthService.authenticate(req.getIdToken(), registerModeService.getRegisterMode());
|
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
|
||||||
if (user.isPresent()) {
|
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken());
|
||||||
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
if (viaInvite && !inviteValidateResult.isValidate()) {
|
||||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
|
return ResponseEntity.badRequest().body(Map.of("error", "Invalid invite token"));
|
||||||
|
}
|
||||||
|
Optional<AuthResult> resultOpt = googleAuthService.authenticate(
|
||||||
|
req.getIdToken(),
|
||||||
|
registerModeService.getRegisterMode(),
|
||||||
|
viaInvite);
|
||||||
|
if (resultOpt.isPresent()) {
|
||||||
|
AuthResult result = resultOpt.get();
|
||||||
|
if (viaInvite && result.isNewUser()) {
|
||||||
|
inviteService.consume(req.getInviteToken(), inviteValidateResult.getInviteToken().getInviter().getUsername());
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"token", jwtService.generateToken(result.getUser().getUsername()),
|
||||||
|
"reason_code", "INVITE_APPROVED"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
if (!user.get().isApproved()) {
|
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
||||||
if (user.get().getRegisterReason() != null && !user.get().getRegisterReason().isEmpty()) {
|
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
|
||||||
|
}
|
||||||
|
if (!result.getUser().isApproved()) {
|
||||||
|
if (result.getUser().getRegisterReason() != null && !result.getUser().getRegisterReason().isEmpty()) {
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(Map.of(
|
||||||
"error", "Account awaiting approval",
|
"error", "Account awaiting approval",
|
||||||
"reason_code", "IS_APPROVING",
|
"reason_code", "IS_APPROVING",
|
||||||
"token", jwtService.generateReasonToken(user.get().getUsername())
|
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(Map.of(
|
||||||
"error", "Account awaiting approval",
|
"error", "Account awaiting approval",
|
||||||
"reason_code", "NOT_APPROVED",
|
"reason_code", "NOT_APPROVED",
|
||||||
"token", jwtService.generateReasonToken(user.get().getUsername())
|
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
|
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
|
||||||
}
|
}
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(Map.of(
|
||||||
"error", "Invalid google token",
|
"error", "Invalid google token",
|
||||||
@@ -153,8 +200,8 @@ public class AuthController {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.reason == null || req.reason.length() <= 20) {
|
if (req.getReason() == null || req.getReason().trim().length() <= 20) {
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(Map.of(
|
||||||
"error", "Reason's length must longer than 20",
|
"error", "Reason's length must longer than 20",
|
||||||
"reason_code", "INVALID_CREDENTIALS"
|
"reason_code", "INVALID_CREDENTIALS"
|
||||||
));
|
));
|
||||||
@@ -172,28 +219,45 @@ public class AuthController {
|
|||||||
|
|
||||||
@PostMapping("/github")
|
@PostMapping("/github")
|
||||||
public ResponseEntity<?> loginWithGithub(@RequestBody GithubLoginRequest req) {
|
public ResponseEntity<?> loginWithGithub(@RequestBody GithubLoginRequest req) {
|
||||||
Optional<User> user = githubAuthService.authenticate(req.getCode(), registerModeService.getRegisterMode(), req.getRedirectUri());
|
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
|
||||||
if (user.isPresent()) {
|
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken());
|
||||||
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
if (viaInvite && !inviteValidateResult.isValidate()) {
|
||||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
|
return ResponseEntity.badRequest().body(Map.of("error", "Invalid invite token"));
|
||||||
|
}
|
||||||
|
Optional<AuthResult> resultOpt = githubAuthService.authenticate(
|
||||||
|
req.getCode(),
|
||||||
|
registerModeService.getRegisterMode(),
|
||||||
|
req.getRedirectUri(),
|
||||||
|
viaInvite);
|
||||||
|
if (resultOpt.isPresent()) {
|
||||||
|
AuthResult result = resultOpt.get();
|
||||||
|
if (viaInvite && result.isNewUser()) {
|
||||||
|
inviteService.consume(req.getInviteToken(), inviteValidateResult.getInviteToken().getInviter().getUsername());
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"token", jwtService.generateToken(result.getUser().getUsername()),
|
||||||
|
"reason_code", "INVITE_APPROVED"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
if (!user.get().isApproved()) {
|
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
||||||
if (user.get().getRegisterReason() != null && !user.get().getRegisterReason().isEmpty()) {
|
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
|
||||||
|
}
|
||||||
|
if (!result.getUser().isApproved()) {
|
||||||
|
if (result.getUser().getRegisterReason() != null && !result.getUser().getRegisterReason().isEmpty()) {
|
||||||
// 已填写注册理由
|
// 已填写注册理由
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(Map.of(
|
||||||
"error", "Account awaiting approval",
|
"error", "Account awaiting approval",
|
||||||
"reason_code", "IS_APPROVING",
|
"reason_code", "IS_APPROVING",
|
||||||
"token", jwtService.generateReasonToken(user.get().getUsername())
|
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(Map.of(
|
||||||
"error", "Account awaiting approval",
|
"error", "Account awaiting approval",
|
||||||
"reason_code", "NOT_APPROVED",
|
"reason_code", "NOT_APPROVED",
|
||||||
"token", jwtService.generateReasonToken(user.get().getUsername())
|
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
|
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
|
||||||
}
|
}
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(Map.of(
|
||||||
"error", "Invalid github code",
|
"error", "Invalid github code",
|
||||||
@@ -203,27 +267,44 @@ public class AuthController {
|
|||||||
|
|
||||||
@PostMapping("/discord")
|
@PostMapping("/discord")
|
||||||
public ResponseEntity<?> loginWithDiscord(@RequestBody DiscordLoginRequest req) {
|
public ResponseEntity<?> loginWithDiscord(@RequestBody DiscordLoginRequest req) {
|
||||||
Optional<User> user = discordAuthService.authenticate(req.getCode(), registerModeService.getRegisterMode(), req.getRedirectUri());
|
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
|
||||||
if (user.isPresent()) {
|
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken());
|
||||||
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
if (viaInvite && !inviteValidateResult.isValidate()) {
|
||||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
|
return ResponseEntity.badRequest().body(Map.of("error", "Invalid invite token"));
|
||||||
|
}
|
||||||
|
Optional<AuthResult> resultOpt = discordAuthService.authenticate(
|
||||||
|
req.getCode(),
|
||||||
|
registerModeService.getRegisterMode(),
|
||||||
|
req.getRedirectUri(),
|
||||||
|
viaInvite);
|
||||||
|
if (resultOpt.isPresent()) {
|
||||||
|
AuthResult result = resultOpt.get();
|
||||||
|
if (viaInvite && result.isNewUser()) {
|
||||||
|
inviteService.consume(req.getInviteToken(), inviteValidateResult.getInviteToken().getInviter().getUsername());
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"token", jwtService.generateToken(result.getUser().getUsername()),
|
||||||
|
"reason_code", "INVITE_APPROVED"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
if (!user.get().isApproved()) {
|
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
||||||
if (user.get().getRegisterReason() != null && !user.get().getRegisterReason().isEmpty()) {
|
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
|
||||||
|
}
|
||||||
|
if (!result.getUser().isApproved()) {
|
||||||
|
if (result.getUser().getRegisterReason() != null && !result.getUser().getRegisterReason().isEmpty()) {
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(Map.of(
|
||||||
"error", "Account awaiting approval",
|
"error", "Account awaiting approval",
|
||||||
"reason_code", "IS_APPROVING",
|
"reason_code", "IS_APPROVING",
|
||||||
"token", jwtService.generateReasonToken(user.get().getUsername())
|
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(Map.of(
|
||||||
"error", "Account awaiting approval",
|
"error", "Account awaiting approval",
|
||||||
"reason_code", "NOT_APPROVED",
|
"reason_code", "NOT_APPROVED",
|
||||||
"token", jwtService.generateReasonToken(user.get().getUsername())
|
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
|
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
|
||||||
}
|
}
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(Map.of(
|
||||||
"error", "Invalid discord code",
|
"error", "Invalid discord code",
|
||||||
@@ -233,31 +314,45 @@ public class AuthController {
|
|||||||
|
|
||||||
@PostMapping("/twitter")
|
@PostMapping("/twitter")
|
||||||
public ResponseEntity<?> loginWithTwitter(@RequestBody TwitterLoginRequest req) {
|
public ResponseEntity<?> loginWithTwitter(@RequestBody TwitterLoginRequest req) {
|
||||||
Optional<User> user = twitterAuthService.authenticate(
|
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
|
||||||
|
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken());
|
||||||
|
if (viaInvite && !inviteValidateResult.isValidate()) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", "Invalid invite token"));
|
||||||
|
}
|
||||||
|
Optional<AuthResult> resultOpt = twitterAuthService.authenticate(
|
||||||
req.getCode(),
|
req.getCode(),
|
||||||
req.getCodeVerifier(),
|
req.getCodeVerifier(),
|
||||||
registerModeService.getRegisterMode(),
|
registerModeService.getRegisterMode(),
|
||||||
req.getRedirectUri());
|
req.getRedirectUri(),
|
||||||
if (user.isPresent()) {
|
viaInvite);
|
||||||
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
if (resultOpt.isPresent()) {
|
||||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
|
AuthResult result = resultOpt.get();
|
||||||
|
if (viaInvite && result.isNewUser()) {
|
||||||
|
inviteService.consume(req.getInviteToken(), inviteValidateResult.getInviteToken().getInviter().getUsername());
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"token", jwtService.generateToken(result.getUser().getUsername()),
|
||||||
|
"reason_code", "INVITE_APPROVED"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
if (!user.get().isApproved()) {
|
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
||||||
if (user.get().getRegisterReason() != null && !user.get().getRegisterReason().isEmpty()) {
|
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
|
||||||
|
}
|
||||||
|
if (!result.getUser().isApproved()) {
|
||||||
|
if (result.getUser().getRegisterReason() != null && !result.getUser().getRegisterReason().isEmpty()) {
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(Map.of(
|
||||||
"error", "Account awaiting approval",
|
"error", "Account awaiting approval",
|
||||||
"reason_code", "IS_APPROVING",
|
"reason_code", "IS_APPROVING",
|
||||||
"token", jwtService.generateReasonToken(user.get().getUsername())
|
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(Map.of(
|
||||||
"error", "Account awaiting approval",
|
"error", "Account awaiting approval",
|
||||||
"reason_code", "NOT_APPROVED",
|
"reason_code", "NOT_APPROVED",
|
||||||
"token", jwtService.generateReasonToken(user.get().getUsername())
|
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
|
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
|
||||||
}
|
}
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(Map.of(
|
||||||
"error", "Invalid twitter code",
|
"error", "Invalid twitter code",
|
||||||
@@ -270,54 +365,40 @@ public class AuthController {
|
|||||||
return ResponseEntity.ok(Map.of("valid", true));
|
return ResponseEntity.ok(Map.of("valid", true));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@PostMapping("/forgot/send")
|
||||||
private static class RegisterRequest {
|
public ResponseEntity<?> sendReset(@RequestBody ForgotPasswordRequest req) {
|
||||||
private String username;
|
Optional<User> userOpt = userService.findByEmail(req.getEmail());
|
||||||
private String email;
|
if (userOpt.isEmpty()) {
|
||||||
private String password;
|
return ResponseEntity.badRequest().body(Map.of("error", "User not found"));
|
||||||
private String captcha;
|
}
|
||||||
|
String code = userService.generatePasswordResetCode(req.getEmail());
|
||||||
|
emailService.sendEmail(req.getEmail(), "请填写验证码以重置密码", "您的验证码是" + code);
|
||||||
|
return ResponseEntity.ok(Map.of("message", "Verification code sent"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@PostMapping("/forgot/verify")
|
||||||
private static class LoginRequest {
|
public ResponseEntity<?> verifyReset(@RequestBody VerifyForgotRequest req) {
|
||||||
private String username;
|
boolean ok = userService.verifyPasswordResetCode(req.getEmail(), req.getCode());
|
||||||
private String password;
|
if (ok) {
|
||||||
private String captcha;
|
String username = userService.findByEmail(req.getEmail()).get().getUsername();
|
||||||
|
return ResponseEntity.ok(Map.of("token", jwtService.generateResetToken(username)));
|
||||||
|
}
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", "Invalid verification code"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@PostMapping("/forgot/reset")
|
||||||
private static class GoogleLoginRequest {
|
public ResponseEntity<?> resetPassword(@RequestBody ResetPasswordRequest req) {
|
||||||
private String idToken;
|
String username = jwtService.validateAndGetSubjectForReset(req.getToken());
|
||||||
|
try {
|
||||||
|
userService.updatePassword(username, req.getPassword());
|
||||||
|
return ResponseEntity.ok(Map.of("message", "Password updated"));
|
||||||
|
} catch (FieldException e) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of(
|
||||||
|
"field", e.getField(),
|
||||||
|
"error", e.getMessage()
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
// DTO classes moved to com.openisle.dto package
|
||||||
private static class GithubLoginRequest {
|
|
||||||
private String code;
|
|
||||||
private String redirectUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
private static class DiscordLoginRequest {
|
|
||||||
private String code;
|
|
||||||
private String redirectUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
private static class TwitterLoginRequest {
|
|
||||||
private String code;
|
|
||||||
private String redirectUri;
|
|
||||||
private String codeVerifier;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
private static class VerifyRequest {
|
|
||||||
private String username;
|
|
||||||
private String code;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
private static class MakeReasonRequest {
|
|
||||||
private String token;
|
|
||||||
private String reason;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+17
-47
@@ -1,13 +1,18 @@
|
|||||||
package com.openisle.controller;
|
package com.openisle.controller;
|
||||||
|
|
||||||
|
import com.openisle.dto.CategoryDto;
|
||||||
|
import com.openisle.dto.CategoryRequest;
|
||||||
|
import com.openisle.dto.PostSummaryDto;
|
||||||
|
import com.openisle.mapper.CategoryMapper;
|
||||||
|
import com.openisle.mapper.PostMapper;
|
||||||
import com.openisle.model.Category;
|
import com.openisle.model.Category;
|
||||||
import com.openisle.service.CategoryService;
|
import com.openisle.service.CategoryService;
|
||||||
import com.openisle.service.PostService;
|
import com.openisle.service.PostService;
|
||||||
import lombok.Data;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -16,19 +21,21 @@ import java.util.stream.Collectors;
|
|||||||
public class CategoryController {
|
public class CategoryController {
|
||||||
private final CategoryService categoryService;
|
private final CategoryService categoryService;
|
||||||
private final PostService postService;
|
private final PostService postService;
|
||||||
|
private final PostMapper postMapper;
|
||||||
|
private final CategoryMapper categoryMapper;
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public CategoryDto create(@RequestBody CategoryRequest req) {
|
public CategoryDto create(@RequestBody CategoryRequest req) {
|
||||||
Category c = categoryService.createCategory(req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon());
|
Category c = categoryService.createCategory(req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon());
|
||||||
long count = postService.countPostsByCategory(c.getId());
|
long count = postService.countPostsByCategory(c.getId());
|
||||||
return toDto(c, count);
|
return categoryMapper.toDto(c, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/{id}")
|
@PutMapping("/{id}")
|
||||||
public CategoryDto update(@PathVariable Long id, @RequestBody CategoryRequest req) {
|
public CategoryDto update(@PathVariable Long id, @RequestBody CategoryRequest req) {
|
||||||
Category c = categoryService.updateCategory(id, req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon());
|
Category c = categoryService.updateCategory(id, req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon());
|
||||||
long count = postService.countPostsByCategory(c.getId());
|
long count = postService.countPostsByCategory(c.getId());
|
||||||
return toDto(c, count);
|
return categoryMapper.toDto(c, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{id}")
|
@DeleteMapping("/{id}")
|
||||||
@@ -38,8 +45,11 @@ public class CategoryController {
|
|||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public List<CategoryDto> list() {
|
public List<CategoryDto> list() {
|
||||||
return categoryService.listCategories().stream()
|
List<Category> all = categoryService.listCategories();
|
||||||
.map(c -> toDto(c, postService.countPostsByCategory(c.getId())))
|
List<Long> ids = all.stream().map(Category::getId).toList();
|
||||||
|
Map<Long, Long> postsCntByCategoryIds = postService.countPostsByCategoryIds(ids);
|
||||||
|
return all.stream()
|
||||||
|
.map(c -> categoryMapper.toDto(c, postsCntByCategoryIds.getOrDefault(c.getId(), 0L)))
|
||||||
.sorted((a, b) -> Long.compare(b.getCount(), a.getCount()))
|
.sorted((a, b) -> Long.compare(b.getCount(), a.getCount()))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
@@ -48,7 +58,7 @@ public class CategoryController {
|
|||||||
public CategoryDto get(@PathVariable Long id) {
|
public CategoryDto get(@PathVariable Long id) {
|
||||||
Category c = categoryService.getCategory(id);
|
Category c = categoryService.getCategory(id);
|
||||||
long count = postService.countPostsByCategory(c.getId());
|
long count = postService.countPostsByCategory(c.getId());
|
||||||
return toDto(c, count);
|
return categoryMapper.toDto(c, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}/posts")
|
@GetMapping("/{id}/posts")
|
||||||
@@ -57,47 +67,7 @@ public class CategoryController {
|
|||||||
@RequestParam(value = "pageSize", required = false) Integer pageSize) {
|
@RequestParam(value = "pageSize", required = false) Integer pageSize) {
|
||||||
return postService.listPostsByCategories(java.util.List.of(id), page, pageSize)
|
return postService.listPostsByCategories(java.util.List.of(id), page, pageSize)
|
||||||
.stream()
|
.stream()
|
||||||
.map(p -> {
|
.map(postMapper::toSummaryDto)
|
||||||
PostSummaryDto dto = new PostSummaryDto();
|
|
||||||
dto.setId(p.getId());
|
|
||||||
dto.setTitle(p.getTitle());
|
|
||||||
return dto;
|
|
||||||
})
|
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
private CategoryDto toDto(Category c, long count) {
|
|
||||||
CategoryDto dto = new CategoryDto();
|
|
||||||
dto.setId(c.getId());
|
|
||||||
dto.setName(c.getName());
|
|
||||||
dto.setIcon(c.getIcon());
|
|
||||||
dto.setSmallIcon(c.getSmallIcon());
|
|
||||||
dto.setDescription(c.getDescription());
|
|
||||||
dto.setCount(count);
|
|
||||||
return dto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
private static class CategoryRequest {
|
|
||||||
private String name;
|
|
||||||
private String description;
|
|
||||||
private String icon;
|
|
||||||
private String smallIcon;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
private static class CategoryDto {
|
|
||||||
private Long id;
|
|
||||||
private String name;
|
|
||||||
private String description;
|
|
||||||
private String icon;
|
|
||||||
private String smallIcon;
|
|
||||||
private Long count;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
private static class PostSummaryDto {
|
|
||||||
private Long id;
|
|
||||||
private String title;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.openisle.controller;
|
||||||
|
|
||||||
|
import com.openisle.dto.ChannelDto;
|
||||||
|
import com.openisle.model.User;
|
||||||
|
import com.openisle.repository.UserRepository;
|
||||||
|
import com.openisle.service.ChannelService;
|
||||||
|
import com.openisle.service.MessageService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/channels")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ChannelController {
|
||||||
|
private final ChannelService channelService;
|
||||||
|
private final MessageService messageService;
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
|
private Long getCurrentUserId(Authentication auth) {
|
||||||
|
User user = userRepository.findByUsername(auth.getName())
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("User not found"));
|
||||||
|
return user.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public List<ChannelDto> listChannels(Authentication auth) {
|
||||||
|
return channelService.listChannels(getCurrentUserId(auth));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{channelId}/join")
|
||||||
|
public ChannelDto joinChannel(@PathVariable Long channelId, Authentication auth) {
|
||||||
|
return channelService.joinChannel(channelId, getCurrentUserId(auth));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/unread-count")
|
||||||
|
public long unreadCount(Authentication auth) {
|
||||||
|
return messageService.getUnreadChannelCount(getCurrentUserId(auth));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package com.openisle.controller;
|
||||||
|
|
||||||
|
import com.openisle.model.Comment;
|
||||||
|
import com.openisle.dto.CommentDto;
|
||||||
|
import com.openisle.dto.CommentRequest;
|
||||||
|
import com.openisle.mapper.CommentMapper;
|
||||||
|
import com.openisle.service.CaptchaService;
|
||||||
|
import com.openisle.service.CommentService;
|
||||||
|
import com.openisle.service.LevelService;
|
||||||
|
import com.openisle.service.PointService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class CommentController {
|
||||||
|
private final CommentService commentService;
|
||||||
|
private final LevelService levelService;
|
||||||
|
private final CaptchaService captchaService;
|
||||||
|
private final CommentMapper commentMapper;
|
||||||
|
private final PointService pointService;
|
||||||
|
|
||||||
|
@Value("${app.captcha.enabled:false}")
|
||||||
|
private boolean captchaEnabled;
|
||||||
|
|
||||||
|
@Value("${app.captcha.comment-enabled:false}")
|
||||||
|
private boolean commentCaptchaEnabled;
|
||||||
|
|
||||||
|
@PostMapping("/posts/{postId}/comments")
|
||||||
|
public ResponseEntity<CommentDto> createComment(@PathVariable Long postId,
|
||||||
|
@RequestBody CommentRequest req,
|
||||||
|
Authentication auth) {
|
||||||
|
log.debug("createComment called by user {} for post {}", auth.getName(), postId);
|
||||||
|
if (captchaEnabled && commentCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
|
||||||
|
log.debug("Captcha verification failed for user {} on post {}", auth.getName(), postId);
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
Comment comment = commentService.addComment(auth.getName(), postId, req.getContent());
|
||||||
|
CommentDto dto = commentMapper.toDto(comment);
|
||||||
|
dto.setReward(levelService.awardForComment(auth.getName()));
|
||||||
|
dto.setPointReward(pointService.awardForComment(auth.getName(), postId, comment.getId()));
|
||||||
|
log.debug("createComment succeeded for comment {}", comment.getId());
|
||||||
|
return ResponseEntity.ok(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/comments/{commentId}/replies")
|
||||||
|
public ResponseEntity<CommentDto> replyComment(@PathVariable Long commentId,
|
||||||
|
@RequestBody CommentRequest req,
|
||||||
|
Authentication auth) {
|
||||||
|
log.debug("replyComment called by user {} for comment {}", auth.getName(), commentId);
|
||||||
|
if (captchaEnabled && commentCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
|
||||||
|
log.debug("Captcha verification failed for user {} on comment {}", auth.getName(), commentId);
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
Comment comment = commentService.addReply(auth.getName(), commentId, req.getContent());
|
||||||
|
CommentDto dto = commentMapper.toDto(comment);
|
||||||
|
dto.setReward(levelService.awardForComment(auth.getName()));
|
||||||
|
log.debug("replyComment succeeded for comment {}", comment.getId());
|
||||||
|
return ResponseEntity.ok(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/posts/{postId}/comments")
|
||||||
|
public List<CommentDto> listComments(@PathVariable Long postId,
|
||||||
|
@RequestParam(value = "sort", required = false, defaultValue = "OLDEST") com.openisle.model.CommentSort sort) {
|
||||||
|
log.debug("listComments called for post {} with sort {}", postId, sort);
|
||||||
|
List<CommentDto> list = commentService.getCommentsForPost(postId, sort).stream()
|
||||||
|
.map(commentMapper::toDtoWithReplies)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
log.debug("listComments returning {} comments", list.size());
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/comments/{id}")
|
||||||
|
public void deleteComment(@PathVariable Long id, Authentication auth) {
|
||||||
|
log.debug("deleteComment called by user {} for comment {}", auth.getName(), id);
|
||||||
|
commentService.deleteComment(auth.getName(), id);
|
||||||
|
log.debug("deleteComment completed for comment {}", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/comments/{id}/pin")
|
||||||
|
public CommentDto pinComment(@PathVariable Long id, Authentication auth) {
|
||||||
|
log.debug("pinComment called by user {} for comment {}", auth.getName(), id);
|
||||||
|
return commentMapper.toDto(commentService.pinComment(auth.getName(), id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/comments/{id}/unpin")
|
||||||
|
public CommentDto unpinComment(@PathVariable Long id, Authentication auth) {
|
||||||
|
log.debug("unpinComment called by user {} for comment {}", auth.getName(), id);
|
||||||
|
return commentMapper.toDto(commentService.unpinComment(auth.getName(), id));
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-16
@@ -1,9 +1,8 @@
|
|||||||
package com.openisle.controller;
|
package com.openisle.controller;
|
||||||
|
|
||||||
import lombok.Data;
|
import com.openisle.dto.SiteConfigDto;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import com.openisle.service.RegisterModeService;
|
import com.openisle.service.RegisterModeService;
|
||||||
import com.openisle.model.RegisterMode;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
@@ -34,8 +33,8 @@ public class ConfigController {
|
|||||||
private final RegisterModeService registerModeService;
|
private final RegisterModeService registerModeService;
|
||||||
|
|
||||||
@GetMapping("/config")
|
@GetMapping("/config")
|
||||||
public ConfigResponse getConfig() {
|
public SiteConfigDto getConfig() {
|
||||||
ConfigResponse resp = new ConfigResponse();
|
SiteConfigDto resp = new SiteConfigDto();
|
||||||
resp.setCaptchaEnabled(captchaEnabled);
|
resp.setCaptchaEnabled(captchaEnabled);
|
||||||
resp.setRegisterCaptchaEnabled(registerCaptchaEnabled);
|
resp.setRegisterCaptchaEnabled(registerCaptchaEnabled);
|
||||||
resp.setLoginCaptchaEnabled(loginCaptchaEnabled);
|
resp.setLoginCaptchaEnabled(loginCaptchaEnabled);
|
||||||
@@ -45,15 +44,4 @@ public class ConfigController {
|
|||||||
resp.setRegisterMode(registerModeService.getRegisterMode());
|
resp.setRegisterMode(registerModeService.getRegisterMode());
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
|
||||||
private static class ConfigResponse {
|
|
||||||
private boolean captchaEnabled;
|
|
||||||
private boolean registerCaptchaEnabled;
|
|
||||||
private boolean loginCaptchaEnabled;
|
|
||||||
private boolean postCaptchaEnabled;
|
|
||||||
private boolean commentCaptchaEnabled;
|
|
||||||
private int aiFormatLimit;
|
|
||||||
private RegisterMode registerMode;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+6
-35
@@ -1,32 +1,32 @@
|
|||||||
package com.openisle.controller;
|
package com.openisle.controller;
|
||||||
|
|
||||||
|
import com.openisle.dto.DraftDto;
|
||||||
|
import com.openisle.dto.DraftRequest;
|
||||||
|
import com.openisle.mapper.DraftMapper;
|
||||||
import com.openisle.model.Draft;
|
import com.openisle.model.Draft;
|
||||||
import com.openisle.service.DraftService;
|
import com.openisle.service.DraftService;
|
||||||
import lombok.Data;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/drafts")
|
@RequestMapping("/api/drafts")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class DraftController {
|
public class DraftController {
|
||||||
private final DraftService draftService;
|
private final DraftService draftService;
|
||||||
|
private final DraftMapper draftMapper;
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public ResponseEntity<DraftDto> saveDraft(@RequestBody DraftRequest req, Authentication auth) {
|
public ResponseEntity<DraftDto> saveDraft(@RequestBody DraftRequest req, Authentication auth) {
|
||||||
Draft draft = draftService.saveDraft(auth.getName(), req.getCategoryId(), req.getTitle(), req.getContent(), req.getTagIds());
|
Draft draft = draftService.saveDraft(auth.getName(), req.getCategoryId(), req.getTitle(), req.getContent(), req.getTagIds());
|
||||||
return ResponseEntity.ok(toDto(draft));
|
return ResponseEntity.ok(draftMapper.toDto(draft));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/me")
|
@GetMapping("/me")
|
||||||
public ResponseEntity<DraftDto> getMyDraft(Authentication auth) {
|
public ResponseEntity<DraftDto> getMyDraft(Authentication auth) {
|
||||||
return draftService.getDraft(auth.getName())
|
return draftService.getDraft(auth.getName())
|
||||||
.map(d -> ResponseEntity.ok(toDto(d)))
|
.map(d -> ResponseEntity.ok(draftMapper.toDto(d)))
|
||||||
.orElseGet(() -> ResponseEntity.noContent().build());
|
.orElseGet(() -> ResponseEntity.noContent().build());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,33 +35,4 @@ public class DraftController {
|
|||||||
draftService.deleteDraft(auth.getName());
|
draftService.deleteDraft(auth.getName());
|
||||||
return ResponseEntity.ok().build();
|
return ResponseEntity.ok().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private DraftDto toDto(Draft draft) {
|
|
||||||
DraftDto dto = new DraftDto();
|
|
||||||
dto.setId(draft.getId());
|
|
||||||
dto.setTitle(draft.getTitle());
|
|
||||||
dto.setContent(draft.getContent());
|
|
||||||
if (draft.getCategory() != null) {
|
|
||||||
dto.setCategoryId(draft.getCategory().getId());
|
|
||||||
}
|
|
||||||
dto.setTagIds(draft.getTags().stream().map(com.openisle.model.Tag::getId).collect(Collectors.toList()));
|
|
||||||
return dto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
private static class DraftRequest {
|
|
||||||
private String title;
|
|
||||||
private String content;
|
|
||||||
private Long categoryId;
|
|
||||||
private List<Long> tagIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
private static class DraftDto {
|
|
||||||
private Long id;
|
|
||||||
private String title;
|
|
||||||
private String content;
|
|
||||||
private Long categoryId;
|
|
||||||
private List<Long> tagIds;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+11
-1
@@ -5,6 +5,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler;
|
|||||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
import com.openisle.exception.FieldException;
|
import com.openisle.exception.FieldException;
|
||||||
import com.openisle.exception.NotFoundException;
|
import com.openisle.exception.NotFoundException;
|
||||||
|
import com.openisle.exception.RateLimitException;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -22,9 +23,18 @@ public class GlobalExceptionHandler {
|
|||||||
return ResponseEntity.status(404).body(Map.of("error", ex.getMessage()));
|
return ResponseEntity.status(404).body(Map.of("error", ex.getMessage()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(RateLimitException.class)
|
||||||
|
public ResponseEntity<?> handleRateLimitException(RateLimitException ex) {
|
||||||
|
return ResponseEntity.status(429).body(Map.of("error", ex.getMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
@ExceptionHandler(Exception.class)
|
@ExceptionHandler(Exception.class)
|
||||||
public ResponseEntity<?> handleException(Exception ex) {
|
public ResponseEntity<?> handleException(Exception ex) {
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", ex.getMessage()));
|
String message = ex.getMessage();
|
||||||
|
if (message == null) {
|
||||||
|
message = ex.getClass().getSimpleName();
|
||||||
|
}
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", message));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.openisle.controller;
|
||||||
|
|
||||||
|
import com.openisle.service.InviteService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/invite")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InviteController {
|
||||||
|
private final InviteService inviteService;
|
||||||
|
|
||||||
|
@PostMapping("/generate")
|
||||||
|
public Map<String, String> generate(Authentication auth) {
|
||||||
|
String token = inviteService.generate(auth.getName());
|
||||||
|
return Map.of("token", token);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.openisle.controller;
|
||||||
|
|
||||||
|
import com.openisle.dto.MedalDto;
|
||||||
|
import com.openisle.dto.MedalSelectRequest;
|
||||||
|
import com.openisle.service.MedalService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/medals")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MedalController {
|
||||||
|
private final MedalService medalService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public List<MedalDto> getMedals(@RequestParam(value = "userId", required = false) Long userId) {
|
||||||
|
return medalService.getMedals(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/select")
|
||||||
|
public ResponseEntity<Void> selectMedal(@RequestBody MedalSelectRequest req, Authentication auth) {
|
||||||
|
try {
|
||||||
|
medalService.selectMedal(auth.getName(), req.getType());
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package com.openisle.controller;
|
||||||
|
|
||||||
|
import com.openisle.dto.ConversationDetailDto;
|
||||||
|
import com.openisle.dto.ConversationDto;
|
||||||
|
import com.openisle.dto.CreateConversationRequest;
|
||||||
|
import com.openisle.dto.CreateConversationResponse;
|
||||||
|
import com.openisle.dto.MessageDto;
|
||||||
|
import com.openisle.model.Message;
|
||||||
|
import com.openisle.model.MessageConversation;
|
||||||
|
import com.openisle.model.User;
|
||||||
|
import com.openisle.repository.UserRepository;
|
||||||
|
import com.openisle.service.MessageService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/messages")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MessageController {
|
||||||
|
|
||||||
|
private final MessageService messageService;
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
|
// This is a placeholder for getting the current user's ID
|
||||||
|
private Long getCurrentUserId(Authentication auth) {
|
||||||
|
User user = userRepository.findByUsername(auth.getName()).orElseThrow(() -> new IllegalArgumentException("Sender not found"));
|
||||||
|
// In a real application, you would get this from the Authentication object
|
||||||
|
return user.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/conversations")
|
||||||
|
public ResponseEntity<List<ConversationDto>> getConversations(Authentication auth) {
|
||||||
|
List<ConversationDto> conversations = messageService.getConversations(getCurrentUserId(auth));
|
||||||
|
return ResponseEntity.ok(conversations);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/conversations/{conversationId}")
|
||||||
|
public ResponseEntity<ConversationDetailDto> getMessages(@PathVariable Long conversationId,
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "20") int size,
|
||||||
|
Authentication auth) {
|
||||||
|
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
|
||||||
|
ConversationDetailDto conversationDetails = messageService.getConversationDetails(conversationId, getCurrentUserId(auth), pageable);
|
||||||
|
return ResponseEntity.ok(conversationDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<MessageDto> sendMessage(@RequestBody MessageRequest req, Authentication auth) {
|
||||||
|
Message message = messageService.sendMessage(getCurrentUserId(auth), req.getRecipientId(), req.getContent(), req.getReplyToId());
|
||||||
|
return ResponseEntity.ok(messageService.toDto(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/conversations/{conversationId}/messages")
|
||||||
|
public ResponseEntity<MessageDto> sendMessageToConversation(@PathVariable Long conversationId,
|
||||||
|
@RequestBody ChannelMessageRequest req,
|
||||||
|
Authentication auth) {
|
||||||
|
Message message = messageService.sendMessageToConversation(getCurrentUserId(auth), conversationId, req.getContent(), req.getReplyToId());
|
||||||
|
return ResponseEntity.ok(messageService.toDto(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/conversations/{conversationId}/read")
|
||||||
|
public ResponseEntity<Void> markAsRead(@PathVariable Long conversationId, Authentication auth) {
|
||||||
|
messageService.markConversationAsRead(conversationId, getCurrentUserId(auth));
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/conversations")
|
||||||
|
public ResponseEntity<CreateConversationResponse> findOrCreateConversation(@RequestBody CreateConversationRequest req, Authentication auth) {
|
||||||
|
MessageConversation conversation = messageService.findOrCreateConversation(getCurrentUserId(auth), req.getRecipientId());
|
||||||
|
return ResponseEntity.ok(new CreateConversationResponse(conversation.getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/unread-count")
|
||||||
|
public ResponseEntity<Long> getUnreadCount(Authentication auth) {
|
||||||
|
return ResponseEntity.ok(messageService.getUnreadMessageCount(getCurrentUserId(auth)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A simple request DTO
|
||||||
|
static class MessageRequest {
|
||||||
|
private Long recipientId;
|
||||||
|
private String content;
|
||||||
|
private Long replyToId;
|
||||||
|
|
||||||
|
public Long getRecipientId() {
|
||||||
|
return recipientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRecipientId(Long recipientId) {
|
||||||
|
this.recipientId = recipientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContent() {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContent(String content) {
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getReplyToId() {
|
||||||
|
return replyToId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReplyToId(Long replyToId) {
|
||||||
|
this.replyToId = replyToId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ChannelMessageRequest {
|
||||||
|
private String content;
|
||||||
|
private Long replyToId;
|
||||||
|
|
||||||
|
public String getContent() {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContent(String content) {
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getReplyToId() {
|
||||||
|
return replyToId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReplyToId(Long replyToId) {
|
||||||
|
this.replyToId = replyToId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package com.openisle.controller;
|
||||||
|
|
||||||
|
import com.openisle.dto.NotificationDto;
|
||||||
|
import com.openisle.dto.NotificationMarkReadRequest;
|
||||||
|
import com.openisle.dto.NotificationUnreadCountDto;
|
||||||
|
import com.openisle.dto.NotificationPreferenceDto;
|
||||||
|
import com.openisle.dto.NotificationPreferenceUpdateRequest;
|
||||||
|
import com.openisle.mapper.NotificationMapper;
|
||||||
|
import com.openisle.service.NotificationService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/** Endpoints for user notifications. */
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/notifications")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class NotificationController {
|
||||||
|
private final NotificationService notificationService;
|
||||||
|
private final NotificationMapper notificationMapper;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public List<NotificationDto> list(@RequestParam(value = "page", defaultValue = "0") int page,
|
||||||
|
@RequestParam(value = "size", defaultValue = "30") int size,
|
||||||
|
Authentication auth) {
|
||||||
|
return notificationService.listNotifications(auth.getName(), null, page, size).stream()
|
||||||
|
.map(notificationMapper::toDto)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/unread")
|
||||||
|
public List<NotificationDto> listUnread(@RequestParam(value = "page", defaultValue = "0") int page,
|
||||||
|
@RequestParam(value = "size", defaultValue = "30") int size,
|
||||||
|
Authentication auth) {
|
||||||
|
return notificationService.listNotifications(auth.getName(), false, page, size).stream()
|
||||||
|
.map(notificationMapper::toDto)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/unread-count")
|
||||||
|
public NotificationUnreadCountDto unreadCount(Authentication auth) {
|
||||||
|
long count = notificationService.countUnread(auth.getName());
|
||||||
|
NotificationUnreadCountDto uc = new NotificationUnreadCountDto();
|
||||||
|
uc.setCount(count);
|
||||||
|
return uc;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/read")
|
||||||
|
public void markRead(@RequestBody NotificationMarkReadRequest req, Authentication auth) {
|
||||||
|
notificationService.markRead(auth.getName(), req.getIds());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/prefs")
|
||||||
|
public List<NotificationPreferenceDto> prefs(Authentication auth) {
|
||||||
|
return notificationService.listPreferences(auth.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/prefs")
|
||||||
|
public void updatePref(@RequestBody NotificationPreferenceUpdateRequest req, Authentication auth) {
|
||||||
|
notificationService.updatePreference(auth.getName(), req.getType(), req.isEnabled());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.openisle.controller;
|
||||||
|
|
||||||
|
import com.openisle.dto.PointHistoryDto;
|
||||||
|
import com.openisle.mapper.PointHistoryMapper;
|
||||||
|
import com.openisle.service.PointService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/point-histories")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PointHistoryController {
|
||||||
|
private final PointService pointService;
|
||||||
|
private final PointHistoryMapper pointHistoryMapper;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public List<PointHistoryDto> list(Authentication auth) {
|
||||||
|
return pointService.listHistory(auth.getName()).stream()
|
||||||
|
.map(pointHistoryMapper::toDto)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.openisle.controller;
|
||||||
|
|
||||||
|
import com.openisle.dto.PointGoodDto;
|
||||||
|
import com.openisle.dto.PointRedeemRequest;
|
||||||
|
import com.openisle.mapper.PointGoodMapper;
|
||||||
|
import com.openisle.model.User;
|
||||||
|
import com.openisle.service.PointMallService;
|
||||||
|
import com.openisle.service.UserService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/** REST controller for point mall. */
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/point-goods")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PointMallController {
|
||||||
|
private final PointMallService pointMallService;
|
||||||
|
private final UserService userService;
|
||||||
|
private final PointGoodMapper pointGoodMapper;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public List<PointGoodDto> list() {
|
||||||
|
return pointMallService.listGoods().stream()
|
||||||
|
.map(pointGoodMapper::toDto)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/redeem")
|
||||||
|
public Map<String, Integer> redeem(@RequestBody PointRedeemRequest req, Authentication auth) {
|
||||||
|
User user = userService.findByIdentifier(auth.getName()).orElseThrow();
|
||||||
|
int point = pointMallService.redeem(user, req.getGoodId(), req.getContact());
|
||||||
|
return Map.of("point", point);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
package com.openisle.controller;
|
||||||
|
|
||||||
|
import com.openisle.dto.PostDetailDto;
|
||||||
|
import com.openisle.dto.PostRequest;
|
||||||
|
import com.openisle.dto.PostSummaryDto;
|
||||||
|
import com.openisle.mapper.PostMapper;
|
||||||
|
import com.openisle.model.Post;
|
||||||
|
import com.openisle.service.*;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/posts")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PostController {
|
||||||
|
private final PostService postService;
|
||||||
|
private final LevelService levelService;
|
||||||
|
private final CaptchaService captchaService;
|
||||||
|
private final DraftService draftService;
|
||||||
|
private final UserVisitService userVisitService;
|
||||||
|
private final PostMapper postMapper;
|
||||||
|
private final PointService pointService;
|
||||||
|
|
||||||
|
@Value("${app.captcha.enabled:false}")
|
||||||
|
private boolean captchaEnabled;
|
||||||
|
|
||||||
|
@Value("${app.captcha.post-enabled:false}")
|
||||||
|
private boolean postCaptchaEnabled;
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<PostDetailDto> createPost(@RequestBody PostRequest req, Authentication auth) {
|
||||||
|
if (captchaEnabled && postCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
Post post = postService.createPost(auth.getName(), req.getCategoryId(),
|
||||||
|
req.getTitle(), req.getContent(), req.getTagIds(),
|
||||||
|
req.getType(), req.getPrizeDescription(), req.getPrizeIcon(),
|
||||||
|
req.getPrizeCount(), req.getStartTime(), req.getEndTime());
|
||||||
|
draftService.deleteDraft(auth.getName());
|
||||||
|
PostDetailDto dto = postMapper.toDetailDto(post, auth.getName());
|
||||||
|
dto.setReward(levelService.awardForPost(auth.getName()));
|
||||||
|
dto.setPointReward(pointService.awardForPost(auth.getName(), post.getId()));
|
||||||
|
return ResponseEntity.ok(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ResponseEntity<PostDetailDto> updatePost(@PathVariable Long id, @RequestBody PostRequest req,
|
||||||
|
Authentication auth) {
|
||||||
|
Post post = postService.updatePost(id, auth.getName(), req.getCategoryId(),
|
||||||
|
req.getTitle(), req.getContent(), req.getTagIds());
|
||||||
|
return ResponseEntity.ok(postMapper.toDetailDto(post, auth.getName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public void deletePost(@PathVariable Long id, Authentication auth) {
|
||||||
|
postService.deletePost(id, auth.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/close")
|
||||||
|
public PostSummaryDto close(@PathVariable Long id, Authentication auth) {
|
||||||
|
return postMapper.toSummaryDto(postService.closePost(id, auth.getName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/reopen")
|
||||||
|
public PostSummaryDto reopen(@PathVariable Long id, Authentication auth) {
|
||||||
|
return postMapper.toSummaryDto(postService.reopenPost(id, auth.getName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity<PostDetailDto> getPost(@PathVariable Long id, Authentication auth) {
|
||||||
|
String viewer = auth != null ? auth.getName() : null;
|
||||||
|
Post post = postService.viewPost(id, viewer);
|
||||||
|
return ResponseEntity.ok(postMapper.toDetailDto(post, viewer));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/lottery/join")
|
||||||
|
public ResponseEntity<Void> joinLottery(@PathVariable Long id, Authentication auth) {
|
||||||
|
postService.joinLottery(id, auth.getName());
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public List<PostSummaryDto> listPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
|
||||||
|
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
|
||||||
|
@RequestParam(value = "tagId", required = false) Long tagId,
|
||||||
|
@RequestParam(value = "tagIds", required = false) List<Long> tagIds,
|
||||||
|
@RequestParam(value = "page", required = false) Integer page,
|
||||||
|
@RequestParam(value = "pageSize", required = false) Integer pageSize,
|
||||||
|
Authentication auth) {
|
||||||
|
List<Long> ids = categoryIds;
|
||||||
|
if (categoryId != null) {
|
||||||
|
ids = java.util.List.of(categoryId);
|
||||||
|
}
|
||||||
|
List<Long> tids = tagIds;
|
||||||
|
if (tagId != null) {
|
||||||
|
tids = java.util.List.of(tagId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auth != null) {
|
||||||
|
userVisitService.recordVisit(auth.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean hasCategories = ids != null && !ids.isEmpty();
|
||||||
|
boolean hasTags = tids != null && !tids.isEmpty();
|
||||||
|
|
||||||
|
if (hasCategories && hasTags) {
|
||||||
|
return postService.listPostsByCategoriesAndTags(ids, tids, page, pageSize)
|
||||||
|
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
if (hasTags) {
|
||||||
|
return postService.listPostsByTags(tids, page, pageSize)
|
||||||
|
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
return postService.listPostsByCategories(ids, page, pageSize)
|
||||||
|
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/ranking")
|
||||||
|
public List<PostSummaryDto> rankingPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
|
||||||
|
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
|
||||||
|
@RequestParam(value = "tagId", required = false) Long tagId,
|
||||||
|
@RequestParam(value = "tagIds", required = false) List<Long> tagIds,
|
||||||
|
@RequestParam(value = "page", required = false) Integer page,
|
||||||
|
@RequestParam(value = "pageSize", required = false) Integer pageSize,
|
||||||
|
Authentication auth) {
|
||||||
|
List<Long> ids = categoryIds;
|
||||||
|
if (categoryId != null) {
|
||||||
|
ids = java.util.List.of(categoryId);
|
||||||
|
}
|
||||||
|
List<Long> tids = tagIds;
|
||||||
|
if (tagId != null) {
|
||||||
|
tids = java.util.List.of(tagId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auth != null) {
|
||||||
|
userVisitService.recordVisit(auth.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
return postService.listPostsByViews(ids, tids, page, pageSize)
|
||||||
|
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/latest-reply")
|
||||||
|
public List<PostSummaryDto> latestReplyPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
|
||||||
|
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
|
||||||
|
@RequestParam(value = "tagId", required = false) Long tagId,
|
||||||
|
@RequestParam(value = "tagIds", required = false) List<Long> tagIds,
|
||||||
|
@RequestParam(value = "page", required = false) Integer page,
|
||||||
|
@RequestParam(value = "pageSize", required = false) Integer pageSize,
|
||||||
|
Authentication auth) {
|
||||||
|
List<Long> ids = categoryIds;
|
||||||
|
if (categoryId != null) {
|
||||||
|
ids = java.util.List.of(categoryId);
|
||||||
|
}
|
||||||
|
List<Long> tids = tagIds;
|
||||||
|
if (tagId != null) {
|
||||||
|
tids = java.util.List.of(tagId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auth != null) {
|
||||||
|
userVisitService.recordVisit(auth.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
return postService.listPostsByLatestReply(ids, tids, page, pageSize)
|
||||||
|
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/featured")
|
||||||
|
public List<PostSummaryDto> featuredPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
|
||||||
|
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
|
||||||
|
@RequestParam(value = "tagId", required = false) Long tagId,
|
||||||
|
@RequestParam(value = "tagIds", required = false) List<Long> tagIds,
|
||||||
|
@RequestParam(value = "page", required = false) Integer page,
|
||||||
|
@RequestParam(value = "pageSize", required = false) Integer pageSize,
|
||||||
|
Authentication auth) {
|
||||||
|
List<Long> ids = categoryIds;
|
||||||
|
if (categoryId != null) {
|
||||||
|
ids = java.util.List.of(categoryId);
|
||||||
|
}
|
||||||
|
List<Long> tids = tagIds;
|
||||||
|
if (tagId != null) {
|
||||||
|
tids = java.util.List.of(tagId);
|
||||||
|
}
|
||||||
|
if (auth != null) {
|
||||||
|
userVisitService.recordVisit(auth.getName());
|
||||||
|
}
|
||||||
|
return postService.listFeaturedPosts(ids, tids, page, pageSize)
|
||||||
|
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.openisle.controller;
|
||||||
|
|
||||||
|
import com.openisle.dto.PushPublicKeyDto;
|
||||||
|
import com.openisle.dto.PushSubscriptionRequest;
|
||||||
|
import com.openisle.service.PushSubscriptionService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/push")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PushSubscriptionController {
|
||||||
|
private final PushSubscriptionService pushSubscriptionService;
|
||||||
|
@Value("${app.webpush.public-key}")
|
||||||
|
private String publicKey;
|
||||||
|
|
||||||
|
@GetMapping("/public-key")
|
||||||
|
public PushPublicKeyDto getPublicKey() {
|
||||||
|
PushPublicKeyDto r = new PushPublicKeyDto();
|
||||||
|
r.setKey(publicKey);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/subscribe")
|
||||||
|
public void subscribe(@RequestBody PushSubscriptionRequest req, Authentication auth) {
|
||||||
|
pushSubscriptionService.saveSubscription(auth.getName(), req.getEndpoint(), req.getP256dh(), req.getAuth());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package com.openisle.controller;
|
||||||
|
|
||||||
|
import com.openisle.dto.ReactionDto;
|
||||||
|
import com.openisle.dto.ReactionRequest;
|
||||||
|
import com.openisle.mapper.ReactionMapper;
|
||||||
|
import com.openisle.model.Reaction;
|
||||||
|
import com.openisle.model.ReactionType;
|
||||||
|
import com.openisle.service.LevelService;
|
||||||
|
import com.openisle.service.PointService;
|
||||||
|
import com.openisle.service.ReactionService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ReactionController {
|
||||||
|
private final ReactionService reactionService;
|
||||||
|
private final LevelService levelService;
|
||||||
|
private final ReactionMapper reactionMapper;
|
||||||
|
private final PointService pointService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all available reaction types.
|
||||||
|
*/
|
||||||
|
@GetMapping("/reaction-types")
|
||||||
|
public ReactionType[] listReactionTypes() {
|
||||||
|
return ReactionType.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/posts/{postId}/reactions")
|
||||||
|
public ResponseEntity<ReactionDto> reactToPost(@PathVariable Long postId,
|
||||||
|
@RequestBody ReactionRequest req,
|
||||||
|
Authentication auth) {
|
||||||
|
Reaction reaction = reactionService.reactToPost(auth.getName(), postId, req.getType());
|
||||||
|
if (reaction == null) {
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
ReactionDto dto = reactionMapper.toDto(reaction);
|
||||||
|
dto.setReward(levelService.awardForReaction(auth.getName()));
|
||||||
|
pointService.awardForReactionOfPost(auth.getName(), postId);
|
||||||
|
return ResponseEntity.ok(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/comments/{commentId}/reactions")
|
||||||
|
public ResponseEntity<ReactionDto> reactToComment(@PathVariable Long commentId,
|
||||||
|
@RequestBody ReactionRequest req,
|
||||||
|
Authentication auth) {
|
||||||
|
Reaction reaction = reactionService.reactToComment(auth.getName(), commentId, req.getType());
|
||||||
|
if (reaction == null) {
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
ReactionDto dto = reactionMapper.toDto(reaction);
|
||||||
|
dto.setReward(levelService.awardForReaction(auth.getName()));
|
||||||
|
pointService.awardForReactionOfComment(auth.getName(), commentId);
|
||||||
|
return ResponseEntity.ok(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/messages/{messageId}/reactions")
|
||||||
|
public ResponseEntity<ReactionDto> reactToMessage(@PathVariable Long messageId,
|
||||||
|
@RequestBody ReactionRequest req,
|
||||||
|
Authentication auth) {
|
||||||
|
Reaction reaction = reactionService.reactToMessage(auth.getName(), messageId, req.getType());
|
||||||
|
if (reaction == null) {
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
ReactionDto dto = reactionMapper.toDto(reaction);
|
||||||
|
dto.setReward(levelService.awardForReaction(auth.getName()));
|
||||||
|
return ResponseEntity.ok(dto);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,352 @@
|
|||||||
|
package com.openisle.controller;
|
||||||
|
|
||||||
|
import com.openisle.model.Post;
|
||||||
|
import com.openisle.model.Comment;
|
||||||
|
import com.openisle.model.CommentSort;
|
||||||
|
import com.openisle.service.PostService;
|
||||||
|
import com.openisle.service.CommentService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.jsoup.Jsoup;
|
||||||
|
import org.jsoup.nodes.Document;
|
||||||
|
import org.jsoup.nodes.Element;
|
||||||
|
import org.jsoup.safety.Safelist;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import com.vladsch.flexmark.ext.autolink.AutolinkExtension;
|
||||||
|
import com.vladsch.flexmark.ext.tables.TablesExtension;
|
||||||
|
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension;
|
||||||
|
import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension;
|
||||||
|
import com.vladsch.flexmark.html.HtmlRenderer;
|
||||||
|
import com.vladsch.flexmark.parser.Parser;
|
||||||
|
import com.vladsch.flexmark.util.data.MutableDataSet;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RssController {
|
||||||
|
private final PostService postService;
|
||||||
|
private final CommentService commentService;
|
||||||
|
|
||||||
|
@Value("${app.website-url:https://www.open-isle.com}")
|
||||||
|
private String websiteUrl;
|
||||||
|
|
||||||
|
// 兼容 Markdown/HTML 两类图片写法(用于 enclosure)
|
||||||
|
private static final Pattern MD_IMAGE = Pattern.compile("!\\[[^\\]]*\\]\\(([^)]+)\\)");
|
||||||
|
private static final Pattern HTML_IMAGE = Pattern.compile("<img[^>]+src=[\"']?([^\"'>]+)[\"']?[^>]*>");
|
||||||
|
|
||||||
|
private static final DateTimeFormatter RFC1123 = DateTimeFormatter.RFC_1123_DATE_TIME;
|
||||||
|
|
||||||
|
// flexmark:Markdown -> HTML
|
||||||
|
private static final Parser MD_PARSER;
|
||||||
|
private static final HtmlRenderer MD_RENDERER;
|
||||||
|
static {
|
||||||
|
MutableDataSet opts = new MutableDataSet();
|
||||||
|
opts.set(Parser.EXTENSIONS, Arrays.asList(
|
||||||
|
TablesExtension.create(),
|
||||||
|
AutolinkExtension.create(),
|
||||||
|
StrikethroughExtension.create(),
|
||||||
|
TaskListExtension.create()
|
||||||
|
));
|
||||||
|
// 允许内联 HTML(下游再做 sanitize)
|
||||||
|
opts.set(Parser.HTML_BLOCK_PARSER, true);
|
||||||
|
MD_PARSER = Parser.builder(opts).build();
|
||||||
|
MD_RENDERER = HtmlRenderer.builder(opts).escapeHtml(false).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(value = "/api/rss", produces = "application/rss+xml;charset=UTF-8")
|
||||||
|
public String feed() {
|
||||||
|
// 建议 20;你现在是 10,这里保留你的 10
|
||||||
|
List<Post> posts = postService.listLatestRssPosts(10);
|
||||||
|
String base = trimTrailingSlash(websiteUrl);
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder(4096);
|
||||||
|
sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
|
||||||
|
sb.append("<rss version=\"2.0\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">");
|
||||||
|
sb.append("<channel>");
|
||||||
|
elem(sb, "title", cdata("OpenIsle RSS"));
|
||||||
|
elem(sb, "link", base + "/");
|
||||||
|
elem(sb, "description", cdata("Latest posts"));
|
||||||
|
ZonedDateTime updated = posts.stream()
|
||||||
|
.map(p -> p.getCreatedAt().atZone(ZoneId.systemDefault()))
|
||||||
|
.max(Comparator.naturalOrder())
|
||||||
|
.orElse(ZonedDateTime.now());
|
||||||
|
// channel lastBuildDate(GMT)
|
||||||
|
elem(sb, "lastBuildDate", toRfc1123Gmt(updated));
|
||||||
|
|
||||||
|
for (Post p : posts) {
|
||||||
|
String link = base + "/posts/" + p.getId();
|
||||||
|
|
||||||
|
// 1) Markdown -> HTML
|
||||||
|
String html = renderMarkdown(p.getContent());
|
||||||
|
|
||||||
|
// 2) Sanitize(白名单增强)
|
||||||
|
String safeHtml = sanitizeHtml(html);
|
||||||
|
|
||||||
|
// 3) 绝对化 href/src + 强制 rel/target
|
||||||
|
String absHtml = absolutifyHtml(safeHtml, base);
|
||||||
|
|
||||||
|
// 4) 纯文本摘要(用于 <description>)
|
||||||
|
String plain = textSummary(absHtml, 180);
|
||||||
|
|
||||||
|
// 5) enclosure(首图,已绝对化)
|
||||||
|
String enclosure = firstImage(p.getContent());
|
||||||
|
if (enclosure == null) {
|
||||||
|
// 如果 Markdown 没有图,尝试从渲染后的 HTML 再抓一次
|
||||||
|
enclosure = firstImage(absHtml);
|
||||||
|
}
|
||||||
|
if (enclosure != null) {
|
||||||
|
enclosure = absolutifyUrl(enclosure, base);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6) 构造优雅的附加区块(原文链接 + 精选评论),编入 <content:encoded>
|
||||||
|
List<Comment> topComments = commentService
|
||||||
|
.getCommentsForPost(p.getId(), CommentSort.MOST_INTERACTIONS);
|
||||||
|
topComments = topComments.subList(0, Math.min(10, topComments.size()));
|
||||||
|
String footerHtml = buildFooterHtml(base, link, topComments);
|
||||||
|
|
||||||
|
sb.append("<item>");
|
||||||
|
elem(sb, "title", cdata(nullSafe(p.getTitle())));
|
||||||
|
elem(sb, "link", link);
|
||||||
|
sb.append("<guid isPermaLink=\"true\">").append(link).append("</guid>");
|
||||||
|
elem(sb, "pubDate", toRfc1123Gmt(p.getCreatedAt().atZone(ZoneId.systemDefault())));
|
||||||
|
// 摘要
|
||||||
|
elem(sb, "description", cdata(plain));
|
||||||
|
// 全文(HTML):正文 + 优雅的 Markdown 区块(已转 HTML)
|
||||||
|
sb.append("<content:encoded><![CDATA[")
|
||||||
|
.append(absHtml)
|
||||||
|
.append(footerHtml)
|
||||||
|
.append("]]></content:encoded>");
|
||||||
|
// 首图 enclosure(图片类型)
|
||||||
|
if (enclosure != null) {
|
||||||
|
sb.append("<enclosure url=\"").append(escapeXml(enclosure)).append("\" type=\"")
|
||||||
|
.append(getMimeType(enclosure)).append("\" />");
|
||||||
|
}
|
||||||
|
sb.append("</item>");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append("</channel></rss>");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================== Markdown → HTML ===================== */
|
||||||
|
|
||||||
|
private static String renderMarkdown(String md) {
|
||||||
|
if (md == null || md.isEmpty()) return "";
|
||||||
|
return MD_RENDERER.render(MD_PARSER.parse(md));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================== Sanitize & 绝对化 ===================== */
|
||||||
|
|
||||||
|
private static String sanitizeHtml(String html) {
|
||||||
|
if (html == null) return "";
|
||||||
|
Safelist wl = Safelist.relaxed()
|
||||||
|
.addTags(
|
||||||
|
"pre","code","figure","figcaption","picture","source",
|
||||||
|
"table","thead","tbody","tr","th","td",
|
||||||
|
"h1","h2","h3","h4","h5","h6",
|
||||||
|
"hr","blockquote"
|
||||||
|
)
|
||||||
|
.addAttributes("a", "href", "title", "target", "rel")
|
||||||
|
.addAttributes("img", "src", "alt", "title", "width", "height")
|
||||||
|
.addAttributes("source", "srcset", "type", "media")
|
||||||
|
.addAttributes("code", "class")
|
||||||
|
.addAttributes("pre", "class")
|
||||||
|
.addProtocols("a", "href", "http", "https", "mailto")
|
||||||
|
.addProtocols("img", "src", "http", "https", "data")
|
||||||
|
.addProtocols("source", "srcset", "http", "https");
|
||||||
|
// 清除所有 on* 事件、style(避免阅读器环境差异)
|
||||||
|
return Jsoup.clean(html, wl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String absolutifyHtml(String html, String baseUrl) {
|
||||||
|
if (html == null || html.isEmpty()) return "";
|
||||||
|
Document doc = Jsoup.parseBodyFragment(html, baseUrl);
|
||||||
|
// a[href]
|
||||||
|
for (Element a : doc.select("a[href]")) {
|
||||||
|
String href = a.attr("href");
|
||||||
|
String abs = absolutifyUrl(href, baseUrl);
|
||||||
|
a.attr("href", abs);
|
||||||
|
// 强制外链安全属性
|
||||||
|
a.attr("rel", "noopener noreferrer nofollow");
|
||||||
|
a.attr("target", "_blank");
|
||||||
|
}
|
||||||
|
// img[src]
|
||||||
|
for (Element img : doc.select("img[src]")) {
|
||||||
|
String src = img.attr("src");
|
||||||
|
String abs = absolutifyUrl(src, baseUrl);
|
||||||
|
img.attr("src", abs);
|
||||||
|
}
|
||||||
|
// source[srcset] (picture/webp)
|
||||||
|
for (Element s : doc.select("source[srcset]")) {
|
||||||
|
String srcset = s.attr("srcset");
|
||||||
|
s.attr("srcset", absolutifySrcset(srcset, baseUrl));
|
||||||
|
}
|
||||||
|
return doc.body().html();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String absolutifyUrl(String url, String baseUrl) {
|
||||||
|
if (url == null || url.isEmpty()) return url;
|
||||||
|
String u = url.trim();
|
||||||
|
if (u.startsWith("//")) {
|
||||||
|
return "https:" + u;
|
||||||
|
}
|
||||||
|
if (u.startsWith("#")) {
|
||||||
|
// 保留页面内锚点:拼接到首页(也可拼接到当前帖子的 link,但此处无上下文)
|
||||||
|
return baseUrl + "/" + u;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
URI base = URI.create(ensureTrailingSlash(baseUrl));
|
||||||
|
URI abs = base.resolve(u);
|
||||||
|
return abs.toString();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String absolutifySrcset(String srcset, String baseUrl) {
|
||||||
|
if (srcset == null || srcset.isEmpty()) return srcset;
|
||||||
|
String[] parts = srcset.split(",");
|
||||||
|
List<String> out = new ArrayList<>(parts.length);
|
||||||
|
for (String part : parts) {
|
||||||
|
String p = part.trim();
|
||||||
|
if (p.isEmpty()) continue;
|
||||||
|
String[] seg = p.split("\\s+");
|
||||||
|
String url = seg[0];
|
||||||
|
String size = seg.length > 1 ? seg[1] : "";
|
||||||
|
out.add(absolutifyUrl(url, baseUrl) + (size.isEmpty() ? "" : " " + size));
|
||||||
|
}
|
||||||
|
return String.join(", ", out);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================== 摘要 & enclosure ===================== */
|
||||||
|
|
||||||
|
private static String textSummary(String html, int maxLen) {
|
||||||
|
if (html == null) return "";
|
||||||
|
String text = Jsoup.parse(html).text().replaceAll("\\s+", " ").trim();
|
||||||
|
if (text.length() <= maxLen) return text;
|
||||||
|
return text.substring(0, maxLen) + "…";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String firstImage(String content) {
|
||||||
|
if (content == null) return null;
|
||||||
|
Matcher m = MD_IMAGE.matcher(content);
|
||||||
|
if (m.find()) return m.group(1);
|
||||||
|
m = HTML_IMAGE.matcher(content);
|
||||||
|
if (m.find()) return m.group(1);
|
||||||
|
// 再从纯 HTML 里解析一次(如果传入的是渲染后的)
|
||||||
|
try {
|
||||||
|
Document doc = Jsoup.parse(content);
|
||||||
|
Element img = doc.selectFirst("img[src]");
|
||||||
|
if (img != null) return img.attr("src");
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getMimeType(String url) {
|
||||||
|
String lower = url == null ? "" : url.toLowerCase(Locale.ROOT);
|
||||||
|
if (lower.endsWith(".png")) return "image/png";
|
||||||
|
if (lower.endsWith(".gif")) return "image/gif";
|
||||||
|
if (lower.endsWith(".webp")) return "image/webp";
|
||||||
|
if (lower.endsWith(".svg")) return "image/svg+xml";
|
||||||
|
if (lower.endsWith(".avif")) return "image/avif";
|
||||||
|
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
||||||
|
// 默认兜底
|
||||||
|
return "image/jpeg";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================== 附加区块(原文链接 + 精选评论) ===================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将“原文链接 + 精选评论(最多 10 条)”以优雅的 Markdown 形式渲染为 HTML,
|
||||||
|
* 并做 sanitize + 绝对化,然后拼入 content:encoded 尾部。
|
||||||
|
*/
|
||||||
|
private static String buildFooterHtml(String baseUrl, String originalLink, List<Comment> topComments) {
|
||||||
|
StringBuilder md = new StringBuilder(256);
|
||||||
|
|
||||||
|
// 分割线
|
||||||
|
md.append("\n\n---\n\n");
|
||||||
|
|
||||||
|
// 原文链接(强调 + 可点击)
|
||||||
|
md.append("**原文链接:** ")
|
||||||
|
.append("[").append(originalLink).append("](").append(originalLink).append(")")
|
||||||
|
.append("\n\n");
|
||||||
|
|
||||||
|
// 精选评论(仅当有评论时展示)
|
||||||
|
if (topComments != null && !topComments.isEmpty()) {
|
||||||
|
md.append("### 精选评论(Top ").append(Math.min(10, topComments.size())).append(")\n\n");
|
||||||
|
for (Comment c : topComments) {
|
||||||
|
String author = usernameOf(c);
|
||||||
|
String content = nullSafe(c.getContent()).replace("\r", "");
|
||||||
|
// 使用引用样式展示,提升可读性
|
||||||
|
md.append("> @").append(author).append(": ").append(content).append("\n\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染为 HTML,并保持和正文一致的处理流程
|
||||||
|
String html = renderMarkdown(md.toString());
|
||||||
|
String safe = sanitizeHtml(html);
|
||||||
|
return absolutifyHtml(safe, baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String usernameOf(Comment c) {
|
||||||
|
if (c == null) return "匿名";
|
||||||
|
try {
|
||||||
|
Object authorObj = c.getAuthor();
|
||||||
|
if (authorObj == null) return "匿名";
|
||||||
|
// 反射避免直接依赖实体字段名变化(也可直接强转到具体类型)
|
||||||
|
String username;
|
||||||
|
try {
|
||||||
|
username = (String) authorObj.getClass().getMethod("getUsername").invoke(authorObj);
|
||||||
|
} catch (Exception e) {
|
||||||
|
username = null;
|
||||||
|
}
|
||||||
|
if (username == null || username.isEmpty()) return "匿名";
|
||||||
|
return username;
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return "匿名";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================== 时间/字符串/XML ===================== */
|
||||||
|
|
||||||
|
private static String toRfc1123Gmt(ZonedDateTime zdt) {
|
||||||
|
return zdt.withZoneSameInstant(ZoneId.of("GMT")).format(RFC1123);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String cdata(String s) {
|
||||||
|
if (s == null) return "<![CDATA[]]>";
|
||||||
|
// 防止出现 "]]>" 终止标记破坏 CDATA
|
||||||
|
return "<![CDATA[" + s.replace("]]>", "]]]]><![CDATA[>") + "]]>";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void elem(StringBuilder sb, String name, String value) {
|
||||||
|
sb.append('<').append(name).append('>').append(value).append("</").append(name).append('>');
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String escapeXml(String s) {
|
||||||
|
if (s == null) return "";
|
||||||
|
return s.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||||
|
.replace("\"", """).replace("'", "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String trimTrailingSlash(String s) {
|
||||||
|
if (s == null) return "";
|
||||||
|
return s.endsWith("/") ? s.substring(0, s.length() - 1) : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String ensureTrailingSlash(String s) {
|
||||||
|
if (s == null || s.isEmpty()) return "/";
|
||||||
|
return s.endsWith("/") ? s : s + "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String nullSafe(String s) { return s == null ? "" : s; }
|
||||||
|
}
|
||||||
+14
-47
@@ -1,10 +1,11 @@
|
|||||||
package com.openisle.controller;
|
package com.openisle.controller;
|
||||||
|
|
||||||
import com.openisle.model.Post;
|
import com.openisle.dto.PostSummaryDto;
|
||||||
import com.openisle.model.Comment;
|
import com.openisle.dto.SearchResultDto;
|
||||||
import com.openisle.model.User;
|
import com.openisle.dto.UserDto;
|
||||||
|
import com.openisle.mapper.PostMapper;
|
||||||
|
import com.openisle.mapper.UserMapper;
|
||||||
import com.openisle.service.SearchService;
|
import com.openisle.service.SearchService;
|
||||||
import lombok.Data;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
@@ -19,32 +20,34 @@ import java.util.stream.Collectors;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class SearchController {
|
public class SearchController {
|
||||||
private final SearchService searchService;
|
private final SearchService searchService;
|
||||||
|
private final UserMapper userMapper;
|
||||||
|
private final PostMapper postMapper;
|
||||||
|
|
||||||
@GetMapping("/users")
|
@GetMapping("/users")
|
||||||
public List<UserDto> searchUsers(@RequestParam String keyword) {
|
public List<UserDto> searchUsers(@RequestParam String keyword) {
|
||||||
return searchService.searchUsers(keyword).stream()
|
return searchService.searchUsers(keyword).stream()
|
||||||
.map(this::toUserDto)
|
.map(userMapper::toDto)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/posts")
|
@GetMapping("/posts")
|
||||||
public List<PostDto> searchPosts(@RequestParam String keyword) {
|
public List<PostSummaryDto> searchPosts(@RequestParam String keyword) {
|
||||||
return searchService.searchPosts(keyword).stream()
|
return searchService.searchPosts(keyword).stream()
|
||||||
.map(this::toPostDto)
|
.map(postMapper::toSummaryDto)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/posts/content")
|
@GetMapping("/posts/content")
|
||||||
public List<PostDto> searchPostsByContent(@RequestParam String keyword) {
|
public List<PostSummaryDto> searchPostsByContent(@RequestParam String keyword) {
|
||||||
return searchService.searchPostsByContent(keyword).stream()
|
return searchService.searchPostsByContent(keyword).stream()
|
||||||
.map(this::toPostDto)
|
.map(postMapper::toSummaryDto)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/posts/title")
|
@GetMapping("/posts/title")
|
||||||
public List<PostDto> searchPostsByTitle(@RequestParam String keyword) {
|
public List<PostSummaryDto> searchPostsByTitle(@RequestParam String keyword) {
|
||||||
return searchService.searchPostsByTitle(keyword).stream()
|
return searchService.searchPostsByTitle(keyword).stream()
|
||||||
.map(this::toPostDto)
|
.map(postMapper::toSummaryDto)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,40 +66,4 @@ public class SearchController {
|
|||||||
})
|
})
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
private UserDto toUserDto(User user) {
|
|
||||||
UserDto dto = new UserDto();
|
|
||||||
dto.setId(user.getId());
|
|
||||||
dto.setUsername(user.getUsername());
|
|
||||||
return dto;
|
|
||||||
}
|
|
||||||
|
|
||||||
private PostDto toPostDto(Post post) {
|
|
||||||
PostDto dto = new PostDto();
|
|
||||||
dto.setId(post.getId());
|
|
||||||
dto.setTitle(post.getTitle());
|
|
||||||
return dto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
private static class UserDto {
|
|
||||||
private Long id;
|
|
||||||
private String username;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
private static class PostDto {
|
|
||||||
private Long id;
|
|
||||||
private String title;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
private static class SearchResultDto {
|
|
||||||
private String type;
|
|
||||||
private Long id;
|
|
||||||
private String text;
|
|
||||||
private String subText;
|
|
||||||
private String extra;
|
|
||||||
private Long postId;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package com.openisle.controller;
|
||||||
|
|
||||||
|
import com.openisle.model.Post;
|
||||||
|
import com.openisle.model.PostStatus;
|
||||||
|
import com.openisle.repository.PostRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller for dynamic sitemap generation.
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RequestMapping("/api")
|
||||||
|
public class SitemapController {
|
||||||
|
private final PostRepository postRepository;
|
||||||
|
|
||||||
|
@Value("${app.website-url}")
|
||||||
|
private String websiteUrl;
|
||||||
|
|
||||||
|
@GetMapping(value = "/sitemap.xml", produces = MediaType.APPLICATION_XML_VALUE)
|
||||||
|
public ResponseEntity<String> sitemap() {
|
||||||
|
List<Post> posts = postRepository.findByStatus(PostStatus.PUBLISHED);
|
||||||
|
|
||||||
|
StringBuilder body = new StringBuilder();
|
||||||
|
body.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
|
||||||
|
body.append("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n");
|
||||||
|
|
||||||
|
List<String> staticRoutes = List.of(
|
||||||
|
"/",
|
||||||
|
"/about",
|
||||||
|
"/activities",
|
||||||
|
"/login",
|
||||||
|
"/signup"
|
||||||
|
);
|
||||||
|
|
||||||
|
for (String path : staticRoutes) {
|
||||||
|
body.append(" <url><loc>")
|
||||||
|
.append(websiteUrl)
|
||||||
|
.append(path)
|
||||||
|
.append("</loc></url>\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Post p : posts) {
|
||||||
|
body.append(" <url>\n")
|
||||||
|
.append(" <loc>")
|
||||||
|
.append(websiteUrl)
|
||||||
|
.append("/posts/")
|
||||||
|
.append(p.getId())
|
||||||
|
.append("</loc>\n")
|
||||||
|
.append(" <lastmod>")
|
||||||
|
.append(p.getCreatedAt().toLocalDate())
|
||||||
|
.append("</lastmod>\n")
|
||||||
|
.append(" </url>\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
body.append("</urlset>");
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.contentType(MediaType.APPLICATION_XML)
|
||||||
|
.body(body.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package com.openisle.controller;
|
||||||
|
|
||||||
|
import com.openisle.service.UserVisitService;
|
||||||
|
import com.openisle.service.StatService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
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;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/stats")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class StatController {
|
||||||
|
private final UserVisitService userVisitService;
|
||||||
|
private final StatService statService;
|
||||||
|
|
||||||
|
@GetMapping("/dau")
|
||||||
|
public Map<String, Long> dau(@RequestParam(value = "date", required = false)
|
||||||
|
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) {
|
||||||
|
long count = userVisitService.countDau(date);
|
||||||
|
return Map.of("dau", count);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/dau-range")
|
||||||
|
public List<Map<String, Object>> dauRange(@RequestParam(value = "days", defaultValue = "30") int days) {
|
||||||
|
if (days < 1) days = 1;
|
||||||
|
LocalDate end = LocalDate.now();
|
||||||
|
LocalDate start = end.minusDays(days - 1L);
|
||||||
|
var data = userVisitService.countDauRange(start, end);
|
||||||
|
return data.entrySet().stream()
|
||||||
|
.map(e -> Map.<String,Object>of(
|
||||||
|
"date", e.getKey().toString(),
|
||||||
|
"value", e.getValue()
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/new-users-range")
|
||||||
|
public List<Map<String, Object>> newUsersRange(@RequestParam(value = "days", defaultValue = "30") int days) {
|
||||||
|
if (days < 1) days = 1;
|
||||||
|
LocalDate end = LocalDate.now();
|
||||||
|
LocalDate start = end.minusDays(days - 1L);
|
||||||
|
var data = statService.countNewUsersRange(start, end);
|
||||||
|
return data.entrySet().stream()
|
||||||
|
.map(e -> Map.<String,Object>of(
|
||||||
|
"date", e.getKey().toString(),
|
||||||
|
"value", e.getValue()
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/posts-range")
|
||||||
|
public List<Map<String, Object>> postsRange(@RequestParam(value = "days", defaultValue = "30") int days) {
|
||||||
|
if (days < 1) days = 1;
|
||||||
|
LocalDate end = LocalDate.now();
|
||||||
|
LocalDate start = end.minusDays(days - 1L);
|
||||||
|
var data = statService.countPostsRange(start, end);
|
||||||
|
return data.entrySet().stream()
|
||||||
|
.map(e -> Map.<String,Object>of(
|
||||||
|
"date", e.getKey().toString(),
|
||||||
|
"value", e.getValue()
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/comments-range")
|
||||||
|
public List<Map<String, Object>> commentsRange(@RequestParam(value = "days", defaultValue = "30") int days) {
|
||||||
|
if (days < 1) days = 1;
|
||||||
|
LocalDate end = LocalDate.now();
|
||||||
|
LocalDate start = end.minusDays(days - 1L);
|
||||||
|
var data = statService.countCommentsRange(start, end);
|
||||||
|
return data.entrySet().stream()
|
||||||
|
.map(e -> Map.<String,Object>of(
|
||||||
|
"date", e.getKey().toString(),
|
||||||
|
"value", e.getValue()
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
-51
@@ -1,16 +1,21 @@
|
|||||||
package com.openisle.controller;
|
package com.openisle.controller;
|
||||||
|
|
||||||
import com.openisle.model.Tag;
|
import com.openisle.dto.PostSummaryDto;
|
||||||
import com.openisle.service.TagService;
|
import com.openisle.dto.TagDto;
|
||||||
import com.openisle.service.PostService;
|
import com.openisle.dto.TagRequest;
|
||||||
import com.openisle.repository.UserRepository;
|
import com.openisle.mapper.PostMapper;
|
||||||
|
import com.openisle.mapper.TagMapper;
|
||||||
import com.openisle.model.PublishMode;
|
import com.openisle.model.PublishMode;
|
||||||
import com.openisle.model.Role;
|
import com.openisle.model.Role;
|
||||||
import lombok.Data;
|
import com.openisle.model.Tag;
|
||||||
|
import com.openisle.repository.UserRepository;
|
||||||
|
import com.openisle.service.PostService;
|
||||||
|
import com.openisle.service.TagService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -20,6 +25,8 @@ public class TagController {
|
|||||||
private final TagService tagService;
|
private final TagService tagService;
|
||||||
private final PostService postService;
|
private final PostService postService;
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
|
private final PostMapper postMapper;
|
||||||
|
private final TagMapper tagMapper;
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public TagDto create(@RequestBody TagRequest req, org.springframework.security.core.Authentication auth) {
|
public TagDto create(@RequestBody TagRequest req, org.springframework.security.core.Authentication auth) {
|
||||||
@@ -38,14 +45,14 @@ public class TagController {
|
|||||||
approved,
|
approved,
|
||||||
auth != null ? auth.getName() : null);
|
auth != null ? auth.getName() : null);
|
||||||
long count = postService.countPostsByTag(tag.getId());
|
long count = postService.countPostsByTag(tag.getId());
|
||||||
return toDto(tag, count);
|
return tagMapper.toDto(tag, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/{id}")
|
@PutMapping("/{id}")
|
||||||
public TagDto update(@PathVariable Long id, @RequestBody TagRequest req) {
|
public TagDto update(@PathVariable Long id, @RequestBody TagRequest req) {
|
||||||
Tag tag = tagService.updateTag(id, req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon());
|
Tag tag = tagService.updateTag(id, req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon());
|
||||||
long count = postService.countPostsByTag(tag.getId());
|
long count = postService.countPostsByTag(tag.getId());
|
||||||
return toDto(tag, count);
|
return tagMapper.toDto(tag, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{id}")
|
@DeleteMapping("/{id}")
|
||||||
@@ -56,8 +63,11 @@ public class TagController {
|
|||||||
@GetMapping
|
@GetMapping
|
||||||
public List<TagDto> list(@RequestParam(value = "keyword", required = false) String keyword,
|
public List<TagDto> list(@RequestParam(value = "keyword", required = false) String keyword,
|
||||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
@RequestParam(value = "limit", required = false) Integer limit) {
|
||||||
List<TagDto> dtos = tagService.searchTags(keyword).stream()
|
List<Tag> tags = tagService.searchTags(keyword);
|
||||||
.map(t -> toDto(t, postService.countPostsByTag(t.getId())))
|
List<Long> tagIds = tags.stream().map(Tag::getId).toList();
|
||||||
|
Map<Long, Long> postCntByTagIds = postService.countPostsByTagIds(tagIds);
|
||||||
|
List<TagDto> dtos = tags.stream()
|
||||||
|
.map(t -> tagMapper.toDto(t, postCntByTagIds.getOrDefault(t.getId(), 0L)))
|
||||||
.sorted((a, b) -> Long.compare(b.getCount(), a.getCount()))
|
.sorted((a, b) -> Long.compare(b.getCount(), a.getCount()))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
if (limit != null && limit > 0 && dtos.size() > limit) {
|
if (limit != null && limit > 0 && dtos.size() > limit) {
|
||||||
@@ -70,7 +80,7 @@ public class TagController {
|
|||||||
public TagDto get(@PathVariable Long id) {
|
public TagDto get(@PathVariable Long id) {
|
||||||
Tag tag = tagService.getTag(id);
|
Tag tag = tagService.getTag(id);
|
||||||
long count = postService.countPostsByTag(tag.getId());
|
long count = postService.countPostsByTag(tag.getId());
|
||||||
return toDto(tag, count);
|
return tagMapper.toDto(tag, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}/posts")
|
@GetMapping("/{id}/posts")
|
||||||
@@ -79,47 +89,7 @@ public class TagController {
|
|||||||
@RequestParam(value = "pageSize", required = false) Integer pageSize) {
|
@RequestParam(value = "pageSize", required = false) Integer pageSize) {
|
||||||
return postService.listPostsByTags(java.util.List.of(id), page, pageSize)
|
return postService.listPostsByTags(java.util.List.of(id), page, pageSize)
|
||||||
.stream()
|
.stream()
|
||||||
.map(p -> {
|
.map(postMapper::toSummaryDto)
|
||||||
PostSummaryDto dto = new PostSummaryDto();
|
|
||||||
dto.setId(p.getId());
|
|
||||||
dto.setTitle(p.getTitle());
|
|
||||||
return dto;
|
|
||||||
})
|
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
private TagDto toDto(Tag tag, long count) {
|
|
||||||
TagDto dto = new TagDto();
|
|
||||||
dto.setId(tag.getId());
|
|
||||||
dto.setName(tag.getName());
|
|
||||||
dto.setIcon(tag.getIcon());
|
|
||||||
dto.setSmallIcon(tag.getSmallIcon());
|
|
||||||
dto.setDescription(tag.getDescription());
|
|
||||||
dto.setCount(count);
|
|
||||||
return dto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
private static class TagRequest {
|
|
||||||
private String name;
|
|
||||||
private String description;
|
|
||||||
private String icon;
|
|
||||||
private String smallIcon;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
private static class TagDto {
|
|
||||||
private Long id;
|
|
||||||
private String name;
|
|
||||||
private String description;
|
|
||||||
private String icon;
|
|
||||||
private String smallIcon;
|
|
||||||
private Long count;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
private static class PostSummaryDto {
|
|
||||||
private Long id;
|
|
||||||
private String title;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+5
@@ -74,4 +74,9 @@ public class UploadController {
|
|||||||
return ResponseEntity.internalServerError().body(Map.of("code", 3, "msg", "Upload failed"));
|
return ResponseEntity.internalServerError().body(Map.of("code", 3, "msg", "Upload failed"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/presign")
|
||||||
|
public java.util.Map<String, String> presign(@RequestParam("filename") String filename) {
|
||||||
|
return imageUploader.presignUpload(filename);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+44
-181
@@ -1,11 +1,13 @@
|
|||||||
package com.openisle.controller;
|
package com.openisle.controller;
|
||||||
|
|
||||||
|
import com.openisle.dto.*;
|
||||||
import com.openisle.exception.NotFoundException;
|
import com.openisle.exception.NotFoundException;
|
||||||
|
import com.openisle.mapper.TagMapper;
|
||||||
|
import com.openisle.mapper.UserMapper;
|
||||||
import com.openisle.model.User;
|
import com.openisle.model.User;
|
||||||
import com.openisle.service.*;
|
import com.openisle.service.*;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
@@ -25,8 +27,10 @@ public class UserController {
|
|||||||
private final ReactionService reactionService;
|
private final ReactionService reactionService;
|
||||||
private final TagService tagService;
|
private final TagService tagService;
|
||||||
private final SubscriptionService subscriptionService;
|
private final SubscriptionService subscriptionService;
|
||||||
private final PostReadService postReadService;
|
private final LevelService levelService;
|
||||||
private final UserVisitService userVisitService;
|
private final JwtService jwtService;
|
||||||
|
private final UserMapper userMapper;
|
||||||
|
private final TagMapper tagMapper;
|
||||||
|
|
||||||
@Value("${app.upload.check-type:true}")
|
@Value("${app.upload.check-type:true}")
|
||||||
private boolean checkImageType;
|
private boolean checkImageType;
|
||||||
@@ -43,13 +47,10 @@ public class UserController {
|
|||||||
@Value("${app.user.tags-limit:50}")
|
@Value("${app.user.tags-limit:50}")
|
||||||
private int defaultTagsLimit;
|
private int defaultTagsLimit;
|
||||||
|
|
||||||
@Value("${app.snippet-length:50}")
|
|
||||||
private int snippetLength;
|
|
||||||
|
|
||||||
@GetMapping("/me")
|
@GetMapping("/me")
|
||||||
public ResponseEntity<UserDto> me(Authentication auth) {
|
public ResponseEntity<UserDto> me(Authentication auth) {
|
||||||
User user = userService.findByUsername(auth.getName()).orElseThrow();
|
User user = userService.findByUsername(auth.getName()).orElseThrow();
|
||||||
return ResponseEntity.ok(toDto(user, auth));
|
return ResponseEntity.ok(userMapper.toDto(user, auth));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/me/avatar")
|
@PostMapping("/me/avatar")
|
||||||
@@ -72,17 +73,26 @@ public class UserController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/me")
|
@PutMapping("/me")
|
||||||
public ResponseEntity<UserDto> updateProfile(@RequestBody UpdateProfileDto dto,
|
public ResponseEntity<?> updateProfile(@RequestBody UpdateProfileDto dto,
|
||||||
Authentication auth) {
|
Authentication auth) {
|
||||||
User user = userService.updateProfile(auth.getName(), dto.getUsername(), dto.getIntroduction());
|
User user = userService.updateProfile(auth.getName(), dto.getUsername(), dto.getIntroduction());
|
||||||
return ResponseEntity.ok(toDto(user, auth));
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"token", jwtService.generateToken(user.getUsername()),
|
||||||
|
"user", userMapper.toDto(user, auth)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/me/signin")
|
||||||
|
public Map<String, Integer> signIn(Authentication auth) {
|
||||||
|
int reward = levelService.awardForSignin(auth.getName());
|
||||||
|
return Map.of("reward", reward);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{identifier}")
|
@GetMapping("/{identifier}")
|
||||||
public ResponseEntity<UserDto> getUser(@PathVariable("identifier") String identifier,
|
public ResponseEntity<UserDto> getUser(@PathVariable("identifier") String identifier,
|
||||||
Authentication auth) {
|
Authentication auth) {
|
||||||
User user = userService.findByIdentifier(identifier).orElseThrow(() -> new NotFoundException("User not found"));
|
User user = userService.findByIdentifier(identifier).orElseThrow(() -> new NotFoundException("User not found"));
|
||||||
return ResponseEntity.ok(toDto(user, auth));
|
return ResponseEntity.ok(userMapper.toDto(user, auth));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{identifier}/posts")
|
@GetMapping("/{identifier}/posts")
|
||||||
@@ -91,7 +101,7 @@ public class UserController {
|
|||||||
int l = limit != null ? limit : defaultPostsLimit;
|
int l = limit != null ? limit : defaultPostsLimit;
|
||||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||||
return postService.getRecentPostsByUser(user.getUsername(), l).stream()
|
return postService.getRecentPostsByUser(user.getUsername(), l).stream()
|
||||||
.map(this::toMetaDto)
|
.map(userMapper::toMetaDto)
|
||||||
.collect(java.util.stream.Collectors.toList());
|
.collect(java.util.stream.Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +111,7 @@ public class UserController {
|
|||||||
int l = limit != null ? limit : defaultRepliesLimit;
|
int l = limit != null ? limit : defaultRepliesLimit;
|
||||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||||
return commentService.getRecentCommentsByUser(user.getUsername(), l).stream()
|
return commentService.getRecentCommentsByUser(user.getUsername(), l).stream()
|
||||||
.map(this::toCommentInfoDto)
|
.map(userMapper::toCommentInfoDto)
|
||||||
.collect(java.util.stream.Collectors.toList());
|
.collect(java.util.stream.Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +122,7 @@ public class UserController {
|
|||||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||||
java.util.List<Long> ids = reactionService.topPostIds(user.getUsername(), l);
|
java.util.List<Long> ids = reactionService.topPostIds(user.getUsername(), l);
|
||||||
return postService.getPostsByIds(ids).stream()
|
return postService.getPostsByIds(ids).stream()
|
||||||
.map(this::toMetaDto)
|
.map(userMapper::toMetaDto)
|
||||||
.collect(java.util.stream.Collectors.toList());
|
.collect(java.util.stream.Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,49 +133,29 @@ public class UserController {
|
|||||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||||
java.util.List<Long> ids = reactionService.topCommentIds(user.getUsername(), l);
|
java.util.List<Long> ids = reactionService.topCommentIds(user.getUsername(), l);
|
||||||
return commentService.getCommentsByIds(ids).stream()
|
return commentService.getCommentsByIds(ids).stream()
|
||||||
.map(this::toCommentInfoDto)
|
.map(userMapper::toCommentInfoDto)
|
||||||
.collect(java.util.stream.Collectors.toList());
|
.collect(java.util.stream.Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{identifier}/hot-tags")
|
@GetMapping("/{identifier}/hot-tags")
|
||||||
public java.util.List<TagInfoDto> hotTags(@PathVariable("identifier") String identifier,
|
public java.util.List<TagDto> hotTags(@PathVariable("identifier") String identifier,
|
||||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
@RequestParam(value = "limit", required = false) Integer limit) {
|
||||||
int l = limit != null ? limit : 10;
|
int l = limit != null ? limit : 10;
|
||||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||||
return tagService.getTagsByUser(user.getUsername()).stream()
|
return tagService.getTagsByUser(user.getUsername()).stream()
|
||||||
.map(t -> {
|
.map(t -> tagMapper.toDto(t, postService.countPostsByTag(t.getId())))
|
||||||
TagInfoDto dto = new TagInfoDto();
|
|
||||||
dto.setId(t.getId());
|
|
||||||
dto.setName(t.getName());
|
|
||||||
dto.setDescription(t.getDescription());
|
|
||||||
dto.setIcon(t.getIcon());
|
|
||||||
dto.setSmallIcon(t.getSmallIcon());
|
|
||||||
dto.setCreatedAt(t.getCreatedAt());
|
|
||||||
dto.setCount(postService.countPostsByTag(t.getId()));
|
|
||||||
return dto;
|
|
||||||
})
|
|
||||||
.sorted((a, b) -> Long.compare(b.getCount(), a.getCount()))
|
.sorted((a, b) -> Long.compare(b.getCount(), a.getCount()))
|
||||||
.limit(l)
|
.limit(l)
|
||||||
.collect(java.util.stream.Collectors.toList());
|
.collect(java.util.stream.Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{identifier}/tags")
|
@GetMapping("/{identifier}/tags")
|
||||||
public java.util.List<TagInfoDto> userTags(@PathVariable("identifier") String identifier,
|
public java.util.List<TagDto> userTags(@PathVariable("identifier") String identifier,
|
||||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
@RequestParam(value = "limit", required = false) Integer limit) {
|
||||||
int l = limit != null ? limit : defaultTagsLimit;
|
int l = limit != null ? limit : defaultTagsLimit;
|
||||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||||
return tagService.getRecentTagsByUser(user.getUsername(), l).stream()
|
return tagService.getRecentTagsByUser(user.getUsername(), l).stream()
|
||||||
.map(t -> {
|
.map(t -> tagMapper.toDto(t, postService.countPostsByTag(t.getId())))
|
||||||
TagInfoDto dto = new TagInfoDto();
|
|
||||||
dto.setId(t.getId());
|
|
||||||
dto.setName(t.getName());
|
|
||||||
dto.setDescription(t.getDescription());
|
|
||||||
dto.setIcon(t.getIcon());
|
|
||||||
dto.setSmallIcon(t.getSmallIcon());
|
|
||||||
dto.setCreatedAt(t.getCreatedAt());
|
|
||||||
dto.setCount(postService.countPostsByTag(t.getId()));
|
|
||||||
return dto;
|
|
||||||
})
|
|
||||||
.collect(java.util.stream.Collectors.toList());
|
.collect(java.util.stream.Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +163,7 @@ public class UserController {
|
|||||||
public java.util.List<UserDto> following(@PathVariable("identifier") String identifier) {
|
public java.util.List<UserDto> following(@PathVariable("identifier") String identifier) {
|
||||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||||
return subscriptionService.getSubscribedUsers(user.getUsername()).stream()
|
return subscriptionService.getSubscribedUsers(user.getUsername()).stream()
|
||||||
.map(this::toDto)
|
.map(userMapper::toDto)
|
||||||
.collect(java.util.stream.Collectors.toList());
|
.collect(java.util.stream.Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +171,14 @@ public class UserController {
|
|||||||
public java.util.List<UserDto> followers(@PathVariable("identifier") String identifier) {
|
public java.util.List<UserDto> followers(@PathVariable("identifier") String identifier) {
|
||||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||||
return subscriptionService.getSubscribers(user.getUsername()).stream()
|
return subscriptionService.getSubscribers(user.getUsername()).stream()
|
||||||
.map(this::toDto)
|
.map(userMapper::toDto)
|
||||||
|
.collect(java.util.stream.Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/admins")
|
||||||
|
public java.util.List<UserDto> admins() {
|
||||||
|
return userService.getAdmins().stream()
|
||||||
|
.map(userMapper::toDto)
|
||||||
.collect(java.util.stream.Collectors.toList());
|
.collect(java.util.stream.Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,149 +191,15 @@ public class UserController {
|
|||||||
int pLimit = postsLimit != null ? postsLimit : defaultPostsLimit;
|
int pLimit = postsLimit != null ? postsLimit : defaultPostsLimit;
|
||||||
int rLimit = repliesLimit != null ? repliesLimit : defaultRepliesLimit;
|
int rLimit = repliesLimit != null ? repliesLimit : defaultRepliesLimit;
|
||||||
java.util.List<PostMetaDto> posts = postService.getRecentPostsByUser(user.getUsername(), pLimit).stream()
|
java.util.List<PostMetaDto> posts = postService.getRecentPostsByUser(user.getUsername(), pLimit).stream()
|
||||||
.map(this::toMetaDto)
|
.map(userMapper::toMetaDto)
|
||||||
.collect(java.util.stream.Collectors.toList());
|
.collect(java.util.stream.Collectors.toList());
|
||||||
java.util.List<CommentInfoDto> replies = commentService.getRecentCommentsByUser(user.getUsername(), rLimit).stream()
|
java.util.List<CommentInfoDto> replies = commentService.getRecentCommentsByUser(user.getUsername(), rLimit).stream()
|
||||||
.map(this::toCommentInfoDto)
|
.map(userMapper::toCommentInfoDto)
|
||||||
.collect(java.util.stream.Collectors.toList());
|
.collect(java.util.stream.Collectors.toList());
|
||||||
UserAggregateDto dto = new UserAggregateDto();
|
UserAggregateDto dto = new UserAggregateDto();
|
||||||
dto.setUser(toDto(user, auth));
|
dto.setUser(userMapper.toDto(user, auth));
|
||||||
dto.setPosts(posts);
|
dto.setPosts(posts);
|
||||||
dto.setReplies(replies);
|
dto.setReplies(replies);
|
||||||
return ResponseEntity.ok(dto);
|
return ResponseEntity.ok(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
private UserDto toDto(User user, Authentication viewer) {
|
|
||||||
UserDto dto = new UserDto();
|
|
||||||
dto.setId(user.getId());
|
|
||||||
dto.setUsername(user.getUsername());
|
|
||||||
dto.setEmail(user.getEmail());
|
|
||||||
dto.setAvatar(user.getAvatar());
|
|
||||||
dto.setRole(user.getRole().name());
|
|
||||||
dto.setIntroduction(user.getIntroduction());
|
|
||||||
dto.setFollowers(subscriptionService.countSubscribers(user.getUsername()));
|
|
||||||
dto.setFollowing(subscriptionService.countSubscribed(user.getUsername()));
|
|
||||||
dto.setCreatedAt(user.getCreatedAt());
|
|
||||||
dto.setLastPostTime(postService.getLastPostTime(user.getUsername()));
|
|
||||||
dto.setTotalViews(postService.getTotalViews(user.getUsername()));
|
|
||||||
dto.setVisitedDays(userVisitService.countVisits(user.getUsername()));
|
|
||||||
dto.setReadPosts(postReadService.countReads(user.getUsername()));
|
|
||||||
dto.setLikesSent(reactionService.countLikesSent(user.getUsername()));
|
|
||||||
dto.setLikesReceived(reactionService.countLikesReceived(user.getUsername()));
|
|
||||||
if (viewer != null) {
|
|
||||||
dto.setSubscribed(subscriptionService.isSubscribed(viewer.getName(), user.getUsername()));
|
|
||||||
} else {
|
|
||||||
dto.setSubscribed(false);
|
|
||||||
}
|
|
||||||
return dto;
|
|
||||||
}
|
|
||||||
|
|
||||||
private UserDto toDto(User user) {
|
|
||||||
return toDto(user, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private PostMetaDto toMetaDto(com.openisle.model.Post post) {
|
|
||||||
PostMetaDto dto = new PostMetaDto();
|
|
||||||
dto.setId(post.getId());
|
|
||||||
dto.setTitle(post.getTitle());
|
|
||||||
String content = post.getContent();
|
|
||||||
if (content == null) {
|
|
||||||
content = "";
|
|
||||||
}
|
|
||||||
if (snippetLength >= 0) {
|
|
||||||
dto.setSnippet(content.length() > snippetLength ? content.substring(0, snippetLength) : content);
|
|
||||||
} else {
|
|
||||||
dto.setSnippet(content);
|
|
||||||
}
|
|
||||||
dto.setCreatedAt(post.getCreatedAt());
|
|
||||||
dto.setCategory(post.getCategory().getName());
|
|
||||||
dto.setViews(post.getViews());
|
|
||||||
return dto;
|
|
||||||
}
|
|
||||||
|
|
||||||
private CommentInfoDto toCommentInfoDto(com.openisle.model.Comment comment) {
|
|
||||||
CommentInfoDto dto = new CommentInfoDto();
|
|
||||||
dto.setId(comment.getId());
|
|
||||||
dto.setContent(comment.getContent());
|
|
||||||
dto.setCreatedAt(comment.getCreatedAt());
|
|
||||||
dto.setPost(toMetaDto(comment.getPost()));
|
|
||||||
if (comment.getParent() != null) {
|
|
||||||
ParentCommentDto pc = new ParentCommentDto();
|
|
||||||
pc.setId(comment.getParent().getId());
|
|
||||||
pc.setAuthor(comment.getParent().getAuthor().getUsername());
|
|
||||||
pc.setContent(comment.getParent().getContent());
|
|
||||||
dto.setParentComment(pc);
|
|
||||||
}
|
|
||||||
return dto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
private static class UserDto {
|
|
||||||
private Long id;
|
|
||||||
private String username;
|
|
||||||
private String email;
|
|
||||||
private String avatar;
|
|
||||||
private String role;
|
|
||||||
private String introduction;
|
|
||||||
private long followers;
|
|
||||||
private long following;
|
|
||||||
private java.time.LocalDateTime createdAt;
|
|
||||||
private java.time.LocalDateTime lastPostTime;
|
|
||||||
private long totalViews;
|
|
||||||
private long visitedDays;
|
|
||||||
private long readPosts;
|
|
||||||
private long likesSent;
|
|
||||||
private long likesReceived;
|
|
||||||
private boolean subscribed;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
private static class PostMetaDto {
|
|
||||||
private Long id;
|
|
||||||
private String title;
|
|
||||||
private String snippet;
|
|
||||||
private java.time.LocalDateTime createdAt;
|
|
||||||
private String category;
|
|
||||||
private long views;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
private static class CommentInfoDto {
|
|
||||||
private Long id;
|
|
||||||
private String content;
|
|
||||||
private java.time.LocalDateTime createdAt;
|
|
||||||
private PostMetaDto post;
|
|
||||||
private ParentCommentDto parentComment;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
private static class TagInfoDto {
|
|
||||||
private Long id;
|
|
||||||
private String name;
|
|
||||||
private String description;
|
|
||||||
private String icon;
|
|
||||||
private String smallIcon;
|
|
||||||
private java.time.LocalDateTime createdAt;
|
|
||||||
private Long count;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
private static class ParentCommentDto {
|
|
||||||
private Long id;
|
|
||||||
private String author;
|
|
||||||
private String content;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
private static class UpdateProfileDto {
|
|
||||||
private String username;
|
|
||||||
private String introduction;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
private static class UserAggregateDto {
|
|
||||||
private UserDto user;
|
|
||||||
private java.util.List<PostMetaDto> posts;
|
|
||||||
private java.util.List<CommentInfoDto> replies;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import com.openisle.model.ActivityType;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO representing an activity without participant details.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ActivityDto {
|
||||||
|
private Long id;
|
||||||
|
private String title;
|
||||||
|
private String icon;
|
||||||
|
private String content;
|
||||||
|
private LocalDateTime startTime;
|
||||||
|
private LocalDateTime endTime;
|
||||||
|
private ActivityType type;
|
||||||
|
private boolean ended;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import com.openisle.model.MedalType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO representing a post or comment author.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AuthorDto {
|
||||||
|
private Long id;
|
||||||
|
private String username;
|
||||||
|
private String avatar;
|
||||||
|
private MedalType displayMedal;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO representing a post category.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class CategoryDto {
|
||||||
|
private Long id;
|
||||||
|
private String name;
|
||||||
|
private String description;
|
||||||
|
private String icon;
|
||||||
|
private String smallIcon;
|
||||||
|
private Long count;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/** Request body for creating or updating a category. */
|
||||||
|
@Data
|
||||||
|
public class CategoryRequest {
|
||||||
|
private String name;
|
||||||
|
private String description;
|
||||||
|
private String icon;
|
||||||
|
private String smallIcon;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class ChannelDto {
|
||||||
|
private Long id;
|
||||||
|
private String name;
|
||||||
|
private String description;
|
||||||
|
private String avatar;
|
||||||
|
private MessageDto lastMessage;
|
||||||
|
private long memberCount;
|
||||||
|
private boolean joined;
|
||||||
|
private long unreadCount;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO representing a comment and its nested replies.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class CommentDto {
|
||||||
|
private Long id;
|
||||||
|
private String content;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime pinnedAt;
|
||||||
|
private AuthorDto author;
|
||||||
|
private List<CommentDto> replies;
|
||||||
|
private List<ReactionDto> reactions;
|
||||||
|
private int reward;
|
||||||
|
private int pointReward;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/** DTO for comment information in user profiles. */
|
||||||
|
@Data
|
||||||
|
public class CommentInfoDto {
|
||||||
|
private Long id;
|
||||||
|
private String content;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private PostMetaDto post;
|
||||||
|
private ParentCommentDto parentComment;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class CommentMedalDto extends MedalDto {
|
||||||
|
private long currentCommentCount;
|
||||||
|
private long targetCommentCount;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/** Request body for creating or replying to a comment. */
|
||||||
|
@Data
|
||||||
|
public class CommentRequest {
|
||||||
|
private String content;
|
||||||
|
private String captcha;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import com.openisle.model.PasswordStrength;
|
||||||
|
import com.openisle.model.PublishMode;
|
||||||
|
import com.openisle.model.RegisterMode;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/** DTO for site configuration. */
|
||||||
|
@Data
|
||||||
|
public class ConfigDto {
|
||||||
|
private PublishMode publishMode;
|
||||||
|
private PasswordStrength passwordStrength;
|
||||||
|
private Integer aiFormatLimit;
|
||||||
|
private RegisterMode registerMode;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class ContributorMedalDto extends MedalDto {
|
||||||
|
private long currentContributionLines;
|
||||||
|
private long targetContributionLines;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class ConversationDetailDto {
|
||||||
|
private Long id;
|
||||||
|
private String name;
|
||||||
|
private boolean channel;
|
||||||
|
private String avatar;
|
||||||
|
private List<UserSummaryDto> participants;
|
||||||
|
private Page<MessageDto> messages;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class ConversationDto {
|
||||||
|
private Long id;
|
||||||
|
private String name;
|
||||||
|
private boolean channel;
|
||||||
|
private String avatar;
|
||||||
|
private MessageDto lastMessage;
|
||||||
|
private List<UserSummaryDto> participants;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private long unreadCount;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class CreateConversationRequest {
|
||||||
|
private Long recipientId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class CreateConversationResponse {
|
||||||
|
private Long conversationId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/** Request for Discord OAuth login. */
|
||||||
|
@Data
|
||||||
|
public class DiscordLoginRequest {
|
||||||
|
private String code;
|
||||||
|
private String redirectUri;
|
||||||
|
private String inviteToken;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** DTO representing a saved draft. */
|
||||||
|
@Data
|
||||||
|
public class DraftDto {
|
||||||
|
private Long id;
|
||||||
|
private String title;
|
||||||
|
private String content;
|
||||||
|
private Long categoryId;
|
||||||
|
private List<Long> tagIds;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** Request body for saving a draft. */
|
||||||
|
@Data
|
||||||
|
public class DraftRequest {
|
||||||
|
private String title;
|
||||||
|
private String content;
|
||||||
|
private Long categoryId;
|
||||||
|
private List<Long> tagIds;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class FeaturedMedalDto extends MedalDto {
|
||||||
|
private long currentFeaturedCount;
|
||||||
|
private long targetFeaturedCount;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/** Request to trigger a forgot password email. */
|
||||||
|
@Data
|
||||||
|
public class ForgotPasswordRequest {
|
||||||
|
private String email;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/** Request for GitHub OAuth login. */
|
||||||
|
@Data
|
||||||
|
public class GithubLoginRequest {
|
||||||
|
private String code;
|
||||||
|
private String redirectUri;
|
||||||
|
private String inviteToken;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/** Request for Google OAuth login. */
|
||||||
|
@Data
|
||||||
|
public class GoogleLoginRequest {
|
||||||
|
private String idToken;
|
||||||
|
private String inviteToken;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/** Request to login. */
|
||||||
|
@Data
|
||||||
|
public class LoginRequest {
|
||||||
|
private String username;
|
||||||
|
private String password;
|
||||||
|
private String captcha;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** Metadata for lottery posts. */
|
||||||
|
@Data
|
||||||
|
public class LotteryDto {
|
||||||
|
private String prizeDescription;
|
||||||
|
private String prizeIcon;
|
||||||
|
private int prizeCount;
|
||||||
|
private LocalDateTime startTime;
|
||||||
|
private LocalDateTime endTime;
|
||||||
|
private List<AuthorDto> participants;
|
||||||
|
private List<AuthorDto> winners;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/** Request to submit a reason (e.g., for moderation). */
|
||||||
|
@Data
|
||||||
|
public class MakeReasonRequest {
|
||||||
|
private String token;
|
||||||
|
private String reason;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import com.openisle.model.MedalType;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class MedalDto {
|
||||||
|
private String icon;
|
||||||
|
private String title;
|
||||||
|
private String description;
|
||||||
|
private MedalType type;
|
||||||
|
private boolean completed;
|
||||||
|
private boolean selected;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import com.openisle.model.MedalType;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class MedalSelectRequest {
|
||||||
|
private MedalType type;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class MessageDto {
|
||||||
|
private Long id;
|
||||||
|
private String content;
|
||||||
|
private UserSummaryDto sender;
|
||||||
|
private Long conversationId;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private MessageDto replyTo;
|
||||||
|
private List<ReactionDto> reactions;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/** Info about the milk tea activity. */
|
||||||
|
@Data
|
||||||
|
public class MilkTeaInfoDto {
|
||||||
|
private long redeemCount;
|
||||||
|
private boolean ended;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/** Request to redeem the milk tea activity. */
|
||||||
|
@Data
|
||||||
|
public class MilkTeaRedeemRequest {
|
||||||
|
private String contact;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import com.openisle.model.NotificationType;
|
||||||
|
import com.openisle.model.ReactionType;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/** DTO representing a user notification. */
|
||||||
|
@Data
|
||||||
|
public class NotificationDto {
|
||||||
|
private Long id;
|
||||||
|
private NotificationType type;
|
||||||
|
private PostSummaryDto post;
|
||||||
|
private CommentDto comment;
|
||||||
|
private CommentDto parentComment;
|
||||||
|
private AuthorDto fromUser;
|
||||||
|
private ReactionType reactionType;
|
||||||
|
private String content;
|
||||||
|
private Boolean approved;
|
||||||
|
private boolean read;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** Request to mark notifications as read. */
|
||||||
|
@Data
|
||||||
|
public class NotificationMarkReadRequest {
|
||||||
|
private List<Long> ids;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import com.openisle.model.NotificationType;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/** User notification preference DTO. */
|
||||||
|
@Data
|
||||||
|
public class NotificationPreferenceDto {
|
||||||
|
private NotificationType type;
|
||||||
|
private boolean enabled;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import com.openisle.model.NotificationType;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/** Request to update a single notification preference. */
|
||||||
|
@Data
|
||||||
|
public class NotificationPreferenceUpdateRequest {
|
||||||
|
private NotificationType type;
|
||||||
|
private boolean enabled;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/** DTO representing unread notification count. */
|
||||||
|
@Data
|
||||||
|
public class NotificationUnreadCountDto {
|
||||||
|
private long count;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/** DTO representing a parent comment. */
|
||||||
|
@Data
|
||||||
|
public class ParentCommentDto {
|
||||||
|
private Long id;
|
||||||
|
private String author;
|
||||||
|
private String content;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class PioneerMedalDto extends MedalDto {
|
||||||
|
private long rank;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/** Point mall good info. */
|
||||||
|
@Data
|
||||||
|
public class PointGoodDto {
|
||||||
|
private Long id;
|
||||||
|
private String name;
|
||||||
|
private int cost;
|
||||||
|
private String image;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import com.openisle.model.PointHistoryType;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class PointHistoryDto {
|
||||||
|
private Long id;
|
||||||
|
private PointHistoryType type;
|
||||||
|
private int amount;
|
||||||
|
private int balance;
|
||||||
|
private Long postId;
|
||||||
|
private String postTitle;
|
||||||
|
private Long commentId;
|
||||||
|
private String commentContent;
|
||||||
|
private Long fromUserId;
|
||||||
|
private String fromUserName;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/** Request to redeem a point mall good. */
|
||||||
|
@Data
|
||||||
|
public class PointRedeemRequest {
|
||||||
|
private Long goodId;
|
||||||
|
private String contact;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detailed DTO for a post, including comments.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class PostDetailDto extends PostSummaryDto {
|
||||||
|
private List<CommentDto> comments;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class PostMedalDto extends MedalDto {
|
||||||
|
private long currentPostCount;
|
||||||
|
private long targetPostCount;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/** Lightweight post metadata used in user profile lists. */
|
||||||
|
@Data
|
||||||
|
public class PostMetaDto {
|
||||||
|
private Long id;
|
||||||
|
private String title;
|
||||||
|
private String snippet;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private String category;
|
||||||
|
private long views;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.openisle.model.PostType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request body for creating or updating a post.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class PostRequest {
|
||||||
|
private Long categoryId;
|
||||||
|
private String title;
|
||||||
|
private String content;
|
||||||
|
private List<Long> tagIds;
|
||||||
|
private String captcha;
|
||||||
|
|
||||||
|
// optional for lottery posts
|
||||||
|
private PostType type;
|
||||||
|
private String prizeDescription;
|
||||||
|
private String prizeIcon;
|
||||||
|
private Integer prizeCount;
|
||||||
|
private LocalDateTime startTime;
|
||||||
|
private LocalDateTime endTime;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import com.openisle.model.PostStatus;
|
||||||
|
import com.openisle.model.PostType;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lightweight DTO for listing posts without comments.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class PostSummaryDto {
|
||||||
|
private Long id;
|
||||||
|
private String title;
|
||||||
|
private String content;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private AuthorDto author;
|
||||||
|
private CategoryDto category;
|
||||||
|
private List<TagDto> tags;
|
||||||
|
private long views;
|
||||||
|
private long commentCount;
|
||||||
|
private PostStatus status;
|
||||||
|
private LocalDateTime pinnedAt;
|
||||||
|
private LocalDateTime lastReplyAt;
|
||||||
|
private List<ReactionDto> reactions;
|
||||||
|
private List<AuthorDto> participants;
|
||||||
|
private boolean subscribed;
|
||||||
|
private int reward;
|
||||||
|
private int pointReward;
|
||||||
|
private PostType type;
|
||||||
|
private LotteryDto lottery;
|
||||||
|
private boolean rssExcluded;
|
||||||
|
private boolean closed;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/** Public key response for web push. */
|
||||||
|
@Data
|
||||||
|
public class PushPublicKeyDto {
|
||||||
|
private String key;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/** Request body for saving a push subscription. */
|
||||||
|
@Data
|
||||||
|
public class PushSubscriptionRequest {
|
||||||
|
private String endpoint;
|
||||||
|
private String p256dh;
|
||||||
|
private String auth;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import com.openisle.model.ReactionType;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO representing a reaction on a post, comment or message.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ReactionDto {
|
||||||
|
private Long id;
|
||||||
|
private ReactionType type;
|
||||||
|
private String user;
|
||||||
|
private Long postId;
|
||||||
|
private Long commentId;
|
||||||
|
private Long messageId;
|
||||||
|
private int reward;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import com.openisle.model.ReactionType;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/** Request for reacting to a post or comment. */
|
||||||
|
@Data
|
||||||
|
public class ReactionRequest {
|
||||||
|
private ReactionType type;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/** Request to register a new user. */
|
||||||
|
@Data
|
||||||
|
public class RegisterRequest {
|
||||||
|
private String username;
|
||||||
|
private String email;
|
||||||
|
private String password;
|
||||||
|
private String captcha;
|
||||||
|
private String inviteToken;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user