mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-06 23:21:16 +08:00
Compare commits
549 Commits
codex/add-
...
codex/add-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b204345a6 | ||
|
|
d146bf2b0d | ||
|
|
864a760b20 | ||
|
|
2ccdc21568 | ||
|
|
ff63d232a9 | ||
|
|
32a624e62d | ||
|
|
5af0c9dee0 | ||
|
|
edaafdd000 | ||
|
|
24838ab714 | ||
|
|
56a80a184b | ||
|
|
ed24ed174b | ||
|
|
3080acb6e4 | ||
|
|
1856eb191b | ||
|
|
0c2a50d620 | ||
|
|
7562de11a5 | ||
|
|
aaacf4efb1 | ||
|
|
1f30cdfe85 | ||
|
|
8b37cf5abb | ||
|
|
4af19a75c9 | ||
|
|
37ea986389 | ||
|
|
fefd0b3b6c | ||
|
|
a31ed29cfa | ||
|
|
2719819ad7 | ||
|
|
27ff9a9c9b | ||
|
|
18fde1052f | ||
|
|
800970f078 | ||
|
|
cbbd1440a1 | ||
|
|
215616d771 | ||
|
|
575e90e558 | ||
|
|
e63d66806d | ||
|
|
1fc0118c5a | ||
|
|
f3512c1184 | ||
|
|
28842c90b1 | ||
|
|
d67cc326c4 | ||
|
|
27c217a630 | ||
|
|
4e3e5f147c | ||
|
|
8767aa31d6 | ||
|
|
a428f472f2 | ||
|
|
8544803e62 | ||
|
|
54874cea7a | ||
|
|
098d82a6a0 | ||
|
|
90eee03198 | ||
|
|
3f152906f2 | ||
|
|
ef71d0b3d4 | ||
|
|
6f80d139ba | ||
|
|
7454931fa5 | ||
|
|
0852664a82 | ||
|
|
5814fb673a | ||
|
|
4ee4266e3d | ||
|
|
6a27fbe1d7 | ||
|
|
38ff04c358 | ||
|
|
fc27200ac1 | ||
|
|
b1998be425 | ||
|
|
72adc5b232 | ||
|
|
d24e67de5d | ||
|
|
eefefac236 | ||
|
|
2f339fdbdb | ||
|
|
3808becc8b | ||
|
|
18db4d7317 | ||
|
|
52cbb71945 | ||
|
|
39c34a9048 | ||
|
|
4baabf2224 | ||
|
|
8023183bc6 | ||
|
|
27efc493b2 | ||
|
|
ca6e45a711 | ||
|
|
803ca9e103 | ||
|
|
9d1e12773a | ||
|
|
5a09934866 | ||
|
|
db1d7981c5 | ||
|
|
6e1a7c773c | ||
|
|
ac4f1064e7 | ||
|
|
4e98fd6a89 | ||
|
|
1bf92ab1ad | ||
|
|
c6ab431c87 | ||
|
|
aaa25d5c2f | ||
|
|
569531b462 | ||
|
|
c3ae97f8ba | ||
|
|
a57f3e6406 | ||
|
|
23582934fa | ||
|
|
5adee4db0e | ||
|
|
a2ccc95b4e | ||
|
|
dc5eb5a637 | ||
|
|
55dd36bd24 | ||
|
|
59232f99ca | ||
|
|
f93f58b055 | ||
|
|
8ad35af199 | ||
|
|
d427a41f6d | ||
|
|
ea53bc3c83 | ||
|
|
3a39cfdb49 | ||
|
|
3d1b8b8e6e | ||
|
|
f0e58d1efe | ||
|
|
5c4aca5ab8 | ||
|
|
fff59e800d | ||
|
|
b42ed19160 | ||
|
|
6fd663d983 | ||
|
|
fd6fc11630 | ||
|
|
d7bfeed259 | ||
|
|
c5e4da5e07 | ||
|
|
b87932560b | ||
|
|
58ff8b177e | ||
|
|
4f6b585735 | ||
|
|
ac81bccd20 | ||
|
|
351447e3d1 | ||
|
|
453d8fa68b | ||
|
|
2c5b38ee9e | ||
|
|
b5fd5a3edc | ||
|
|
ee717aced2 | ||
|
|
9a9152593e | ||
|
|
856d3dd513 | ||
|
|
0e42a3335a | ||
|
|
d96aae59d2 | ||
|
|
122722d0e9 | ||
|
|
0c2264e509 | ||
|
|
1e503e26f2 | ||
|
|
ec0fd63e30 | ||
|
|
dfd4c70b6e | ||
|
|
d79dc8877d | ||
|
|
e979350d40 | ||
|
|
99bf80a47a | ||
|
|
bfadda1e7d | ||
|
|
906998a07f | ||
|
|
02287c05be | ||
|
|
56aed4603e | ||
|
|
a1fa7b2d5b | ||
|
|
083c7980c6 | ||
|
|
3d51f29be7 | ||
|
|
d243e3a9d6 | ||
|
|
2b3c60f9a7 | ||
|
|
8b948a20cd | ||
|
|
5053ac213d | ||
|
|
e5ec801785 | ||
|
|
31e25232d0 | ||
|
|
cdc92aeebe | ||
|
|
d2c2213197 | ||
|
|
c687ffed54 | ||
|
|
5bc9ff45d7 | ||
|
|
78c7681bc8 | ||
|
|
5eb206a358 | ||
|
|
18179cca22 | ||
|
|
2b28cb2ac1 | ||
|
|
610a645092 | ||
|
|
504ca55cad | ||
|
|
0fc1415a14 | ||
|
|
50a84220fe | ||
|
|
af3e049c23 | ||
|
|
c33b411659 | ||
|
|
e8a162d859 | ||
|
|
e819926cf3 | ||
|
|
013d47e8e4 | ||
|
|
6cc76593e4 | ||
|
|
a2a08331e2 | ||
|
|
3eabafadf8 | ||
|
|
62c1983fd5 | ||
|
|
689b719e18 | ||
|
|
c6eccb01b9 | ||
|
|
cdf7e61157 | ||
|
|
d23511ecb9 | ||
|
|
c76708d2ff | ||
|
|
d978bd428e | ||
|
|
e5954cfb62 | ||
|
|
cb614b9739 | ||
|
|
88ce6b682d | ||
|
|
e02db635c4 | ||
|
|
231379181a | ||
|
|
bd9ce67d4b | ||
|
|
6527b3790e | ||
|
|
f01e8c942a | ||
|
|
1e1ae29d32 | ||
|
|
d31a8bfee4 | ||
|
|
29a96595f7 | ||
|
|
2b242367d7 | ||
|
|
3f0cd2bf0f | ||
|
|
a98a631378 | ||
|
|
7701d359dc | ||
|
|
ffd9ef8a32 | ||
|
|
36cd5ab171 | ||
|
|
58d86fa065 | ||
|
|
df71cf901b | ||
|
|
ac3fc6702a | ||
|
|
b0eef220a6 | ||
|
|
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 | ||
|
|
b3ea41ad1e | ||
|
|
80ecb1620d |
20
.github/ISSUE_TEMPLATE/新功能建议.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/新功能建议.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: 新功能建议
|
||||
about: 请为该项目提出一个想法
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**你的功能请求是否与某个问题相关?请描述。**
|
||||
请清晰、简洁地说明问题。例如:“我经常因为……而感到困扰。”
|
||||
|
||||
**你期望的解决方案**
|
||||
请清晰、简洁地描述你希望发生的事情/功能如何工作。
|
||||
|
||||
**你考虑过的替代方案**
|
||||
请清晰、简洁地说明你已考虑过的其他解决方案或功能。
|
||||
|
||||
**其他上下文**
|
||||
在此添加与功能请求相关的其他信息或截图。
|
||||
41
.github/ISSUE_TEMPLATE/错误-bug报告.md
vendored
Normal file
41
.github/ISSUE_TEMPLATE/错误-bug报告.md
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: 错误/Bug报告
|
||||
about: 创建报告以帮助我们改进
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**描述 Bug**
|
||||
对该 Bug 进行清晰简明的描述。
|
||||
|
||||
**复现步骤**
|
||||
复现该问题的步骤:
|
||||
|
||||
1. 进入 '...'
|
||||
2. 点击 '...'
|
||||
3. 下拉到 '...'
|
||||
4. 看到错误
|
||||
|
||||
**预期行为**
|
||||
清晰简明地描述你期望发生的情况。
|
||||
|
||||
**截图**
|
||||
如果适用,请添加截图以帮助解释问题。
|
||||
|
||||
**桌面端(请完成以下信息):**
|
||||
|
||||
* 操作系统:\[例如 iOS]
|
||||
* 浏览器:\[例如 Chrome、Safari]
|
||||
* 版本:\[例如 22]
|
||||
|
||||
**移动端(请完成以下信息):**
|
||||
|
||||
* 设备:\[例如 iPhone6]
|
||||
* 操作系统:\[例如 iOS8.1]
|
||||
* 浏览器:\[例如 系统自带浏览器、Safari]
|
||||
* 版本:\[例如 22]
|
||||
|
||||
**附加上下文**
|
||||
在此添加与问题相关的其他上下文信息。
|
||||
23
.github/workflows/deploy-staging.yml
vendored
Normal file
23
.github/workflows/deploy-staging.yml
vendored
Normal file
@@ -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
|
||||
|
||||
20
.github/workflows/deploy.yml
vendored
20
.github/workflows/deploy.yml
vendored
@@ -1,9 +1,9 @@
|
||||
name: CI & CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 19 * * *" # 每天 UTC 19:00,相当于北京时间凌晨3点
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
@@ -13,22 +13,6 @@ jobs:
|
||||
steps:
|
||||
- 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:
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
|
||||
32
CODE_OF_CONDUCT.md
Normal file
32
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# OpenIsle Code of Conduct
|
||||
|
||||
Like the technical community as a whole, the OpenIsle team and community is made up of a mixture of professionals and volunteers from all over the world, working on every aspect of the mission - including mentorship, teaching, and connecting people.
|
||||
|
||||
Diversity is one of our huge strengths, but it can also lead to communication issues and unhappiness. To that end, we have a few ground rules that we ask people to adhere to. This code applies equally to founders, mentors and those seeking help and guidance.
|
||||
|
||||
This isn’t an exhaustive list of things that you can’t do. Rather, take it in the spirit in which it’s intended - a guide to make it easier to enrich all of us and the technical communities in which we participate.
|
||||
|
||||
This code of conduct applies to all spaces managed by the OpenIsle project or . This includes IRC, the mailing lists, the issue tracker, DSF events, and any other forums created by the project team which the community uses for communication. In addition, violations of this code outside these spaces may affect a person's ability to participate within them.
|
||||
|
||||
If you believe someone is violating the code of conduct, we ask that you report it by emailing [](mailto:). For more details please see our
|
||||
|
||||
- **Be friendly and patient.**
|
||||
- **Be welcoming.** We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, colour, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability.
|
||||
- **Be considerate.** Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and colleagues, and you should take those consequences into account when making decisions. Remember that we're a world-wide community, so you might not be communicating in someone else's primary language.
|
||||
- **Be respectful.** Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. It’s important to remember that a community where people feel uncomfortable or threatened is not a productive one. Members of the OpenIsle community should be respectful when dealing with other members as well as with people outside the OpenIsle community.
|
||||
- **Be careful in the words that you choose.** We are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do not insult or put down other participants. Harassment and other exclusionary behavior aren't acceptable. This includes, but is not limited to:
|
||||
- Violent threats or language directed against another person.
|
||||
- Discriminatory jokes and language.
|
||||
- Posting sexually explicit or violent material.
|
||||
- Posting (or threatening to post) other people's personally identifying information ("doxing").
|
||||
- Personal insults, especially those using racist or sexist terms.
|
||||
- Unwelcome sexual attention.
|
||||
- Advocating for, or encouraging, any of the above behavior.
|
||||
- Repeated harassment of others. In general, if someone asks you to stop, then stop.
|
||||
- **When we disagree, try to understand why.** Disagreements, both social and technical, happen all the time and OpenIsle is no exception. It is important that we resolve disagreements and differing views constructively. Remember that we’re different. The strength of OpenIsle comes from its varied community, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesn’t mean that they’re wrong. Don’t forget that it is human to err and blaming each other doesn’t get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes.
|
||||
|
||||
Original text courtesy of the [Speak Up! project](http://web.archive.org/web/20141109123859/http://speakup.io/coc.html).
|
||||
|
||||
## Questions?
|
||||
|
||||
If you have questions, please see . If that doesn't answer your questions, feel free to [contact us](mailto:).
|
||||
120
CONTRIBUTING.md
Normal file
120
CONTRIBUTING.md
Normal file
@@ -0,0 +1,120 @@
|
||||
#### **⚠️注意:仅想修改前端的朋友可不用部署后端服务**
|
||||
|
||||
## 如何部署
|
||||
|
||||
> 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 前端部署
|
||||
|
||||
**⚠️ 环境要求:Node.js 版本最低 20.0.0(因为 Nuxt 框架要求)**
|
||||
|
||||
前端可以依赖本机部署的后端,也可以直接调用线上的后端接口
|
||||
|
||||
```shell
|
||||
cd ../frontend_nuxt/
|
||||
```
|
||||
|
||||
copy环境.env文件
|
||||
|
||||
```shell
|
||||
cp .env.staging.example .env
|
||||
```
|
||||
|
||||
1. 依赖本机部署的后端:打开本文件夹,修改.env 修改为瞄准本机后端端口
|
||||
|
||||
```yaml
|
||||
; 本地部署后端
|
||||
NUXT_PUBLIC_API_BASE_URL=http://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
|
||||
; 开发环境
|
||||
NUXT_PUBLIC_WEBSITE_BASE_URL=localhost:3000
|
||||
```
|
||||
|
||||
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 即可访问前端页面
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Tim
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
22
README.md
22
README.md
@@ -1,28 +1,18 @@
|
||||
<p align="center">
|
||||
<img alt="OpenIsle" src="https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/image.png" width="200">
|
||||
<br><br>
|
||||
高效的开源社区前后端端平台
|
||||
<br><br>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square"></a>
|
||||
<br>
|
||||
高效的开源社区前后端平台
|
||||
<br><br><br>
|
||||
<img alt="Image" src="https://openisle-1307107697.cos.accelerate.myqcloud.com/dynamic_assert/22752cfac5a04a9c90c41995b9f55fed.png" width="1200">
|
||||
</p>
|
||||
|
||||
## 💡 简介
|
||||
|
||||
OpenIsle 是一个使用 Spring Boot 和 Vue 3 构建的全栈开源社区平台,提供用户注册、登录、贴文发布、评论交互等完整功能,可用于项目社区或直接打造自主社区站点。
|
||||
|
||||
## 🚀 部署
|
||||
## 🚧 开发 & 部署
|
||||
|
||||
### 后端
|
||||
|
||||
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/` 文件,配合线上网站方式部署
|
||||
详细见 [Contributing](https://github.com/nagisa77/OpenIsle?tab=contributing-ov-file)
|
||||
|
||||
## ✨ 项目特点
|
||||
|
||||
|
||||
@@ -3,6 +3,12 @@ MYSQL_URL=jdbc:mysql://<数据库地址>:<端口>/<数据库名>?useUnicode=yes&
|
||||
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>
|
||||
@@ -22,6 +28,7 @@ TWITTER_CLIENT_ID=<你的twitter-client-id>
|
||||
TWITTER_CLIENT_SECRET=<你的-twitter-client-secret>
|
||||
DISCORD_CLIENT_ID=<你的discord-client-id>
|
||||
DISCORD_CLIENT_SECRET=<你的discord-client-secret>
|
||||
TELEGRAM_BOT_TOKEN=<你的telegram-bot-token>
|
||||
|
||||
# === OPENAI ===
|
||||
OPENAI_API_KEY=<你的openai-api-key>
|
||||
@@ -30,4 +37,4 @@ OPENAI_API_KEY=<你的openai-api-key>
|
||||
WEBPUSH_PUBLIC_KEY=<你的webpush-public-key>
|
||||
WEBPUSH_PRIVATE_KEY=<你的webpush-private-key>
|
||||
|
||||
# LOG_LEVEL=DEBUG
|
||||
# LOG_LEVEL=DEBUG
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
@@ -38,6 +42,16 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</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>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
|
||||
@@ -6,6 +6,8 @@ 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
|
||||
@@ -22,5 +24,16 @@ public class ActivityInitializer implements CommandLineRunner {
|
||||
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,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ public class SecurityConfig {
|
||||
private final UserRepository userRepository;
|
||||
private final AccessDeniedHandler customAccessDeniedHandler;
|
||||
private final UserVisitService userVisitService;
|
||||
@Value("${app.website-url:https://www.open-isle.com}")
|
||||
@Value("${app.website-url}")
|
||||
private String websiteUrl;
|
||||
|
||||
@Bean
|
||||
@@ -75,14 +75,16 @@ public class SecurityConfig {
|
||||
cfg.setAllowedOrigins(List.of(
|
||||
"http://127.0.0.1:8080",
|
||||
"http://127.0.0.1:3000",
|
||||
"http://127.0.0.1:3001",
|
||||
"http://127.0.0.1",
|
||||
"http://localhost:8080",
|
||||
"http://localhost:3000",
|
||||
"http://localhost:3001",
|
||||
"http://localhost",
|
||||
"http://30.211.97.238:3000",
|
||||
"http://30.211.97.238",
|
||||
"http://192.168.7.70",
|
||||
"http://192.168.7.70:8080",
|
||||
"http://192.168.7.98",
|
||||
"http://192.168.7.98:3000",
|
||||
websiteUrl,
|
||||
websiteUrl.replace("://www.", "://")
|
||||
));
|
||||
@@ -97,11 +99,13 @@ public class SecurityConfig {
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http.csrf(csrf -> csrf.disable())
|
||||
.cors(Customizer.withDefaults()) // 让 Spring 自带 CorsFilter 处理预检
|
||||
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.cors(Customizer.withDefaults())
|
||||
.headers(h -> h.frameOptions(f -> f.sameOrigin()))
|
||||
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.exceptionHandling(eh -> eh.accessDeniedHandler(customAccessDeniedHandler))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
|
||||
.requestMatchers("/api/ws/**", "/api/sockjs/**").permitAll()
|
||||
.requestMatchers(HttpMethod.POST, "/api/auth/**").permitAll()
|
||||
.requestMatchers(HttpMethod.GET, "/api/posts/**").permitAll()
|
||||
.requestMatchers(HttpMethod.GET, "/api/comments/**").permitAll()
|
||||
@@ -117,6 +121,10 @@ public class SecurityConfig {
|
||||
.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/tags/**").authenticated()
|
||||
.requestMatchers(HttpMethod.DELETE, "/api/categories/**").hasAuthority("ADMIN")
|
||||
@@ -149,7 +157,9 @@ public class SecurityConfig {
|
||||
uri.startsWith("/api/search") || uri.startsWith("/api/users") ||
|
||||
uri.startsWith("/api/reaction-types") || uri.startsWith("/api/config") ||
|
||||
uri.startsWith("/api/activities") || uri.startsWith("/api/push/public-key") ||
|
||||
uri.startsWith("/api/sitemap.xml") || uri.startsWith("/api/medals"));
|
||||
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 ")) {
|
||||
String token = authHeader.substring(7);
|
||||
@@ -165,7 +175,8 @@ public class SecurityConfig {
|
||||
response.getWriter().write("{\"error\": \"Invalid or expired token\"}");
|
||||
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.setContentType("application/json");
|
||||
response.getWriter().write("{\"error\": \"Missing token\"}");
|
||||
|
||||
110
backend/src/main/java/com/openisle/config/WebSocketConfig.java
Normal file
110
backend/src/main/java/com/openisle/config/WebSocketConfig.java
Normal file
@@ -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,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));
|
||||
}
|
||||
}
|
||||
@@ -45,4 +45,14 @@ public class AdminPostController {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ public class AdminUserController {
|
||||
private final UserRepository userRepository;
|
||||
private final NotificationRepository notificationRepository;
|
||||
private final EmailSender emailSender;
|
||||
@Value("${app.website-url:https://www.open-isle.com}")
|
||||
@Value("${app.website-url}")
|
||||
private String websiteUrl;
|
||||
|
||||
@PostMapping("/{id}/approve")
|
||||
|
||||
@@ -26,9 +26,11 @@ public class AuthController {
|
||||
private final GithubAuthService githubAuthService;
|
||||
private final DiscordAuthService discordAuthService;
|
||||
private final TwitterAuthService twitterAuthService;
|
||||
private final TelegramAuthService telegramAuthService;
|
||||
private final RegisterModeService registerModeService;
|
||||
private final NotificationService notificationService;
|
||||
private final UserRepository userRepository;
|
||||
private final InviteService inviteService;
|
||||
|
||||
|
||||
@Value("${app.captcha.enabled:false}")
|
||||
@@ -45,6 +47,27 @@ public class AuthController {
|
||||
if (captchaEnabled && registerCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
|
||||
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(
|
||||
req.getUsername(), req.getEmail(), req.getPassword(), "", registerModeService.getRegisterMode());
|
||||
emailService.sendEmail(user.getEmail(), "在网站填写验证码以验证", "您的验证码是 " + user.getVerificationCode());
|
||||
@@ -58,10 +81,26 @@ public class AuthController {
|
||||
public ResponseEntity<?> verify(@RequestBody VerifyRequest req) {
|
||||
boolean ok = userService.verifyCode(req.getUsername(), req.getCode());
|
||||
if (ok) {
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"message", "Verified",
|
||||
"token", jwtService.generateReasonToken(req.getUsername())
|
||||
));
|
||||
Optional<User> userOpt = userService.findByUsername(req.getUsername());
|
||||
if (userOpt.isEmpty()) {
|
||||
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"));
|
||||
}
|
||||
@@ -106,27 +145,43 @@ public class AuthController {
|
||||
|
||||
@PostMapping("/google")
|
||||
public ResponseEntity<?> loginWithGoogle(@RequestBody GoogleLoginRequest req) {
|
||||
Optional<User> user = googleAuthService.authenticate(req.getIdToken(), registerModeService.getRegisterMode());
|
||||
if (user.isPresent()) {
|
||||
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
|
||||
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 = 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 (user.get().getRegisterReason() != null && !user.get().getRegisterReason().isEmpty()) {
|
||||
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
||||
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(
|
||||
"error", "Account awaiting approval",
|
||||
"reason_code", "IS_APPROVING",
|
||||
"token", jwtService.generateReasonToken(user.get().getUsername())
|
||||
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
||||
));
|
||||
}
|
||||
return ResponseEntity.badRequest().body(Map.of(
|
||||
"error", "Account awaiting approval",
|
||||
"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(
|
||||
"error", "Invalid google token",
|
||||
@@ -165,28 +220,45 @@ public class AuthController {
|
||||
|
||||
@PostMapping("/github")
|
||||
public ResponseEntity<?> loginWithGithub(@RequestBody GithubLoginRequest req) {
|
||||
Optional<User> user = githubAuthService.authenticate(req.getCode(), registerModeService.getRegisterMode(), req.getRedirectUri());
|
||||
if (user.isPresent()) {
|
||||
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
|
||||
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 = 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 (user.get().getRegisterReason() != null && !user.get().getRegisterReason().isEmpty()) {
|
||||
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
||||
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(
|
||||
"error", "Account awaiting approval",
|
||||
"reason_code", "IS_APPROVING",
|
||||
"token", jwtService.generateReasonToken(user.get().getUsername())
|
||||
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
||||
));
|
||||
}
|
||||
return ResponseEntity.badRequest().body(Map.of(
|
||||
"error", "Account awaiting approval",
|
||||
"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(
|
||||
"error", "Invalid github code",
|
||||
@@ -196,27 +268,44 @@ public class AuthController {
|
||||
|
||||
@PostMapping("/discord")
|
||||
public ResponseEntity<?> loginWithDiscord(@RequestBody DiscordLoginRequest req) {
|
||||
Optional<User> user = discordAuthService.authenticate(req.getCode(), registerModeService.getRegisterMode(), req.getRedirectUri());
|
||||
if (user.isPresent()) {
|
||||
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
|
||||
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 = 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 (user.get().getRegisterReason() != null && !user.get().getRegisterReason().isEmpty()) {
|
||||
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
||||
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(
|
||||
"error", "Account awaiting approval",
|
||||
"reason_code", "IS_APPROVING",
|
||||
"token", jwtService.generateReasonToken(user.get().getUsername())
|
||||
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
||||
));
|
||||
}
|
||||
return ResponseEntity.badRequest().body(Map.of(
|
||||
"error", "Account awaiting approval",
|
||||
"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(
|
||||
"error", "Invalid discord code",
|
||||
@@ -226,31 +315,45 @@ public class AuthController {
|
||||
|
||||
@PostMapping("/twitter")
|
||||
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.getCodeVerifier(),
|
||||
registerModeService.getRegisterMode(),
|
||||
req.getRedirectUri());
|
||||
if (user.isPresent()) {
|
||||
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
|
||||
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 (user.get().getRegisterReason() != null && !user.get().getRegisterReason().isEmpty()) {
|
||||
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
||||
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(
|
||||
"error", "Account awaiting approval",
|
||||
"reason_code", "IS_APPROVING",
|
||||
"token", jwtService.generateReasonToken(user.get().getUsername())
|
||||
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
||||
));
|
||||
}
|
||||
return ResponseEntity.badRequest().body(Map.of(
|
||||
"error", "Account awaiting approval",
|
||||
"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(
|
||||
"error", "Invalid twitter code",
|
||||
@@ -258,6 +361,51 @@ public class AuthController {
|
||||
));
|
||||
}
|
||||
|
||||
@PostMapping("/telegram")
|
||||
public ResponseEntity<?> loginWithTelegram(@RequestBody TelegramLoginRequest req) {
|
||||
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 = telegramAuthService.authenticate(
|
||||
req,
|
||||
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 (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
||||
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(
|
||||
"error", "Account awaiting approval",
|
||||
"reason_code", "IS_APPROVING",
|
||||
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
||||
));
|
||||
}
|
||||
return ResponseEntity.badRequest().body(Map.of(
|
||||
"error", "Account awaiting approval",
|
||||
"reason_code", "NOT_APPROVED",
|
||||
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
||||
));
|
||||
}
|
||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
|
||||
}
|
||||
return ResponseEntity.badRequest().body(Map.of(
|
||||
"error", "Invalid telegram data",
|
||||
"reason_code", "INVALID_CREDENTIALS"
|
||||
));
|
||||
}
|
||||
|
||||
@GetMapping("/check")
|
||||
public ResponseEntity<?> checkToken() {
|
||||
return ResponseEntity.ok(Map.of("valid", true));
|
||||
|
||||
@@ -12,6 +12,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@@ -44,8 +45,11 @@ public class CategoryController {
|
||||
|
||||
@GetMapping
|
||||
public List<CategoryDto> list() {
|
||||
return categoryService.listCategories().stream()
|
||||
.map(c -> categoryMapper.toDto(c, postService.countPostsByCategory(c.getId())))
|
||||
List<Category> all = categoryService.listCategories();
|
||||
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()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ public class CommentController {
|
||||
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));
|
||||
dto.setPointReward(pointService.awardForComment(auth.getName(), postId, comment.getId()));
|
||||
log.debug("createComment succeeded for comment {}", comment.getId());
|
||||
return ResponseEntity.ok(dto);
|
||||
}
|
||||
@@ -85,4 +85,16 @@ public class CommentController {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,9 +23,19 @@ public class NotificationController {
|
||||
private final NotificationMapper notificationMapper;
|
||||
|
||||
@GetMapping
|
||||
public List<NotificationDto> list(@RequestParam(value = "read", required = false) Boolean read,
|
||||
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(), read).stream()
|
||||
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());
|
||||
}
|
||||
@@ -52,4 +62,14 @@ public class NotificationController {
|
||||
public void updatePref(@RequestBody NotificationPreferenceUpdateRequest req, Authentication auth) {
|
||||
notificationService.updatePreference(auth.getName(), req.getType(), req.isEnabled());
|
||||
}
|
||||
|
||||
@GetMapping("/email-prefs")
|
||||
public List<NotificationPreferenceDto> emailPrefs(Authentication auth) {
|
||||
return notificationService.listEmailPreferences(auth.getName());
|
||||
}
|
||||
|
||||
@PostMapping("/email-prefs")
|
||||
public void updateEmailPref(@RequestBody NotificationPreferenceUpdateRequest req, Authentication auth) {
|
||||
notificationService.updateEmailPreference(auth.getName(), req.getType(), req.isEnabled());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
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.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
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());
|
||||
}
|
||||
|
||||
@GetMapping("/trend")
|
||||
public List<Map<String, Object>> trend(Authentication auth,
|
||||
@RequestParam(value = "days", defaultValue = "30") int days) {
|
||||
return pointService.trend(auth.getName(), days);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.openisle.controller;
|
||||
import com.openisle.dto.PostDetailDto;
|
||||
import com.openisle.dto.PostRequest;
|
||||
import com.openisle.dto.PostSummaryDto;
|
||||
import com.openisle.dto.PollDto;
|
||||
import com.openisle.mapper.PostMapper;
|
||||
import com.openisle.model.Post;
|
||||
import com.openisle.service.*;
|
||||
@@ -41,11 +42,13 @@ public class PostController {
|
||||
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());
|
||||
req.getPrizeCount(), req.getPointCost(),
|
||||
req.getStartTime(), req.getEndTime(),
|
||||
req.getOptions(), req.getMultiple());
|
||||
draftService.deleteDraft(auth.getName());
|
||||
PostDetailDto dto = postMapper.toDetailDto(post, auth.getName());
|
||||
dto.setReward(levelService.awardForPost(auth.getName()));
|
||||
dto.setPointReward(pointService.awardForPost(auth.getName()));
|
||||
dto.setPointReward(pointService.awardForPost(auth.getName(), post.getId()));
|
||||
return ResponseEntity.ok(dto);
|
||||
}
|
||||
|
||||
@@ -62,6 +65,16 @@ public class PostController {
|
||||
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;
|
||||
@@ -75,6 +88,17 @@ public class PostController {
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/poll/progress")
|
||||
public ResponseEntity<PollDto> pollProgress(@PathVariable Long id) {
|
||||
return ResponseEntity.ok(postMapper.toSummaryDto(postService.getPoll(id)).getPoll());
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/poll/vote")
|
||||
public ResponseEntity<Void> vote(@PathVariable Long id, @RequestParam("option") List<Integer> option, Authentication auth) {
|
||||
postService.votePoll(id, auth.getName(), option);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<PostSummaryDto> listPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
|
||||
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
|
||||
@@ -161,4 +185,27 @@ public class PostController {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ public class ReactionController {
|
||||
Authentication auth) {
|
||||
Reaction reaction = reactionService.reactToPost(auth.getName(), postId, req.getType());
|
||||
if (reaction == null) {
|
||||
pointService.deductForReactionOfPost(auth.getName(), postId);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
ReactionDto dto = reactionMapper.toDto(reaction);
|
||||
@@ -50,6 +51,7 @@ public class ReactionController {
|
||||
Authentication auth) {
|
||||
Reaction reaction = reactionService.reactToComment(auth.getName(), commentId, req.getType());
|
||||
if (reaction == null) {
|
||||
pointService.deductForReactionOfComment(auth.getName(), commentId);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
ReactionDto dto = reactionMapper.toDto(reaction);
|
||||
@@ -57,4 +59,17 @@ public class ReactionController {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
352
backend/src/main/java/com/openisle/controller/RssController.java
Normal file
352
backend/src/main/java/com/openisle/controller/RssController.java
Normal file
@@ -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("<BaseImage[^>]+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; }
|
||||
}
|
||||
@@ -22,7 +22,7 @@ import java.util.List;
|
||||
public class SitemapController {
|
||||
private final PostRepository postRepository;
|
||||
|
||||
@Value("${app.website-url:https://www.open-isle.com}")
|
||||
@Value("${app.website-url}")
|
||||
private String websiteUrl;
|
||||
|
||||
@GetMapping(value = "/sitemap.xml", produces = MediaType.APPLICATION_XML_VALUE)
|
||||
|
||||
@@ -15,6 +15,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@@ -62,8 +63,11 @@ public class TagController {
|
||||
@GetMapping
|
||||
public List<TagDto> list(@RequestParam(value = "keyword", required = false) String keyword,
|
||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
||||
List<TagDto> dtos = tagService.searchTags(keyword).stream()
|
||||
.map(t -> tagMapper.toDto(t, postService.countPostsByTag(t.getId())))
|
||||
List<Tag> tags = tagService.searchTags(keyword);
|
||||
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()))
|
||||
.collect(Collectors.toList());
|
||||
if (limit != null && limit > 0 && dtos.size() > limit) {
|
||||
|
||||
@@ -105,6 +105,17 @@ public class UserController {
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
|
||||
@GetMapping("/{identifier}/subscribed-posts")
|
||||
public java.util.List<PostMetaDto> subscribedPosts(@PathVariable("identifier") String identifier,
|
||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
||||
int l = limit != null ? limit : defaultPostsLimit;
|
||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||
return subscriptionService.getSubscribedPosts(user.getUsername()).stream()
|
||||
.limit(l)
|
||||
.map(userMapper::toMetaDto)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
|
||||
@GetMapping("/{identifier}/replies")
|
||||
public java.util.List<CommentInfoDto> userReplies(@PathVariable("identifier") String identifier,
|
||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
||||
|
||||
17
backend/src/main/java/com/openisle/dto/ChannelDto.java
Normal file
17
backend/src/main/java/com/openisle/dto/ChannelDto.java
Normal file
@@ -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;
|
||||
}
|
||||
@@ -13,6 +13,7 @@ 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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
20
backend/src/main/java/com/openisle/dto/ConversationDto.java
Normal file
20
backend/src/main/java/com/openisle/dto/ConversationDto.java
Normal file
@@ -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;
|
||||
}
|
||||
@@ -7,4 +7,5 @@ import lombok.Data;
|
||||
public class DiscordLoginRequest {
|
||||
private String code;
|
||||
private String redirectUri;
|
||||
private String inviteToken;
|
||||
}
|
||||
|
||||
12
backend/src/main/java/com/openisle/dto/FeaturedMedalDto.java
Normal file
12
backend/src/main/java/com/openisle/dto/FeaturedMedalDto.java
Normal file
@@ -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;
|
||||
}
|
||||
|
||||
@@ -7,4 +7,5 @@ import lombok.Data;
|
||||
public class GithubLoginRequest {
|
||||
private String code;
|
||||
private String redirectUri;
|
||||
private String inviteToken;
|
||||
}
|
||||
|
||||
@@ -6,4 +6,5 @@ import lombok.Data;
|
||||
@Data
|
||||
public class GoogleLoginRequest {
|
||||
private String idToken;
|
||||
private String inviteToken;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ public class LotteryDto {
|
||||
private String prizeDescription;
|
||||
private String prizeIcon;
|
||||
private int prizeCount;
|
||||
private int pointCost;
|
||||
private LocalDateTime startTime;
|
||||
private LocalDateTime endTime;
|
||||
private List<AuthorDto> participants;
|
||||
|
||||
16
backend/src/main/java/com/openisle/dto/MessageDto.java
Normal file
16
backend/src/main/java/com/openisle/dto/MessageDto.java
Normal file
@@ -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;
|
||||
}
|
||||
12
backend/src/main/java/com/openisle/dto/PointGoodDto.java
Normal file
12
backend/src/main/java/com/openisle/dto/PointGoodDto.java
Normal file
@@ -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;
|
||||
}
|
||||
23
backend/src/main/java/com/openisle/dto/PointHistoryDto.java
Normal file
23
backend/src/main/java/com/openisle/dto/PointHistoryDto.java
Normal file
@@ -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;
|
||||
}
|
||||
17
backend/src/main/java/com/openisle/dto/PollDto.java
Normal file
17
backend/src/main/java/com/openisle/dto/PollDto.java
Normal file
@@ -0,0 +1,17 @@
|
||||
package com.openisle.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
public class PollDto {
|
||||
private List<String> options;
|
||||
private Map<Integer, Integer> votes;
|
||||
private LocalDateTime endTime;
|
||||
private List<AuthorDto> participants;
|
||||
private Map<Integer, List<AuthorDto>> optionParticipants;
|
||||
private boolean multiple;
|
||||
}
|
||||
@@ -23,7 +23,11 @@ public class PostRequest {
|
||||
private String prizeDescription;
|
||||
private String prizeIcon;
|
||||
private Integer prizeCount;
|
||||
private Integer pointCost;
|
||||
private LocalDateTime startTime;
|
||||
private LocalDateTime endTime;
|
||||
// fields for poll posts
|
||||
private List<String> options;
|
||||
private Boolean multiple;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,5 +31,8 @@ public class PostSummaryDto {
|
||||
private int pointReward;
|
||||
private PostType type;
|
||||
private LotteryDto lottery;
|
||||
private PollDto poll;
|
||||
private boolean rssExcluded;
|
||||
private boolean closed;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import com.openisle.model.ReactionType;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* DTO representing a reaction on a post or comment.
|
||||
* DTO representing a reaction on a post, comment or message.
|
||||
*/
|
||||
@Data
|
||||
public class ReactionDto {
|
||||
@@ -13,6 +13,7 @@ public class ReactionDto {
|
||||
private String user;
|
||||
private Long postId;
|
||||
private Long commentId;
|
||||
private Long messageId;
|
||||
private int reward;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,4 +9,5 @@ public class RegisterRequest {
|
||||
private String email;
|
||||
private String password;
|
||||
private String captcha;
|
||||
private String inviteToken;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.openisle.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/** Request for Telegram login. */
|
||||
@Data
|
||||
public class TelegramLoginRequest {
|
||||
private String id;
|
||||
private String firstName;
|
||||
private String lastName;
|
||||
private String username;
|
||||
private String photoUrl;
|
||||
private Long authDate;
|
||||
private String hash;
|
||||
private String inviteToken;
|
||||
}
|
||||
@@ -8,4 +8,5 @@ public class TwitterLoginRequest {
|
||||
private String code;
|
||||
private String redirectUri;
|
||||
private String codeVerifier;
|
||||
private String inviteToken;
|
||||
}
|
||||
|
||||
10
backend/src/main/java/com/openisle/dto/UserSummaryDto.java
Normal file
10
backend/src/main/java/com/openisle/dto/UserSummaryDto.java
Normal file
@@ -0,0 +1,10 @@
|
||||
package com.openisle.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class UserSummaryDto {
|
||||
private Long id;
|
||||
private String username;
|
||||
private String avatar;
|
||||
}
|
||||
@@ -24,6 +24,7 @@ public class CommentMapper {
|
||||
dto.setId(comment.getId());
|
||||
dto.setContent(comment.getContent());
|
||||
dto.setCreatedAt(comment.getCreatedAt());
|
||||
dto.setPinnedAt(comment.getPinnedAt());
|
||||
dto.setAuthor(userMapper.toAuthorDto(comment.getAuthor()));
|
||||
dto.setReward(0);
|
||||
return dto;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.openisle.mapper;
|
||||
|
||||
import com.openisle.dto.PointGoodDto;
|
||||
import com.openisle.model.PointGood;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** Mapper for point mall goods. */
|
||||
@Component
|
||||
public class PointGoodMapper {
|
||||
public PointGoodDto toDto(PointGood good) {
|
||||
PointGoodDto dto = new PointGoodDto();
|
||||
dto.setId(good.getId());
|
||||
dto.setName(good.getName());
|
||||
dto.setCost(good.getCost());
|
||||
dto.setImage(good.getImage());
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.openisle.mapper;
|
||||
|
||||
import com.openisle.dto.PointHistoryDto;
|
||||
import com.openisle.model.PointHistory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class PointHistoryMapper {
|
||||
public PointHistoryDto toDto(PointHistory history) {
|
||||
PointHistoryDto dto = new PointHistoryDto();
|
||||
dto.setId(history.getId());
|
||||
dto.setType(history.getType());
|
||||
dto.setAmount(history.getAmount());
|
||||
dto.setBalance(history.getBalance());
|
||||
dto.setCreatedAt(history.getCreatedAt());
|
||||
if (history.getPost() != null) {
|
||||
dto.setPostId(history.getPost().getId());
|
||||
dto.setPostTitle(history.getPost().getTitle());
|
||||
}
|
||||
if (history.getComment() != null) {
|
||||
dto.setCommentId(history.getComment().getId());
|
||||
dto.setCommentContent(history.getComment().getContent());
|
||||
if (history.getComment().getPost() != null && dto.getPostId() == null) {
|
||||
dto.setPostId(history.getComment().getPost().getId());
|
||||
dto.setPostTitle(history.getComment().getPost().getTitle());
|
||||
}
|
||||
}
|
||||
if (history.getFromUser() != null) {
|
||||
dto.setFromUserId(history.getFromUser().getId());
|
||||
dto.setFromUserName(history.getFromUser().getUsername());
|
||||
}
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
@@ -5,18 +5,24 @@ import com.openisle.dto.PostDetailDto;
|
||||
import com.openisle.dto.PostSummaryDto;
|
||||
import com.openisle.dto.ReactionDto;
|
||||
import com.openisle.dto.LotteryDto;
|
||||
import com.openisle.dto.PollDto;
|
||||
import com.openisle.dto.AuthorDto;
|
||||
import com.openisle.model.CommentSort;
|
||||
import com.openisle.model.Post;
|
||||
import com.openisle.model.LotteryPost;
|
||||
import com.openisle.model.PollPost;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.model.PollVote;
|
||||
import com.openisle.service.CommentService;
|
||||
import com.openisle.service.ReactionService;
|
||||
import com.openisle.service.SubscriptionService;
|
||||
import com.openisle.repository.PollVoteRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** Mapper responsible for converting posts into DTOs. */
|
||||
@@ -32,6 +38,7 @@ public class PostMapper {
|
||||
private final UserMapper userMapper;
|
||||
private final TagMapper tagMapper;
|
||||
private final CategoryMapper categoryMapper;
|
||||
private final PollVoteRepository pollVoteRepository;
|
||||
|
||||
public PostSummaryDto toSummaryDto(Post post) {
|
||||
PostSummaryDto dto = new PostSummaryDto();
|
||||
@@ -63,6 +70,8 @@ public class PostMapper {
|
||||
dto.setCommentCount(commentService.countComments(post.getId()));
|
||||
dto.setStatus(post.getStatus());
|
||||
dto.setPinnedAt(post.getPinnedAt());
|
||||
dto.setRssExcluded(post.getRssExcluded() == null || post.getRssExcluded());
|
||||
dto.setClosed(post.isClosed());
|
||||
|
||||
List<ReactionDto> reactions = reactionService.getReactionsForPost(post.getId())
|
||||
.stream()
|
||||
@@ -84,11 +93,26 @@ public class PostMapper {
|
||||
l.setPrizeDescription(lp.getPrizeDescription());
|
||||
l.setPrizeIcon(lp.getPrizeIcon());
|
||||
l.setPrizeCount(lp.getPrizeCount());
|
||||
l.setPointCost(lp.getPointCost());
|
||||
l.setStartTime(lp.getStartTime());
|
||||
l.setEndTime(lp.getEndTime());
|
||||
l.setParticipants(lp.getParticipants().stream().map(userMapper::toAuthorDto).collect(Collectors.toList()));
|
||||
l.setWinners(lp.getWinners().stream().map(userMapper::toAuthorDto).collect(Collectors.toList()));
|
||||
dto.setLottery(l);
|
||||
}
|
||||
|
||||
if (post instanceof PollPost pp) {
|
||||
PollDto p = new PollDto();
|
||||
p.setOptions(pp.getOptions());
|
||||
p.setVotes(pp.getVotes());
|
||||
p.setEndTime(pp.getEndTime());
|
||||
p.setParticipants(pp.getParticipants().stream().map(userMapper::toAuthorDto).collect(Collectors.toList()));
|
||||
Map<Integer, List<AuthorDto>> optionParticipants = pollVoteRepository.findByPostId(pp.getId()).stream()
|
||||
.collect(Collectors.groupingBy(PollVote::getOptionIndex,
|
||||
Collectors.mapping(v -> userMapper.toAuthorDto(v.getUser()), Collectors.toList())));
|
||||
p.setOptionParticipants(optionParticipants);
|
||||
p.setMultiple(Boolean.TRUE.equals(pp.getMultiple()));
|
||||
dto.setPoll(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ public class ReactionMapper {
|
||||
if (reaction.getComment() != null) {
|
||||
dto.setCommentId(reaction.getComment().getId());
|
||||
}
|
||||
if (reaction.getMessage() != null) {
|
||||
dto.setMessageId(reaction.getMessage().getId());
|
||||
}
|
||||
dto.setReward(0);
|
||||
return dto;
|
||||
}
|
||||
|
||||
@@ -3,5 +3,6 @@ package com.openisle.model;
|
||||
/** Activity type enumeration. */
|
||||
public enum ActivityType {
|
||||
NORMAL,
|
||||
MILK_TEA
|
||||
MILK_TEA,
|
||||
INVITE_POINTS
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.SQLDelete;
|
||||
import org.hibernate.annotations.Where;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@@ -13,6 +15,8 @@ import java.time.LocalDateTime;
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@Table(name = "comments")
|
||||
@SQLDelete(sql = "UPDATE comments SET deleted_at = CURRENT_TIMESTAMP(6) WHERE id = ?")
|
||||
@Where(clause = "deleted_at IS NULL")
|
||||
public class Comment {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@@ -38,4 +42,10 @@ public class Comment {
|
||||
@JoinColumn(name = "parent_id")
|
||||
private Comment parent;
|
||||
|
||||
@Column
|
||||
private LocalDateTime pinnedAt;
|
||||
|
||||
@Column(name = "deleted_at")
|
||||
private LocalDateTime deletedAt;
|
||||
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ public class Draft {
|
||||
|
||||
private String title;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
@Column(columnDefinition = "LONGTEXT")
|
||||
private String content;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||
|
||||
30
backend/src/main/java/com/openisle/model/InviteToken.java
Normal file
30
backend/src/main/java/com/openisle/model/InviteToken.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package com.openisle.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* Invite token entity tracking usage counts.
|
||||
*/
|
||||
@Data
|
||||
@Entity
|
||||
public class InviteToken {
|
||||
@Id
|
||||
private String token;
|
||||
|
||||
/**
|
||||
* Short token used in invite links. Existing records may have this field null
|
||||
* and fall back to {@link #token} for backward compatibility.
|
||||
*/
|
||||
@Column(unique = true)
|
||||
private String shortToken;
|
||||
|
||||
@ManyToOne
|
||||
private User inviter;
|
||||
|
||||
private LocalDate createdDate;
|
||||
|
||||
private int usageCount;
|
||||
}
|
||||
@@ -26,6 +26,9 @@ public class LotteryPost extends Post {
|
||||
@Column(nullable = false)
|
||||
private int prizeCount;
|
||||
|
||||
@Column(nullable = false)
|
||||
private int pointCost;
|
||||
|
||||
@Column
|
||||
private LocalDateTime startTime;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.openisle.model;
|
||||
public enum MedalType {
|
||||
COMMENT,
|
||||
POST,
|
||||
FEATURED,
|
||||
CONTRIBUTOR,
|
||||
SEED,
|
||||
PIONEER
|
||||
|
||||
39
backend/src/main/java/com/openisle/model/Message.java
Normal file
39
backend/src/main/java/com/openisle/model/Message.java
Normal file
@@ -0,0 +1,39 @@
|
||||
package com.openisle.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@Table(name = "messages")
|
||||
public class Message {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "conversation_id")
|
||||
private MessageConversation conversation;
|
||||
|
||||
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "sender_id")
|
||||
private User sender;
|
||||
|
||||
@Column(nullable = false, columnDefinition = "TEXT")
|
||||
private String content;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "reply_to_id")
|
||||
private Message replyTo;
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.openisle.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@Table(name = "message_conversations")
|
||||
public class MessageConversation {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
// Indicates whether this conversation represents a public channel
|
||||
@Column(nullable = false)
|
||||
private boolean channel = false;
|
||||
|
||||
// Channel metadata
|
||||
private String name;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String description;
|
||||
|
||||
private String avatar;
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@OneToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "last_message_id")
|
||||
private Message lastMessage;
|
||||
|
||||
@OneToMany(mappedBy = "conversation", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private Set<MessageParticipant> participants = new HashSet<>();
|
||||
|
||||
@OneToMany(mappedBy = "conversation", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private Set<Message> messages = new HashSet<>();
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.openisle.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@Table(name = "message_participants")
|
||||
public class MessageParticipant {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "conversation_id")
|
||||
private MessageConversation conversation;
|
||||
|
||||
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "user_id")
|
||||
private User user;
|
||||
|
||||
@Column
|
||||
private LocalDateTime lastReadAt;
|
||||
}
|
||||
@@ -22,7 +22,7 @@ public class Notification {
|
||||
private Long id;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
@Column(nullable = false, length = 50)
|
||||
private NotificationType type;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||
|
||||
@@ -14,6 +14,8 @@ public enum NotificationType {
|
||||
POST_REVIEW_REQUEST,
|
||||
/** Your post under review was approved or rejected */
|
||||
POST_REVIEWED,
|
||||
/** An administrator deleted your post */
|
||||
POST_DELETED,
|
||||
/** A subscribed post received a new comment */
|
||||
POST_UPDATED,
|
||||
/** Someone subscribed to your post */
|
||||
@@ -32,6 +34,20 @@ public enum NotificationType {
|
||||
REGISTER_REQUEST,
|
||||
/** A user redeemed an activity reward */
|
||||
ACTIVITY_REDEEM,
|
||||
/** A user redeemed a point good */
|
||||
POINT_REDEEM,
|
||||
/** You won a lottery post */
|
||||
LOTTERY_WIN,
|
||||
/** Your lottery post was drawn */
|
||||
LOTTERY_DRAW,
|
||||
/** Someone participated in your poll */
|
||||
POLL_VOTE,
|
||||
/** Your poll post has concluded */
|
||||
POLL_RESULT_OWNER,
|
||||
/** A poll you participated in has concluded */
|
||||
POLL_RESULT_PARTICIPANT,
|
||||
/** Your post was featured */
|
||||
POST_FEATURED,
|
||||
/** You were mentioned in a post or comment */
|
||||
MENTION
|
||||
}
|
||||
|
||||
26
backend/src/main/java/com/openisle/model/PointGood.java
Normal file
26
backend/src/main/java/com/openisle/model/PointGood.java
Normal file
@@ -0,0 +1,26 @@
|
||||
package com.openisle.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/** Item available in the point mall. */
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@Table(name = "point_goods")
|
||||
public class PointGood {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
@Column(nullable = false)
|
||||
private int cost;
|
||||
|
||||
private String image;
|
||||
}
|
||||
56
backend/src/main/java/com/openisle/model/PointHistory.java
Normal file
56
backend/src/main/java/com/openisle/model/PointHistory.java
Normal file
@@ -0,0 +1,56 @@
|
||||
package com.openisle.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.SQLDelete;
|
||||
import org.hibernate.annotations.Where;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** Point change history for a user. */
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@Table(name = "point_histories")
|
||||
@SQLDelete(sql = "UPDATE point_histories SET deleted_at = CURRENT_TIMESTAMP(6) WHERE id = ?")
|
||||
@Where(clause = "deleted_at IS NULL")
|
||||
public class PointHistory {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||
@JoinColumn(name = "user_id")
|
||||
private User user;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
private PointHistoryType type;
|
||||
|
||||
@Column(nullable = false)
|
||||
private int amount;
|
||||
|
||||
@Column(nullable = false)
|
||||
private int balance;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "post_id")
|
||||
private Post post;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "comment_id")
|
||||
private Comment comment;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "from_user_id")
|
||||
private User fromUser;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "deleted_at")
|
||||
private LocalDateTime deletedAt;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.openisle.model;
|
||||
|
||||
public enum PointHistoryType {
|
||||
POST,
|
||||
COMMENT,
|
||||
POST_LIKED,
|
||||
COMMENT_LIKED,
|
||||
POST_LIKE_CANCELLED,
|
||||
COMMENT_LIKE_CANCELLED,
|
||||
INVITE,
|
||||
FEATURE,
|
||||
SYSTEM_ONLINE,
|
||||
REDEEM,
|
||||
LOTTERY_JOIN,
|
||||
LOTTERY_REWARD
|
||||
}
|
||||
43
backend/src/main/java/com/openisle/model/PollPost.java
Normal file
43
backend/src/main/java/com/openisle/model/PollPost.java
Normal file
@@ -0,0 +1,43 @@
|
||||
package com.openisle.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "poll_posts")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@PrimaryKeyJoinColumn(name = "post_id")
|
||||
public class PollPost extends Post {
|
||||
@ElementCollection
|
||||
@CollectionTable(name = "poll_post_options", joinColumns = @JoinColumn(name = "post_id"))
|
||||
@Column(name = "option_text")
|
||||
private List<String> options = new ArrayList<>();
|
||||
|
||||
@ElementCollection
|
||||
@CollectionTable(name = "poll_post_votes", joinColumns = @JoinColumn(name = "post_id"))
|
||||
@MapKeyColumn(name = "option_index")
|
||||
@Column(name = "vote_count")
|
||||
private Map<Integer, Integer> votes = new HashMap<>();
|
||||
|
||||
@ManyToMany
|
||||
@JoinTable(name = "poll_participants",
|
||||
joinColumns = @JoinColumn(name = "post_id"),
|
||||
inverseJoinColumns = @JoinColumn(name = "user_id"))
|
||||
private Set<User> participants = new HashSet<>();
|
||||
|
||||
@Column
|
||||
private Boolean multiple = false;
|
||||
|
||||
@Column
|
||||
private LocalDateTime endTime;
|
||||
|
||||
@Column
|
||||
private boolean resultAnnounced = false;
|
||||
}
|
||||
28
backend/src/main/java/com/openisle/model/PollVote.java
Normal file
28
backend/src/main/java/com/openisle/model/PollVote.java
Normal file
@@ -0,0 +1,28 @@
|
||||
package com.openisle.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@Entity
|
||||
@Table(name = "poll_votes", uniqueConstraints = @UniqueConstraint(columnNames = {"post_id", "user_id", "option_index"}))
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class PollVote {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||
@JoinColumn(name = "post_id")
|
||||
private PollPost post;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||
@JoinColumn(name = "user_id")
|
||||
private User user;
|
||||
|
||||
@Column(name = "option_index", nullable = false)
|
||||
private int optionIndex;
|
||||
}
|
||||
@@ -31,7 +31,7 @@ public class Post {
|
||||
@Column(nullable = false)
|
||||
private String title;
|
||||
|
||||
@Column(nullable = false, columnDefinition = "TEXT")
|
||||
@Column(nullable = false, columnDefinition = "LONGTEXT")
|
||||
private String content;
|
||||
|
||||
@CreationTimestamp
|
||||
@@ -64,7 +64,12 @@ public class Post {
|
||||
@Column(nullable = false)
|
||||
private PostType type = PostType.NORMAL;
|
||||
|
||||
@Column(nullable = false)
|
||||
private boolean closed = false;
|
||||
|
||||
@Column
|
||||
private LocalDateTime pinnedAt;
|
||||
|
||||
@Column(nullable = true)
|
||||
private Boolean rssExcluded = true;
|
||||
}
|
||||
|
||||
@@ -2,5 +2,6 @@ package com.openisle.model;
|
||||
|
||||
public enum PostType {
|
||||
NORMAL,
|
||||
LOTTERY
|
||||
LOTTERY,
|
||||
POLL
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import lombok.Setter;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
|
||||
/**
|
||||
* Reaction entity representing a user's reaction to a post or comment.
|
||||
* Reaction entity representing a user's reaction to a post, comment or message.
|
||||
*/
|
||||
@Entity
|
||||
@Getter
|
||||
@@ -16,7 +16,8 @@ import org.hibernate.annotations.CreationTimestamp;
|
||||
@Table(name = "reactions",
|
||||
uniqueConstraints = {
|
||||
@UniqueConstraint(columnNames = {"user_id", "post_id", "type"}),
|
||||
@UniqueConstraint(columnNames = {"user_id", "comment_id", "type"})
|
||||
@UniqueConstraint(columnNames = {"user_id", "comment_id", "type"}),
|
||||
@UniqueConstraint(columnNames = {"user_id", "message_id", "type"})
|
||||
})
|
||||
public class Reaction {
|
||||
@Id
|
||||
@@ -39,6 +40,10 @@ public class Reaction {
|
||||
@JoinColumn(name = "comment_id")
|
||||
private Comment comment;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "message_id")
|
||||
private Message message;
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(nullable = false, updatable = false,
|
||||
columnDefinition = "DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6)")
|
||||
|
||||
@@ -6,7 +6,9 @@ package com.openisle.model;
|
||||
public enum ReactionType {
|
||||
LIKE,
|
||||
DISLIKE,
|
||||
SMILE,
|
||||
RECOMMEND,
|
||||
CONGRATULATIONS,
|
||||
ANGRY,
|
||||
FLUSHED,
|
||||
STAR_STRUCK,
|
||||
@@ -26,5 +28,5 @@ public enum ReactionType {
|
||||
CHINA,
|
||||
USA,
|
||||
JAPAN,
|
||||
KOREA
|
||||
KOREA,
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import lombok.Setter;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -68,7 +69,16 @@ public class User {
|
||||
@CollectionTable(name = "user_disabled_notification_types", joinColumns = @JoinColumn(name = "user_id"))
|
||||
@Column(name = "notification_type")
|
||||
@Enumerated(EnumType.STRING)
|
||||
private Set<NotificationType> disabledNotificationTypes = new HashSet<>();
|
||||
private Set<NotificationType> disabledNotificationTypes = EnumSet.of(
|
||||
NotificationType.POST_VIEWED,
|
||||
NotificationType.USER_ACTIVITY
|
||||
);
|
||||
|
||||
@ElementCollection(targetClass = NotificationType.class)
|
||||
@CollectionTable(name = "user_disabled_email_notification_types", joinColumns = @JoinColumn(name = "user_id"))
|
||||
@Column(name = "notification_type")
|
||||
@Enumerated(EnumType.STRING)
|
||||
private Set<NotificationType> disabledEmailNotificationTypes = EnumSet.noneOf(NotificationType.class);
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(nullable = false, updatable = false,
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.openisle.repository;
|
||||
|
||||
import com.openisle.model.InviteToken;
|
||||
import com.openisle.model.User;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface InviteTokenRepository extends JpaRepository<InviteToken, String> {
|
||||
Optional<InviteToken> findByInviterAndCreatedDate(User inviter, LocalDate createdDate);
|
||||
|
||||
Optional<InviteToken> findByShortToken(String shortToken);
|
||||
|
||||
boolean existsByShortToken(String shortToken);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.openisle.repository;
|
||||
|
||||
import com.openisle.model.MessageConversation;
|
||||
import com.openisle.model.User;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface MessageConversationRepository extends JpaRepository<MessageConversation, Long> {
|
||||
@Query("SELECT c FROM MessageConversation c " +
|
||||
"WHERE c.channel = false AND size(c.participants) = 2 " +
|
||||
"AND EXISTS (SELECT 1 FROM c.participants p1 WHERE p1.user = :user1) " +
|
||||
"AND EXISTS (SELECT 1 FROM c.participants p2 WHERE p2.user = :user2) " +
|
||||
"ORDER BY c.createdAt DESC")
|
||||
List<MessageConversation> findConversationsByUsers(@Param("user1") User user1, @Param("user2") User user2);
|
||||
|
||||
@Query("SELECT DISTINCT c FROM MessageConversation c " +
|
||||
"JOIN c.participants p " +
|
||||
"LEFT JOIN FETCH c.lastMessage lm " +
|
||||
"LEFT JOIN FETCH lm.sender " +
|
||||
"LEFT JOIN FETCH c.participants cp " +
|
||||
"LEFT JOIN FETCH cp.user " +
|
||||
"WHERE p.user.id = :userId " +
|
||||
"ORDER BY COALESCE(lm.createdAt, c.createdAt) DESC")
|
||||
List<MessageConversation> findConversationsByUserIdOrderByLastMessageDesc(@Param("userId") Long userId);
|
||||
|
||||
List<MessageConversation> findByChannelTrue();
|
||||
|
||||
long countByChannelTrue();
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.openisle.repository;
|
||||
|
||||
import com.openisle.model.MessageParticipant;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface MessageParticipantRepository extends JpaRepository<MessageParticipant, Long> {
|
||||
Optional<MessageParticipant> findByConversationIdAndUserId(Long conversationId, Long userId);
|
||||
List<MessageParticipant> findByUserId(Long userId);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.openisle.repository;
|
||||
|
||||
import com.openisle.model.Message;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface MessageRepository extends JpaRepository<Message, Long> {
|
||||
List<Message> findByConversationIdOrderByCreatedAtAsc(Long conversationId);
|
||||
|
||||
Page<Message> findByConversationId(Long conversationId, Pageable pageable);
|
||||
|
||||
long countByConversationIdAndCreatedAtAfter(Long conversationId, java.time.LocalDateTime createdAt);
|
||||
|
||||
// 只计算不是指定用户发送的消息(即别人发给当前用户的消息)
|
||||
long countByConversationIdAndCreatedAtAfterAndSenderIdNot(Long conversationId, java.time.LocalDateTime createdAt, Long senderId);
|
||||
}
|
||||
@@ -5,7 +5,10 @@ import com.openisle.model.User;
|
||||
import com.openisle.model.Post;
|
||||
import com.openisle.model.Comment;
|
||||
import com.openisle.model.NotificationType;
|
||||
import com.openisle.model.ReactionType;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -13,7 +16,12 @@ import java.util.List;
|
||||
public interface NotificationRepository extends JpaRepository<Notification, Long> {
|
||||
List<Notification> findByUserOrderByCreatedAtDesc(User user);
|
||||
List<Notification> findByUserAndReadOrderByCreatedAtDesc(User user, boolean read);
|
||||
Page<Notification> findByUserOrderByCreatedAtDesc(User user, Pageable pageable);
|
||||
Page<Notification> findByUserAndReadOrderByCreatedAtDesc(User user, boolean read, Pageable pageable);
|
||||
Page<Notification> findByUserAndTypeNotInOrderByCreatedAtDesc(User user, java.util.Collection<NotificationType> types, Pageable pageable);
|
||||
Page<Notification> findByUserAndReadAndTypeNotInOrderByCreatedAtDesc(User user, boolean read, java.util.Collection<NotificationType> types, Pageable pageable);
|
||||
long countByUserAndRead(User user, boolean read);
|
||||
long countByUserAndReadAndTypeNotIn(User user, boolean read, java.util.Collection<NotificationType> types);
|
||||
List<Notification> findByPost(Post post);
|
||||
List<Notification> findByComment(Comment comment);
|
||||
|
||||
@@ -22,4 +30,8 @@ public interface NotificationRepository extends JpaRepository<Notification, Long
|
||||
List<Notification> findByTypeAndFromUser(NotificationType type, User fromUser);
|
||||
|
||||
void deleteByTypeAndFromUserAndPost(NotificationType type, User fromUser, Post post);
|
||||
|
||||
void deleteByTypeAndFromUserAndPostAndReactionType(NotificationType type, User fromUser, Post post, ReactionType reactionType);
|
||||
|
||||
void deleteByTypeAndFromUserAndCommentAndReactionType(NotificationType type, User fromUser, Comment comment, ReactionType reactionType);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.openisle.repository;
|
||||
|
||||
import com.openisle.model.PointGood;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
/** Repository for point mall goods. */
|
||||
public interface PointGoodRepository extends JpaRepository<PointGood, Long> {
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.openisle.repository;
|
||||
|
||||
import com.openisle.model.PointHistory;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.model.Comment;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public interface PointHistoryRepository extends JpaRepository<PointHistory, Long> {
|
||||
List<PointHistory> findByUserOrderByIdDesc(User user);
|
||||
long countByUser(User user);
|
||||
|
||||
List<PointHistory> findByUserAndCreatedAtAfterOrderByCreatedAtDesc(User user, LocalDateTime createdAt);
|
||||
|
||||
List<PointHistory> findByComment(Comment comment);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.openisle.repository;
|
||||
|
||||
import com.openisle.model.PollPost;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public interface PollPostRepository extends JpaRepository<PollPost, Long> {
|
||||
List<PollPost> findByEndTimeAfterAndResultAnnouncedFalse(LocalDateTime now);
|
||||
|
||||
List<PollPost> findByEndTimeBeforeAndResultAnnouncedFalse(LocalDateTime now);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.openisle.repository;
|
||||
|
||||
import com.openisle.model.PollVote;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface PollVoteRepository extends JpaRepository<PollVote, Long> {
|
||||
List<PollVote> findByPostId(Long postId);
|
||||
}
|
||||
@@ -92,12 +92,22 @@ public interface PostRepository extends JpaRepository<Post, Long> {
|
||||
|
||||
long countByCategory_Id(Long categoryId);
|
||||
|
||||
@Query("SELECT c.id, COUNT(p) FROM Post p JOIN p.category c WHERE c.id IN :categoryIds GROUP BY c.id")
|
||||
List<Object[]> countPostsByCategoryIds(@Param("categoryIds") List<Long> categoryIds);
|
||||
|
||||
long countDistinctByTags_Id(Long tagId);
|
||||
|
||||
long countByAuthor_IdAndRssExcludedFalse(Long userId);
|
||||
|
||||
@Query("SELECT t.id, COUNT(DISTINCT p) FROM Post p JOIN p.tags t WHERE t.id IN :tagIds GROUP BY t.id")
|
||||
List<Object[]> countPostsByTagIds(@Param("tagIds") List<Long> tagIds);
|
||||
|
||||
long countByAuthor_Id(Long userId);
|
||||
|
||||
@Query("SELECT FUNCTION('date', p.createdAt) AS d, COUNT(p) AS c FROM Post p " +
|
||||
"WHERE p.createdAt >= :start AND p.createdAt < :end GROUP BY d ORDER BY d")
|
||||
java.util.List<Object[]> countDailyRange(@Param("start") LocalDateTime start,
|
||||
@Param("end") LocalDateTime end);
|
||||
|
||||
List<Post> findByStatusAndRssExcludedFalseOrderByCreatedAtDesc(PostStatus status, Pageable pageable);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.openisle.repository;
|
||||
|
||||
import com.openisle.model.Comment;
|
||||
import com.openisle.model.Message;
|
||||
import com.openisle.model.Post;
|
||||
import com.openisle.model.Reaction;
|
||||
import com.openisle.model.User;
|
||||
@@ -15,8 +16,10 @@ import java.util.Optional;
|
||||
public interface ReactionRepository extends JpaRepository<Reaction, Long> {
|
||||
Optional<Reaction> findByUserAndPostAndType(User user, Post post, com.openisle.model.ReactionType type);
|
||||
Optional<Reaction> findByUserAndCommentAndType(User user, Comment comment, com.openisle.model.ReactionType type);
|
||||
Optional<Reaction> findByUserAndMessageAndType(User user, Message message, com.openisle.model.ReactionType type);
|
||||
List<Reaction> findByPost(Post post);
|
||||
List<Reaction> findByComment(Comment comment);
|
||||
List<Reaction> findByMessage(Message message);
|
||||
|
||||
@Query("SELECT r.post.id FROM Reaction r WHERE r.post IS NOT NULL AND r.post.author.username = :username AND r.type = com.openisle.model.ReactionType.LIKE GROUP BY r.post.id ORDER BY COUNT(r.id) DESC")
|
||||
List<Long> findTopPostIds(@Param("username") String username, Pageable pageable);
|
||||
|
||||
12
backend/src/main/java/com/openisle/service/AuthResult.java
Normal file
12
backend/src/main/java/com/openisle/service/AuthResult.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.openisle.service;
|
||||
|
||||
import com.openisle.model.User;
|
||||
import lombok.Value;
|
||||
|
||||
/** Result for OAuth authentication indicating whether a new user was created. */
|
||||
@Value
|
||||
public class AuthResult {
|
||||
User user;
|
||||
boolean newUser;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.openisle.service;
|
||||
|
||||
import com.openisle.dto.ChannelDto;
|
||||
import com.openisle.dto.MessageDto;
|
||||
import com.openisle.dto.UserSummaryDto;
|
||||
import com.openisle.model.Message;
|
||||
import com.openisle.model.MessageConversation;
|
||||
import com.openisle.model.MessageParticipant;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.repository.MessageConversationRepository;
|
||||
import com.openisle.repository.MessageParticipantRepository;
|
||||
import com.openisle.repository.MessageRepository;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ChannelService {
|
||||
private final MessageConversationRepository conversationRepository;
|
||||
private final MessageParticipantRepository participantRepository;
|
||||
private final MessageRepository messageRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<ChannelDto> listChannels(Long userId) {
|
||||
List<MessageConversation> channels = conversationRepository.findByChannelTrue();
|
||||
return channels.stream().map(c -> toDto(c, userId)).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ChannelDto joinChannel(Long channelId, Long userId) {
|
||||
MessageConversation channel = conversationRepository.findById(channelId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Channel not found"));
|
||||
User user = userRepository.findById(userId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("User not found"));
|
||||
participantRepository.findByConversationIdAndUserId(channelId, userId)
|
||||
.orElseGet(() -> {
|
||||
MessageParticipant p = new MessageParticipant();
|
||||
p.setConversation(channel);
|
||||
p.setUser(user);
|
||||
MessageParticipant saved = participantRepository.save(p);
|
||||
channel.getParticipants().add(saved);
|
||||
return saved;
|
||||
});
|
||||
return toDto(channel, userId);
|
||||
}
|
||||
|
||||
private ChannelDto toDto(MessageConversation channel, Long userId) {
|
||||
ChannelDto dto = new ChannelDto();
|
||||
dto.setId(channel.getId());
|
||||
dto.setName(channel.getName());
|
||||
dto.setDescription(channel.getDescription());
|
||||
dto.setAvatar(channel.getAvatar());
|
||||
if (channel.getLastMessage() != null) {
|
||||
dto.setLastMessage(toMessageDto(channel.getLastMessage()));
|
||||
}
|
||||
dto.setMemberCount(channel.getParticipants().size());
|
||||
boolean joined = channel.getParticipants().stream()
|
||||
.anyMatch(p -> p.getUser().getId().equals(userId));
|
||||
dto.setJoined(joined);
|
||||
if (joined) {
|
||||
MessageParticipant participant = channel.getParticipants().stream()
|
||||
.filter(p -> p.getUser().getId().equals(userId))
|
||||
.findFirst().orElse(null);
|
||||
LocalDateTime lastRead = participant.getLastReadAt() == null
|
||||
? LocalDateTime.of(1970, 1, 1, 0, 0)
|
||||
: participant.getLastReadAt();
|
||||
long unread = messageRepository
|
||||
.countByConversationIdAndCreatedAtAfterAndSenderIdNot(channel.getId(), lastRead, userId);
|
||||
dto.setUnreadCount(unread);
|
||||
} else {
|
||||
dto.setUnreadCount(0);
|
||||
}
|
||||
return dto;
|
||||
}
|
||||
|
||||
private MessageDto toMessageDto(Message message) {
|
||||
MessageDto dto = new MessageDto();
|
||||
dto.setId(message.getId());
|
||||
dto.setContent(message.getContent());
|
||||
dto.setConversationId(message.getConversation().getId());
|
||||
dto.setCreatedAt(message.getCreatedAt());
|
||||
|
||||
UserSummaryDto userDto = new UserSummaryDto();
|
||||
userDto.setId(message.getSender().getId());
|
||||
userDto.setUsername(message.getSender().getUsername());
|
||||
userDto.setAvatar(message.getSender().getAvatar());
|
||||
dto.setSender(userDto);
|
||||
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import com.openisle.model.Comment;
|
||||
import com.openisle.model.Post;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.model.NotificationType;
|
||||
import com.openisle.model.PointHistory;
|
||||
import com.openisle.model.CommentSort;
|
||||
import com.openisle.repository.CommentRepository;
|
||||
import com.openisle.repository.PostRepository;
|
||||
@@ -11,8 +12,10 @@ import com.openisle.repository.UserRepository;
|
||||
import com.openisle.repository.ReactionRepository;
|
||||
import com.openisle.repository.CommentSubscriptionRepository;
|
||||
import com.openisle.repository.NotificationRepository;
|
||||
import com.openisle.repository.PointHistoryRepository;
|
||||
import com.openisle.service.NotificationService;
|
||||
import com.openisle.service.SubscriptionService;
|
||||
import com.openisle.service.PointService;
|
||||
import com.openisle.model.Role;
|
||||
import com.openisle.exception.RateLimitException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -20,9 +23,13 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.HashSet;
|
||||
import java.util.stream.Collectors;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -36,6 +43,8 @@ public class CommentService {
|
||||
private final ReactionRepository reactionRepository;
|
||||
private final CommentSubscriptionRepository commentSubscriptionRepository;
|
||||
private final NotificationRepository notificationRepository;
|
||||
private final PointHistoryRepository pointHistoryRepository;
|
||||
private final PointService pointService;
|
||||
private final ImageUploader imageUploader;
|
||||
|
||||
@Transactional
|
||||
@@ -51,6 +60,9 @@ public class CommentService {
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Post post = postRepository.findById(postId)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
||||
if (post.isClosed()) {
|
||||
throw new IllegalStateException("Post closed");
|
||||
}
|
||||
Comment comment = new Comment();
|
||||
comment.setAuthor(author);
|
||||
comment.setPost(post);
|
||||
@@ -59,16 +71,19 @@ public class CommentService {
|
||||
log.debug("Comment {} saved for post {}", comment.getId(), postId);
|
||||
imageUploader.addReferences(imageUploader.extractUrls(content));
|
||||
if (!author.getId().equals(post.getAuthor().getId())) {
|
||||
notificationService.createNotification(post.getAuthor(), NotificationType.COMMENT_REPLY, post, comment, null, null, null, null);
|
||||
notificationService.createNotification(post.getAuthor(), NotificationType.COMMENT_REPLY, post, comment,
|
||||
null, null, null, null);
|
||||
}
|
||||
for (User u : subscriptionService.getPostSubscribers(postId)) {
|
||||
if (!u.getId().equals(author.getId())) {
|
||||
notificationService.createNotification(u, NotificationType.POST_UPDATED, post, comment, null, null, null, null);
|
||||
notificationService.createNotification(u, NotificationType.POST_UPDATED, post, comment, null, null,
|
||||
null, null);
|
||||
}
|
||||
}
|
||||
for (User u : subscriptionService.getSubscribers(author.getUsername())) {
|
||||
if (!u.getId().equals(author.getId())) {
|
||||
notificationService.createNotification(u, NotificationType.USER_ACTIVITY, post, comment, null, null, null, null);
|
||||
notificationService.createNotification(u, NotificationType.USER_ACTIVITY, post, comment, null, null,
|
||||
null, null);
|
||||
}
|
||||
}
|
||||
notificationService.notifyMentions(content, author, post, comment);
|
||||
@@ -93,6 +108,9 @@ public class CommentService {
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
Comment parent = commentRepository.findById(parentId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Comment not found"));
|
||||
if (parent.getPost().isClosed()) {
|
||||
throw new IllegalStateException("Post closed");
|
||||
}
|
||||
Comment comment = new Comment();
|
||||
comment.setAuthor(author);
|
||||
comment.setPost(parent.getPost());
|
||||
@@ -102,21 +120,25 @@ public class CommentService {
|
||||
log.debug("Reply {} saved for parent {}", comment.getId(), parentId);
|
||||
imageUploader.addReferences(imageUploader.extractUrls(content));
|
||||
if (!author.getId().equals(parent.getAuthor().getId())) {
|
||||
notificationService.createNotification(parent.getAuthor(), NotificationType.COMMENT_REPLY, parent.getPost(), comment, null, null, null, null);
|
||||
notificationService.createNotification(parent.getAuthor(), NotificationType.COMMENT_REPLY, parent.getPost(),
|
||||
comment, null, null, null, null);
|
||||
}
|
||||
for (User u : subscriptionService.getCommentSubscribers(parentId)) {
|
||||
if (!u.getId().equals(author.getId())) {
|
||||
notificationService.createNotification(u, NotificationType.COMMENT_REPLY, parent.getPost(), comment, null, null, null, null);
|
||||
notificationService.createNotification(u, NotificationType.COMMENT_REPLY, parent.getPost(), comment,
|
||||
null, null, null, null);
|
||||
}
|
||||
}
|
||||
for (User u : subscriptionService.getPostSubscribers(parent.getPost().getId())) {
|
||||
if (!u.getId().equals(author.getId())) {
|
||||
notificationService.createNotification(u, NotificationType.POST_UPDATED, parent.getPost(), comment, null, null, null, null);
|
||||
notificationService.createNotification(u, NotificationType.POST_UPDATED, parent.getPost(), comment,
|
||||
null, null, null, null);
|
||||
}
|
||||
}
|
||||
for (User u : subscriptionService.getSubscribers(author.getUsername())) {
|
||||
if (!u.getId().equals(author.getId())) {
|
||||
notificationService.createNotification(u, NotificationType.USER_ACTIVITY, parent.getPost(), comment, null, null, null, null);
|
||||
notificationService.createNotification(u, NotificationType.USER_ACTIVITY, parent.getPost(), comment,
|
||||
null, null, null, null);
|
||||
}
|
||||
}
|
||||
notificationService.notifyMentions(content, author, parent.getPost(), comment);
|
||||
@@ -129,13 +151,26 @@ public class CommentService {
|
||||
Post post = postRepository.findById(postId)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
||||
List<Comment> list = commentRepository.findByPostAndParentIsNullOrderByCreatedAtAsc(post);
|
||||
if (sort == CommentSort.NEWEST) {
|
||||
list.sort(java.util.Comparator.comparing(Comment::getCreatedAt).reversed());
|
||||
} else if (sort == CommentSort.MOST_INTERACTIONS) {
|
||||
list.sort((a, b) -> Integer.compare(interactionCount(b), interactionCount(a)));
|
||||
java.util.List<Comment> pinned = new java.util.ArrayList<>();
|
||||
java.util.List<Comment> others = new java.util.ArrayList<>();
|
||||
for (Comment c : list) {
|
||||
if (c.getPinnedAt() != null) {
|
||||
pinned.add(c);
|
||||
} else {
|
||||
others.add(c);
|
||||
}
|
||||
}
|
||||
log.debug("getCommentsForPost returning {} comments", list.size());
|
||||
return list;
|
||||
pinned.sort(java.util.Comparator.comparing(Comment::getPinnedAt).reversed());
|
||||
if (sort == CommentSort.NEWEST) {
|
||||
others.sort(java.util.Comparator.comparing(Comment::getCreatedAt).reversed());
|
||||
} else if (sort == CommentSort.MOST_INTERACTIONS) {
|
||||
others.sort((a, b) -> Integer.compare(interactionCount(b), interactionCount(a)));
|
||||
}
|
||||
java.util.List<Comment> result = new java.util.ArrayList<>();
|
||||
result.addAll(pinned);
|
||||
result.addAll(others);
|
||||
log.debug("getCommentsForPost returning {} comments", result.size());
|
||||
return result;
|
||||
}
|
||||
|
||||
public List<Comment> getReplies(Long parentId) {
|
||||
@@ -215,14 +250,62 @@ public class CommentService {
|
||||
for (Comment c : replies) {
|
||||
deleteCommentCascade(c);
|
||||
}
|
||||
|
||||
// 逻辑删除相关的积分历史记录,并收集受影响的用户
|
||||
List<PointHistory> pointHistories = pointHistoryRepository.findByComment(comment);
|
||||
// 收集需要重新计算积分的用户
|
||||
Set<User> usersToRecalculate = pointHistories.stream().map(PointHistory::getUser).collect(Collectors.toSet());
|
||||
|
||||
// 删除其他相关数据
|
||||
reactionRepository.findByComment(comment).forEach(reactionRepository::delete);
|
||||
commentSubscriptionRepository.findByComment(comment).forEach(commentSubscriptionRepository::delete);
|
||||
notificationRepository.deleteAll(notificationRepository.findByComment(comment));
|
||||
imageUploader.removeReferences(imageUploader.extractUrls(comment.getContent()));
|
||||
|
||||
// 逻辑删除评论
|
||||
commentRepository.delete(comment);
|
||||
// 删除积分历史
|
||||
pointHistoryRepository.deleteAll(pointHistories);
|
||||
|
||||
// 重新计算受影响用户的积分
|
||||
if (!usersToRecalculate.isEmpty()) {
|
||||
for (User user : usersToRecalculate) {
|
||||
int newPoints = pointService.recalculateUserPoints(user);
|
||||
user.setPoint(newPoints);
|
||||
log.debug("Recalculated points for user {}: {}", user.getUsername(), newPoints);
|
||||
}
|
||||
userRepository.saveAll(usersToRecalculate);
|
||||
}
|
||||
|
||||
log.debug("deleteCommentCascade removed comment {}", comment.getId());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Comment pinComment(String username, Long id) {
|
||||
Comment c = commentRepository.findById(id)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Comment not found"));
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
if (!user.getId().equals(c.getPost().getAuthor().getId()) && user.getRole() != Role.ADMIN) {
|
||||
throw new IllegalArgumentException("Unauthorized");
|
||||
}
|
||||
c.setPinnedAt(LocalDateTime.now());
|
||||
return commentRepository.save(c);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Comment unpinComment(String username, Long id) {
|
||||
Comment c = commentRepository.findById(id)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Comment not found"));
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
if (!user.getId().equals(c.getPost().getAuthor().getId()) && user.getRole() != Role.ADMIN) {
|
||||
throw new IllegalArgumentException("Unauthorized");
|
||||
}
|
||||
c.setPinnedAt(null);
|
||||
return commentRepository.save(c);
|
||||
}
|
||||
|
||||
private int interactionCount(Comment comment) {
|
||||
int reactions = reactionRepository.findByComment(comment).size();
|
||||
int replies = commentRepository.findByParentOrderByCreatedAtAsc(comment).size();
|
||||
|
||||
@@ -26,7 +26,7 @@ public class DiscordAuthService {
|
||||
@Value("${discord.client-secret:}")
|
||||
private String clientSecret;
|
||||
|
||||
public Optional<User> authenticate(String code, com.openisle.model.RegisterMode mode, String redirectUri) {
|
||||
public Optional<AuthResult> authenticate(String code, com.openisle.model.RegisterMode mode, String redirectUri, boolean viaInvite) {
|
||||
try {
|
||||
String tokenUrl = "https://discord.com/api/oauth2/token";
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
@@ -67,13 +67,13 @@ public class DiscordAuthService {
|
||||
if (email == null) {
|
||||
email = (username != null ? username : id) + "@users.noreply.discord.com";
|
||||
}
|
||||
return Optional.of(processUser(email, username, avatar, mode));
|
||||
return Optional.of(processUser(email, username, avatar, mode, viaInvite));
|
||||
} catch (Exception e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private User processUser(String email, String username, String avatar, com.openisle.model.RegisterMode mode) {
|
||||
private AuthResult processUser(String email, String username, String avatar, com.openisle.model.RegisterMode mode, boolean viaInvite) {
|
||||
Optional<User> existing = userRepository.findByEmail(email);
|
||||
if (existing.isPresent()) {
|
||||
User user = existing.get();
|
||||
@@ -82,7 +82,7 @@ public class DiscordAuthService {
|
||||
user.setVerificationCode(null);
|
||||
userRepository.save(user);
|
||||
}
|
||||
return user;
|
||||
return new AuthResult(user, false);
|
||||
}
|
||||
String baseUsername = username != null ? username : email.split("@")[0];
|
||||
String finalUsername = baseUsername;
|
||||
@@ -96,12 +96,12 @@ public class DiscordAuthService {
|
||||
user.setPassword("");
|
||||
user.setRole(Role.USER);
|
||||
user.setVerified(true);
|
||||
user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
|
||||
user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT || viaInvite);
|
||||
if (avatar != null) {
|
||||
user.setAvatar(avatar);
|
||||
} else {
|
||||
user.setAvatar("https://cdn.discordapp.com/embed/avatars/0.png");
|
||||
}
|
||||
return userRepository.save(user);
|
||||
return new AuthResult(userRepository.save(user), true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ public class GithubAuthService {
|
||||
@Value("${github.client-secret:}")
|
||||
private String clientSecret;
|
||||
|
||||
public Optional<User> authenticate(String code, com.openisle.model.RegisterMode mode, String redirectUri) {
|
||||
public Optional<AuthResult> authenticate(String code, com.openisle.model.RegisterMode mode, String redirectUri, boolean viaInvite) {
|
||||
try {
|
||||
String tokenUrl = "https://github.com/login/oauth/access_token";
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
@@ -86,13 +86,13 @@ public class GithubAuthService {
|
||||
if (email == null) {
|
||||
email = username + "@users.noreply.github.com";
|
||||
}
|
||||
return Optional.of(processUser(email, username, avatarUrl, mode));
|
||||
return Optional.of(processUser(email, username, avatarUrl, mode, viaInvite));
|
||||
} catch (Exception e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private User processUser(String email, String username, String avatar, com.openisle.model.RegisterMode mode) {
|
||||
private AuthResult processUser(String email, String username, String avatar, com.openisle.model.RegisterMode mode, boolean viaInvite) {
|
||||
Optional<User> existing = userRepository.findByEmail(email);
|
||||
if (existing.isPresent()) {
|
||||
User user = existing.get();
|
||||
@@ -101,7 +101,7 @@ public class GithubAuthService {
|
||||
user.setVerificationCode(null);
|
||||
userRepository.save(user);
|
||||
}
|
||||
return user;
|
||||
return new AuthResult(user, false);
|
||||
}
|
||||
String baseUsername = username != null ? username : email.split("@")[0];
|
||||
String finalUsername = baseUsername;
|
||||
@@ -115,12 +115,12 @@ public class GithubAuthService {
|
||||
user.setPassword("");
|
||||
user.setRole(Role.USER);
|
||||
user.setVerified(true);
|
||||
user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
|
||||
user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT || viaInvite);
|
||||
if (avatar != null) {
|
||||
user.setAvatar(avatar);
|
||||
} else {
|
||||
user.setAvatar(avatarGenerator.generate(finalUsername));
|
||||
}
|
||||
return userRepository.save(user);
|
||||
return new AuthResult(userRepository.save(user), true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ public class GoogleAuthService {
|
||||
@Value("${google.client-id:}")
|
||||
private String clientId;
|
||||
|
||||
public Optional<User> authenticate(String idTokenString, com.openisle.model.RegisterMode mode) {
|
||||
public Optional<AuthResult> authenticate(String idTokenString, com.openisle.model.RegisterMode mode, boolean viaInvite) {
|
||||
GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(new NetHttpTransport(), new JacksonFactory())
|
||||
.setAudience(Collections.singletonList(clientId))
|
||||
.build();
|
||||
@@ -38,13 +38,13 @@ public class GoogleAuthService {
|
||||
String email = payload.getEmail();
|
||||
String name = (String) payload.get("name");
|
||||
String picture = (String) payload.get("picture");
|
||||
return Optional.of(processUser(email, name, picture, mode));
|
||||
return Optional.of(processUser(email, name, picture, mode, viaInvite));
|
||||
} catch (Exception e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private User processUser(String email, String name, String avatar, com.openisle.model.RegisterMode mode) {
|
||||
private AuthResult processUser(String email, String name, String avatar, com.openisle.model.RegisterMode mode, boolean viaInvite) {
|
||||
Optional<User> existing = userRepository.findByEmail(email);
|
||||
if (existing.isPresent()) {
|
||||
User user = existing.get();
|
||||
@@ -53,8 +53,7 @@ public class GoogleAuthService {
|
||||
user.setVerificationCode(null);
|
||||
userRepository.save(user);
|
||||
}
|
||||
|
||||
return user;
|
||||
return new AuthResult(user, false);
|
||||
}
|
||||
User user = new User();
|
||||
String baseUsername = email.split("@")[0];
|
||||
@@ -68,12 +67,12 @@ public class GoogleAuthService {
|
||||
user.setPassword("");
|
||||
user.setRole(Role.USER);
|
||||
user.setVerified(true);
|
||||
user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
|
||||
user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT || viaInvite);
|
||||
if (avatar != null) {
|
||||
user.setAvatar(avatar);
|
||||
} else {
|
||||
user.setAvatar(avatarGenerator.generate(username));
|
||||
}
|
||||
return userRepository.save(user);
|
||||
return new AuthResult(userRepository.save(user), true);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user