mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-07 23:51:17 +08:00
Compare commits
342 Commits
codex/remo
...
codex/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83bf8c1d5e | ||
|
|
34e206f05d | ||
|
|
dc349923e9 | ||
|
|
0d44c9a823 | ||
|
|
02645af321 | ||
|
|
c3a175f13f | ||
|
|
0821d447f7 | ||
|
|
257794ca00 | ||
|
|
6a527de3eb | ||
|
|
2313f90eb3 | ||
|
|
7fde984e7d | ||
|
|
fc41e605e4 | ||
|
|
042e5fdbe6 | ||
|
|
629442bff6 | ||
|
|
7798910be0 | ||
|
|
6f036eb4fe | ||
|
|
56fc05cb3c | ||
|
|
a55a15659b | ||
|
|
ccf6e0c7ce | ||
|
|
87677f5968 | ||
|
|
fd93a2dc61 | ||
|
|
80f862a226 | ||
|
|
26bb85f4d4 | ||
|
|
398b4b482f | ||
|
|
2cfb302981 | ||
|
|
e75bd76b71 | ||
|
|
99c3ac1837 | ||
|
|
749ab560ff | ||
|
|
541ad4d149 | ||
|
|
03eb027ea4 | ||
|
|
4194b2be91 | ||
|
|
9dadaad5ba | ||
|
|
d4b3400c5f | ||
|
|
e585100625 | ||
|
|
e94471b53e | ||
|
|
997dacdbe6 | ||
|
|
c01349a436 | ||
|
|
4cf48f9157 | ||
|
|
796afbe612 | ||
|
|
dca14390ca | ||
|
|
39875acd35 | ||
|
|
62edc75735 | ||
|
|
26ca9fc916 | ||
|
|
cad70c23b3 | ||
|
|
016276dbc3 | ||
|
|
bd2d6e7485 | ||
|
|
df59a9fd4b | ||
|
|
2e70a3d273 | ||
|
|
3dc6935d19 | ||
|
|
779bb2db78 | ||
|
|
b3b0b194a3 | ||
|
|
e21b2f42d2 | ||
|
|
05a5acee7e | ||
|
|
755982098b | ||
|
|
af24263c0a | ||
|
|
8fd268bd11 | ||
|
|
a24bd81942 | ||
|
|
8a008a090a | ||
|
|
5dfb69e636 | ||
|
|
499069573e | ||
|
|
636912941a | ||
|
|
bdcc1488b9 | ||
|
|
d33bd233af | ||
|
|
efe4b97d83 | ||
|
|
8a256e167d | ||
|
|
9c5a49a47f | ||
|
|
2271bbbd1d | ||
|
|
d6470e04fc | ||
|
|
4db35a4531 | ||
|
|
1906ffd8aa | ||
|
|
426884385f | ||
|
|
8193c92c91 | ||
|
|
90649b422d | ||
|
|
67efb64ccc | ||
|
|
23d8eafc08 | ||
|
|
d1cc16e31e | ||
|
|
0f1c45b155 | ||
|
|
8ed11df99c | ||
|
|
458b125834 | ||
|
|
971a3d36c6 | ||
|
|
e5d66d73cb | ||
|
|
a9608cc706 | ||
|
|
232f40151b | ||
|
|
3b3f99754d | ||
|
|
e14566ee66 | ||
|
|
892312c6d4 | ||
|
|
dfb31771ff | ||
|
|
bf7df629cc | ||
|
|
f17b644a9b | ||
|
|
61f8fa4bb7 | ||
|
|
43929bcdc5 | ||
|
|
6aecb4f583 | ||
|
|
0d2e6a9505 | ||
|
|
b2d70b9bde | ||
|
|
d914579d64 | ||
|
|
8643446d8b | ||
|
|
2db958f8c9 | ||
|
|
fa29d255c9 | ||
|
|
b3fa5e2bef | ||
|
|
a7ef4380d8 | ||
|
|
39d954d98a | ||
|
|
596d1558a2 | ||
|
|
ce04570efb | ||
|
|
215c7077d5 | ||
|
|
a68c925c68 | ||
|
|
4f248e8a71 | ||
|
|
277883f9d9 | ||
|
|
e9e996f291 | ||
|
|
a8667ce5e9 | ||
|
|
0d316af22a | ||
|
|
f8e13af672 | ||
|
|
92d90c997c | ||
|
|
303ec9b6c1 | ||
|
|
90eafe27fd | ||
|
|
98e2ea7ef8 | ||
|
|
e3290f3431 | ||
|
|
160570574c | ||
|
|
cf7b667f30 | ||
|
|
60fa6051b7 | ||
|
|
1c0e90d32d | ||
|
|
a15065575d | ||
|
|
cb958e162e | ||
|
|
660d8ffe51 | ||
|
|
5509a1eead | ||
|
|
1acd776d3b | ||
|
|
53be8d943a | ||
|
|
9957042746 | ||
|
|
302f98f44e | ||
|
|
790c4db8ea | ||
|
|
bbb0a11d49 | ||
|
|
35340319c6 | ||
|
|
343c4d3793 | ||
|
|
87b214cbc0 | ||
|
|
e7f06787d2 | ||
|
|
d7d2fd5dcb | ||
|
|
76b65a1400 | ||
|
|
fa8ee113a2 | ||
|
|
181237adee | ||
|
|
1b8135acfb | ||
|
|
67bbe832a0 | ||
|
|
9d67f7d8d6 | ||
|
|
da0d26c8b5 | ||
|
|
81d64bfc7b | ||
|
|
3e255c1288 | ||
|
|
224e1a1018 | ||
|
|
4456997573 | ||
|
|
ef0f0d013b | ||
|
|
a83ddc40fe | ||
|
|
f36ed28185 | ||
|
|
1d31284dba | ||
|
|
995d68b50b | ||
|
|
55b680ef83 | ||
|
|
024e52b763 | ||
|
|
536979501e | ||
|
|
85a67a6215 | ||
|
|
57a9a98da6 | ||
|
|
e8976a98d4 | ||
|
|
57e6bcaa0c | ||
|
|
c95b2ebdc2 | ||
|
|
83cf7439c9 | ||
|
|
994f4028fc | ||
|
|
2362458024 | ||
|
|
03c92d4861 | ||
|
|
8df566a9c9 | ||
|
|
870d1e2940 | ||
|
|
0033374481 | ||
|
|
8f36422609 | ||
|
|
b98871bed9 | ||
|
|
2cb8c12f65 | ||
|
|
87a256ba0c | ||
|
|
737157e557 | ||
|
|
6f9570dc95 | ||
|
|
12bc405856 | ||
|
|
a2b0cd1a47 | ||
|
|
25a7f1e138 | ||
|
|
a6dd2bfbc2 | ||
|
|
a0ea63700f | ||
|
|
b49e20d010 | ||
|
|
e44443a605 | ||
|
|
0a3bfb9451 | ||
|
|
adfc05b9b2 | ||
|
|
18a6953ff7 | ||
|
|
181ac7bc8f | ||
|
|
9dc9ca9bd8 | ||
|
|
2457efd11d | ||
|
|
b62b9c691f | ||
|
|
180c45bf2d | ||
|
|
263f2deeb1 | ||
|
|
22b813e40b | ||
|
|
d00dbbbd03 | ||
|
|
3b92bdaf2a | ||
|
|
7ce5de7f7c | ||
|
|
28618c7452 | ||
|
|
f8a2ee6ee9 | ||
|
|
ca26b931da | ||
|
|
24fe90cfc6 | ||
|
|
5971700e8a | ||
|
|
f872a32410 | ||
|
|
fffd335ebb | ||
|
|
287d52df10 | ||
|
|
73790d1992 | ||
|
|
3d5cee6e68 | ||
|
|
2f509cc2d8 | ||
|
|
35c503eb6c | ||
|
|
0cf8113691 | ||
|
|
b2a29913aa | ||
|
|
2b6d7c5ab9 | ||
|
|
e9878487e8 | ||
|
|
201af061e4 | ||
|
|
4080f60f60 | ||
|
|
06d76438e8 | ||
|
|
bb955c98ba | ||
|
|
a12368602d | ||
|
|
208c875868 | ||
|
|
39ae8c02cb | ||
|
|
c9854e1840 | ||
|
|
0119605649 | ||
|
|
0d7dc93a67 | ||
|
|
774611f3a8 | ||
|
|
04616a30f3 | ||
|
|
c0ca615439 | ||
|
|
b0597d34b6 | ||
|
|
e3f680ad0f | ||
|
|
c8a1e6d8c8 | ||
|
|
ffebeb46b7 | ||
|
|
2977d2898f | ||
|
|
8869121bcb | ||
|
|
61f6e7c90a | ||
|
|
892aa6a7c6 | ||
|
|
23cc2d1606 | ||
|
|
44addd2a7b | ||
|
|
0bc65077df | ||
|
|
69869348f6 | ||
|
|
4821b77c17 | ||
|
|
3da5d24488 | ||
|
|
76962d6d1c | ||
|
|
4fc7c861ee | ||
|
|
81dfddf6e1 | ||
|
|
8b93aa95cf | ||
|
|
425fc7d2b1 | ||
|
|
0fff73b682 | ||
|
|
e1171212d7 | ||
|
|
e96db5d0d6 | ||
|
|
1083c4241a | ||
|
|
1eeabab41a | ||
|
|
2b5f6f2208 | ||
|
|
bda377336d | ||
|
|
77507f7b18 | ||
|
|
a39f2f7c00 | ||
|
|
229439aa05 | ||
|
|
612881f1b1 | ||
|
|
05c7bc18d7 | ||
|
|
c68c5985f6 | ||
|
|
7d44791011 | ||
|
|
15b992b949 | ||
|
|
4b8229b0a1 | ||
|
|
6e4fbc3c42 | ||
|
|
779264623c | ||
|
|
76aef40de7 | ||
|
|
a1eccb3b1e | ||
|
|
0f75a95dbe | ||
|
|
efbb83924b | ||
|
|
26d1db79f4 | ||
|
|
dc13b2941f | ||
|
|
13c250d392 | ||
|
|
f5b40feaa2 | ||
|
|
c47c318e6f | ||
|
|
c02d993e90 | ||
|
|
f36bcb74ca | ||
|
|
2263fd97db | ||
|
|
9234d1099e | ||
|
|
373dece19d | ||
|
|
b09828bcc2 | ||
|
|
8751a7707c | ||
|
|
f91b240802 | ||
|
|
062b289f7a | ||
|
|
c1dc77f6db | ||
|
|
cea60175c2 | ||
|
|
2bd3630512 | ||
|
|
a9d8181940 | ||
|
|
4cc108094d | ||
|
|
bfa57cce44 | ||
|
|
8ebdcd94f5 | ||
|
|
9991210db2 | ||
|
|
1c59815afa | ||
|
|
e7593c8ebf | ||
|
|
bc767a6ac9 | ||
|
|
1c1915285d | ||
|
|
b6c2471bc3 | ||
|
|
4cc2800f09 | ||
|
|
396434a82e | ||
|
|
07c6b53f82 | ||
|
|
930a861ba6 | ||
|
|
1f4e1dea75 | ||
|
|
bc617837be | ||
|
|
17e4862eaf | ||
|
|
72b2b82e02 | ||
|
|
70f7442f0c | ||
|
|
2b2deb8f66 | ||
|
|
0a7a433bc6 | ||
|
|
b64f9ef1f6 | ||
|
|
f22ca9cdcd | ||
|
|
d26b96ebd1 | ||
|
|
13cc981421 | ||
|
|
efc8589ca0 | ||
|
|
940690889c | ||
|
|
d46420ef81 | ||
|
|
b36b5b59dc | ||
|
|
cf96806f80 | ||
|
|
3d0d0496b6 | ||
|
|
f67e220894 | ||
|
|
9306e35b84 | ||
|
|
d2268a1944 | ||
|
|
6baa4d4233 | ||
|
|
ef9d90455f | ||
|
|
5d499956d7 | ||
|
|
9101ed336c | ||
|
|
28e3ebb911 | ||
|
|
e93e33fe43 | ||
|
|
0ebeccf21e | ||
|
|
89842b82e9 | ||
|
|
58594229f2 | ||
|
|
b4a811ff4e | ||
|
|
7067630bcc | ||
|
|
b28e8d4bc9 | ||
|
|
063866cc3a | ||
|
|
6f968d16aa | ||
|
|
6db969cc4d | ||
|
|
6ea9b4a33c | ||
|
|
bcfc40d795 | ||
|
|
c5c7066b92 | ||
|
|
51b73fcc93 | ||
|
|
da181b9d6d | ||
|
|
134e3fc866 | ||
|
|
c3758cafe8 | ||
|
|
1a21ba8935 | ||
|
|
a397ebe79b | ||
|
|
abbdb224e0 | ||
|
|
f4fb3b2544 | ||
|
|
ae2412a906 | ||
|
|
d8534fb94d | ||
|
|
37c4306010 |
119
.env.example
Normal file
119
.env.example
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# === Core Service Ports ===
|
||||||
|
SERVER_PORT=8080
|
||||||
|
FRONTEND_PORT=3000
|
||||||
|
WEBSOCKET_PORT=8082
|
||||||
|
OPENISLE_MCP_PORT=8085
|
||||||
|
MYSQL_PORT=3306
|
||||||
|
REDIS_PORT=6379
|
||||||
|
RABBITMQ_PORT=5672
|
||||||
|
RABBITMQ_MANAGEMENT_PORT=15672
|
||||||
|
|
||||||
|
# === OpenSearch Configuration ===
|
||||||
|
OPENSEARCH_PORT=9200
|
||||||
|
OPENSEARCH_METRICS_PORT=9600
|
||||||
|
OPENSEARCH_DASHBOARDS_PORT=5601
|
||||||
|
OPENSEARCH_ENABLED=true
|
||||||
|
OPENSEARCH_SCHEME=http
|
||||||
|
OPENSEARCH_USERNAME=
|
||||||
|
OPENSEARCH_PASSWORD=
|
||||||
|
OPENSEARCH_HOST=opensearch
|
||||||
|
|
||||||
|
# === Database Configuration ===
|
||||||
|
MYSQL_DATABASE=openisle
|
||||||
|
MYSQL_ROOT_PASSWORD=openisle
|
||||||
|
MYSQL_USER=openisle
|
||||||
|
MYSQL_PASSWORD=openisle
|
||||||
|
MYSQL_HOST=mysql
|
||||||
|
|
||||||
|
# === Redis Configuration ===
|
||||||
|
REDIS_HOST=redis
|
||||||
|
REDIS_DATABASE=0
|
||||||
|
|
||||||
|
# === RabbitMQ Configuration ===
|
||||||
|
RABBITMQ_HOST=rabbitmq
|
||||||
|
RABBITMQ_USERNAME=nagisa
|
||||||
|
RABBITMQ_PASSWORD=nagisa
|
||||||
|
|
||||||
|
# === Backend Application Secrets ===
|
||||||
|
JWT_SECRET=change-me-jwt-secret
|
||||||
|
JWT_REASON_SECRET=change-me-jwt-reason-secret
|
||||||
|
JWT_RESET_SECRET=change-me-jwt-reset-secret
|
||||||
|
JWT_INVITE_SECRET=change-me-jwt-invite-secret
|
||||||
|
JWT_EXPIRATION=2592000000
|
||||||
|
PASSWORD_STRENGTH=LOW
|
||||||
|
POST_PUBLISH_MODE=DIRECT
|
||||||
|
REGISTER_MODE=WHITELIST
|
||||||
|
UPLOAD_CHECK_TYPE=true
|
||||||
|
UPLOAD_MAX_SIZE=5242880
|
||||||
|
AVATAR_STYLE=pixel-art-neutral
|
||||||
|
AVATAR_SIZE=128
|
||||||
|
AVATAR_BASE_URL=https://api.dicebear.com/6.x
|
||||||
|
USER_POSTS_LIMIT=10
|
||||||
|
USER_REPLIES_LIMIT=50
|
||||||
|
SNIPPET_LENGTH=200
|
||||||
|
SEARCH_INDEX_PREFIX=openisle
|
||||||
|
SEARCH_HIGHLIGHT_FRAGMENT_SIZE=200
|
||||||
|
SEARCH_REINDEX_ON_STARTUP=true
|
||||||
|
SEARCH_REINDEX_BATCH_SIZE=500
|
||||||
|
CAPTCHA_ENABLED=false
|
||||||
|
RECAPTCHA_SECRET_KEY=
|
||||||
|
CAPTCHA_REGISTER_ENABLED=false
|
||||||
|
CAPTCHA_LOGIN_ENABLED=false
|
||||||
|
CAPTCHA_POST_ENABLED=false
|
||||||
|
CAPTCHA_COMMENT_ENABLED=false
|
||||||
|
RESEND_API_KEY=
|
||||||
|
RESEND_FROM_EMAIL=
|
||||||
|
COS_BASE_URL=https://<你的cos>.cos.accelerate.myqcloud.com
|
||||||
|
COS_SECRET_ID=
|
||||||
|
COS_SECRET_KEY=
|
||||||
|
COS_REGION=ap-guangzhou
|
||||||
|
COS_BUCKET_NAME=
|
||||||
|
GITHUB_CLIENT_SECRET=
|
||||||
|
DISCORD_CLIENT_SECRET=
|
||||||
|
TWITTER_CLIENT_SECRET=
|
||||||
|
TELEGRAM_BOT_TOKEN=
|
||||||
|
OPENAI_API_KEY=
|
||||||
|
OPENAI_MODEL=gpt-4o
|
||||||
|
AI_FORMAT_LIMIT=3
|
||||||
|
WEBSITE_URL=http://localhost:3000
|
||||||
|
WEBPUSH_PUBLIC_KEY=
|
||||||
|
WEBPUSH_PRIVATE_KEY=
|
||||||
|
LOG_LEVEL=INFO
|
||||||
|
|
||||||
|
# === Frontend (Nuxt) ===
|
||||||
|
# 本地开发
|
||||||
|
NUXT_PUBLIC_API_BASE_URL=http://localhost:8080
|
||||||
|
# 线上环境
|
||||||
|
# NUXT_PUBLIC_API_BASE_URL=https://www.open-isle.com
|
||||||
|
# 测试环境
|
||||||
|
# NUXT_PUBLIC_API_BASE_URL=https://www.staging.open-isle.com
|
||||||
|
|
||||||
|
# 本地开发
|
||||||
|
NUXT_PUBLIC_WEBSOCKET_URL=http://localhost:8082
|
||||||
|
# 线上环境
|
||||||
|
# NUXT_PUBLIC_WEBSOCKET_URL=https://www.open-isle.com/websocket
|
||||||
|
# 测试环境
|
||||||
|
# NUXT_PUBLIC_WEBSOCKET_URL=https://www.staging.open-isle.com/websocket
|
||||||
|
|
||||||
|
# 本地开发
|
||||||
|
NUXT_PUBLIC_WEBSITE_BASE_URL=http://localhost:3000
|
||||||
|
# 线上 & 测试 (www.staging.open-isle.com) & 本地均可使用
|
||||||
|
NUXT_PUBLIC_GOOGLE_CLIENT_ID=777830451304-nt8afkkap18gui4f9entcha99unal744.apps.googleusercontent.com
|
||||||
|
# 线上
|
||||||
|
NUXT_PUBLIC_GITHUB_CLIENT_ID=Ov23liVkO1NPAX5JyWxJ
|
||||||
|
# 测试环境 (www.staging.open-isle.com)
|
||||||
|
# NUXT_PUBLIC_GITHUB_CLIENT_ID=Ov23li6GHPxx4MwipWnM
|
||||||
|
# 本地
|
||||||
|
# NUXT_PUBLIC_GITHUB_CLIENT_ID=Ov23liOlrZnPKRF7s7NN
|
||||||
|
|
||||||
|
# 线上 & 本地均可使用
|
||||||
|
NUXT_PUBLIC_DISCORD_CLIENT_ID=1394985417044000779
|
||||||
|
|
||||||
|
# 线上 & 本地均可使用
|
||||||
|
NUXT_PUBLIC_TWITTER_CLIENT_ID=ZTRTU05KSk9KTTJrTTdrVC1tc1E6MTpjaQ
|
||||||
|
|
||||||
|
# 线上
|
||||||
|
NUXT_PUBLIC_TELEGRAM_BOT_ID=8450237135
|
||||||
|
# 测试环境 (www.staging.open-isle.com)
|
||||||
|
# NUXT_PUBLIC_TELEGRAM_BOT_ID=7832207011
|
||||||
|
|
||||||
7
.github/ISSUE_TEMPLATE/新功能建议.md
vendored
7
.github/ISSUE_TEMPLATE/新功能建议.md
vendored
@@ -1,10 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: 新功能建议
|
name: 新功能建议
|
||||||
about: 请为该项目提出一个想法
|
about: 请为该项目提出一个想法
|
||||||
title: ''
|
title: ""
|
||||||
labels: ''
|
labels: ""
|
||||||
assignees: ''
|
assignees: ""
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**你的功能请求是否与某个问题相关?请描述。**
|
**你的功能请求是否与某个问题相关?请描述。**
|
||||||
|
|||||||
21
.github/ISSUE_TEMPLATE/错误-bug报告.md
vendored
21
.github/ISSUE_TEMPLATE/错误-bug报告.md
vendored
@@ -1,10 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: 错误/Bug报告
|
name: 错误/Bug报告
|
||||||
about: 创建报告以帮助我们改进
|
about: 创建报告以帮助我们改进
|
||||||
title: ''
|
title: ""
|
||||||
labels: ''
|
labels: ""
|
||||||
assignees: ''
|
assignees: ""
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**描述 Bug**
|
**描述 Bug**
|
||||||
@@ -26,16 +25,16 @@ assignees: ''
|
|||||||
|
|
||||||
**桌面端(请完成以下信息):**
|
**桌面端(请完成以下信息):**
|
||||||
|
|
||||||
* 操作系统:\[例如 iOS]
|
- 操作系统:\[例如 iOS]
|
||||||
* 浏览器:\[例如 Chrome、Safari]
|
- 浏览器:\[例如 Chrome、Safari]
|
||||||
* 版本:\[例如 22]
|
- 版本:\[例如 22]
|
||||||
|
|
||||||
**移动端(请完成以下信息):**
|
**移动端(请完成以下信息):**
|
||||||
|
|
||||||
* 设备:\[例如 iPhone6]
|
- 设备:\[例如 iPhone6]
|
||||||
* 操作系统:\[例如 iOS8.1]
|
- 操作系统:\[例如 iOS8.1]
|
||||||
* 浏览器:\[例如 系统自带浏览器、Safari]
|
- 浏览器:\[例如 系统自带浏览器、Safari]
|
||||||
* 版本:\[例如 22]
|
- 版本:\[例如 22]
|
||||||
|
|
||||||
**附加上下文**
|
**附加上下文**
|
||||||
在此添加与问题相关的其他上下文信息。
|
在此添加与问题相关的其他上下文信息。
|
||||||
|
|||||||
29
.github/workflows/coffee-bot.yml
vendored
Normal file
29
.github/workflows/coffee-bot.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
name: Coffee Bot
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 1 * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-coffee-bot:
|
||||||
|
environment: Bots
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
cache: "npm"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install --no-save @openai/agents tsx typescript
|
||||||
|
|
||||||
|
- name: Run coffee bot
|
||||||
|
env:
|
||||||
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||||
|
OPENISLE_TOKEN: ${{ secrets.OPENISLE_TOKEN }}
|
||||||
|
run: npx tsx bots/instance/coffee_bot.ts
|
||||||
7
.github/workflows/deploy-docs.yml
vendored
7
.github/workflows/deploy-docs.yml
vendored
@@ -11,12 +11,17 @@ on:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
|
# 文档发布自己的排队锁,不影响服务器部署
|
||||||
|
concurrency:
|
||||||
|
group: openisle-docs
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-docs:
|
build-docs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
|
|||||||
13
.github/workflows/deploy-staging.yml
vendored
13
.github/workflows/deploy-staging.yml
vendored
@@ -2,27 +2,33 @@ name: Staging CI & CD
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [ "main" ]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
|
# 与生产部署共用同一把锁,确保服务器上始终串行(跨工作流也互斥)
|
||||||
|
concurrency:
|
||||||
|
group: openisle-server
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: Deploy
|
environment: Deploy
|
||||||
|
if: ${{ !github.event.repository.fork }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Deploy to Server
|
- name: Deploy to Server (staging)
|
||||||
uses: appleboy/ssh-action@v1.0.3
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
with:
|
with:
|
||||||
host: ${{ secrets.SSH_HOST }}
|
host: ${{ secrets.SSH_HOST }}
|
||||||
username: root
|
username: root
|
||||||
key: ${{ secrets.SSH_KEY }}
|
key: ${{ secrets.SSH_KEY }}
|
||||||
script: bash /opt/openisle/deploy-staging.sh
|
script: bash /opt/openisle/OpenIsle/deploy/deploy_staging.sh
|
||||||
|
|
||||||
deploy-docs:
|
deploy-docs:
|
||||||
needs: build-and-deploy
|
needs: build-and-deploy
|
||||||
@@ -31,4 +37,3 @@ jobs:
|
|||||||
secrets: inherit
|
secrets: inherit
|
||||||
with:
|
with:
|
||||||
build-id: ${{ github.run_id }}
|
build-id: ${{ github.run_id }}
|
||||||
|
|
||||||
|
|||||||
11
.github/workflows/deploy.yml
vendored
11
.github/workflows/deploy.yml
vendored
@@ -3,7 +3,12 @@ name: CI & CD
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 19 * * *" # 每天 UTC 19:00,相当于北京时间凌晨3点
|
- cron: "0 19 * * *" # 每天 UTC 19:00(北京 03:00)
|
||||||
|
|
||||||
|
# 与 Staging 共用同一把锁,避免两边同时在 8G 服务器上跑
|
||||||
|
concurrency:
|
||||||
|
group: openisle-server
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
@@ -13,10 +18,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Deploy to Server
|
- name: Deploy to Server (prod)
|
||||||
uses: appleboy/ssh-action@v1.0.3
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
with:
|
with:
|
||||||
host: ${{ secrets.SSH_HOST }}
|
host: ${{ secrets.SSH_HOST }}
|
||||||
username: root
|
username: root
|
||||||
key: ${{ secrets.SSH_KEY }}
|
key: ${{ secrets.SSH_KEY }}
|
||||||
script: bash /opt/openisle/deploy.sh
|
script: bash /opt/openisle/OpenIsle/deploy/deploy.sh
|
||||||
|
|||||||
29
.github/workflows/reply-bots.yml
vendored
Normal file
29
.github/workflows/reply-bots.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
name: Reply Bots
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "*/30 * * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-reply-bot:
|
||||||
|
environment: Bots
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
cache: "npm"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install --no-save @openai/agents tsx typescript
|
||||||
|
|
||||||
|
- name: Run reply bot
|
||||||
|
env:
|
||||||
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||||
|
OPENISLE_TOKEN: ${{ secrets.OPENISLE_TOKEN }}
|
||||||
|
run: npx tsx bots/instance/reply_bot.ts
|
||||||
@@ -8,21 +8,21 @@ This isn’t an exhaustive list of things that you can’t do. Rather, take it i
|
|||||||
|
|
||||||
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.
|
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
|
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 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 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 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 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:
|
- **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.
|
- Violent threats or language directed against another person.
|
||||||
- Discriminatory jokes and language.
|
- Discriminatory jokes and language.
|
||||||
- Posting sexually explicit or violent material.
|
- Posting sexually explicit or violent material.
|
||||||
- Posting (or threatening to post) other people's personally identifying information ("doxing").
|
- Posting (or threatening to post) other people's personally identifying information ("doxing").
|
||||||
- Personal insults, especially those using racist or sexist terms.
|
- Personal insults, especially those using racist or sexist terms.
|
||||||
- Unwelcome sexual attention.
|
- Unwelcome sexual attention.
|
||||||
- Advocating for, or encouraging, any of the above behavior.
|
- Advocating for, or encouraging, any of the above behavior.
|
||||||
- Repeated harassment of others. In general, if someone asks you to stop, then stop.
|
- 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.
|
- **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).
|
Original text courtesy of the [Speak Up! project](http://web.archive.org/web/20141109123859/http://speakup.io/coc.html).
|
||||||
|
|||||||
339
CONTRIBUTING.md
339
CONTRIBUTING.md
@@ -1,16 +1,20 @@
|
|||||||
- [前置工作](#前置工作)
|
- [前置工作](#前置工作)
|
||||||
|
- [前端极速调试(Docker 全量环境)](#前端极速调试docker-全量环境)
|
||||||
|
- [dev 与 dev_local_backend 巡航指南](#dev-dev_local_backend-guide)
|
||||||
- [启动后端服务](#启动后端服务)
|
- [启动后端服务](#启动后端服务)
|
||||||
- [本地 IDEA](#本地-idea)
|
- [本地 IDEA](#本地-idea)
|
||||||
- [配置环境变量](#配置环境变量)
|
- [配置环境变量](#配置环境变量)
|
||||||
- [配置 IDEA 参数](#配置-idea-参数)
|
- [配置 IDEA 参数](#配置-idea-参数)
|
||||||
- [配置 MySQL](#配置-mysql)
|
|
||||||
- [Docker 环境](#docker-环境)
|
|
||||||
- [配置环境变量](#配置环境变量-1)
|
|
||||||
- [构建并启动镜像](#构建并启动镜像)
|
|
||||||
- [启动前端服务](#启动前端服务)
|
- [启动前端服务](#启动前端服务)
|
||||||
- [配置环境变量](#配置环境变量-2)
|
- [连接预发或正式环境](#连接预发或正式环境)
|
||||||
- [安装依赖和运行](#安装依赖和运行)
|
|
||||||
- [其他配置](#其他配置)
|
- [其他配置](#其他配置)
|
||||||
|
- [配置第三方登录以GitHub为例](#配置第三方登录以github为例)
|
||||||
|
- [配置Resend邮箱服务](#配置resend邮箱服务)
|
||||||
|
- [API文档](#api文档)
|
||||||
|
- [OpenAPI文档](#openapi文档)
|
||||||
|
- [部署时间线以及文档时效性](#部署时间线以及文档时效性)
|
||||||
|
- [OpenAPI文档使用](#openapi文档使用)
|
||||||
|
- [OpenAPI文档应用场景](#openapi文档应用场景)
|
||||||
|
|
||||||
## 前置工作
|
## 前置工作
|
||||||
|
|
||||||
@@ -22,9 +26,89 @@ cd OpenIsle
|
|||||||
```
|
```
|
||||||
|
|
||||||
- 后端开发环境
|
- 后端开发环境
|
||||||
- JDK 17+
|
- JDK 17+
|
||||||
- 前端开发环境
|
- 前端开发环境
|
||||||
- Node.JS 20+
|
- Node.JS 20+
|
||||||
|
|
||||||
|
## 前端极速调试(Docker 全量环境)
|
||||||
|
|
||||||
|
想要最快速地同时体验前端和后端,可直接使用仓库提供的 Docker Compose。该方案会一次性拉起数据库、消息队列、搜索、后端、WebSocket 以及前端 Dev Server,适合需要全链路联调的场景。
|
||||||
|
|
||||||
|
1. 准备环境变量文件:
|
||||||
|
```shell
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
`.env.example` 是模板,可在 `.env` 中按需覆盖如端口、密钥等配置。确保 `NUXT_PUBLIC_API_BASE_URL`、`NUXT_PUBLIC_WEBSOCKET_URL` 等仍指向 `localhost`,方便前端直接访问容器映射端口。
|
||||||
|
2. 启动 Dev Profile:
|
||||||
|
```shell
|
||||||
|
docker compose \
|
||||||
|
-f docker/docker-compose.yaml \
|
||||||
|
--env-file .env \
|
||||||
|
--profile dev up -d
|
||||||
|
```
|
||||||
|
该命令会创建名为 `frontend_dev` 的容器并运行 `npm run dev`,浏览器访问 http://127.0.0.1:3000 即可查看页面。
|
||||||
|
修改前端代码,页面会热更新。
|
||||||
|
如果修改后端代码,可以重启后端容器, 或是环境变量中指向IDEA,采用IDEA编译运行也可以哦。
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker compose \
|
||||||
|
-f docker/docker-compose.yaml \
|
||||||
|
--env-file .env \
|
||||||
|
--profile dev up -d --force-recreate
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 查看服务状态:
|
||||||
|
```shell
|
||||||
|
docker compose -f docker/docker-compose.yaml --env-file .env ps
|
||||||
|
docker compose -f docker/docker-compose.yaml --env-file .env logs -f frontend_dev
|
||||||
|
```
|
||||||
|
4. 停止所有容器:
|
||||||
|
```shell
|
||||||
|
docker compose -f docker/docker-compose.yaml --env-file .env --profile dev down
|
||||||
|
```
|
||||||
|
|
||||||
|
5. 开发时若需要**重置所有容器及其挂载的数据卷**,可以执行:
|
||||||
|
```shell
|
||||||
|
docker compose -f docker/docker-compose.yaml --env-file .env --profile dev down -v
|
||||||
|
```
|
||||||
|
`-v` 参数会在关闭容器的同时移除通过 `volumes` 声明的挂载卷,适用于希望清理数据库、缓存等持久化数据,确保下一次启动时获得全新环境的场景。
|
||||||
|
|
||||||
|
如需自定义 Node 依赖缓存、数据库持久化等,可参考 `docker/docker-compose.yaml` 中各卷的定义进行调整。
|
||||||
|
|
||||||
|
<a id="dev-dev_local_backend-guide"></a>
|
||||||
|
|
||||||
|
### 🧭 dev 与 dev_local_backend 巡航指南
|
||||||
|
|
||||||
|
在需要本地 IDE 启动后端、而容器只提供 MySQL、Redis、RabbitMQ、OpenSearch 等依赖时,可切换到 `dev_local_backend` Profile:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose \
|
||||||
|
-f docker/docker-compose.yaml \
|
||||||
|
--env-file .env \
|
||||||
|
--profile dev_local_backend up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> 该 Profile 不会启动 Docker 内的 Spring Boot 服务,`frontend_dev_local_backend` 会通过 `host.docker.internal` 访问你本机正在运行的后端。非常适合用 IDEA/VS Code 调试 Java 服务的场景!
|
||||||
|
|
||||||
|
| 想要的体验 | 推荐 Profile | 会启动的关键容器 | 备注 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 🚀 一键启动前后端 | `dev` | `springboot`、`frontend_dev`、`mysql`… | 纯容器内跑全链路,省心省力 |
|
||||||
|
| 🛠️ IDE 启动后端 + 容器托管依赖 | `dev_local_backend` | `frontend_dev_local_backend`、`mysql`、`redis`… | 记得本地后端监听 `8080`/`8082` 等端口 |
|
||||||
|
|
||||||
|
切换 Profile 时,请先停掉当前组合再启动另一组,避免端口占用或容器命名冲突:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker/docker-compose.yaml --env-file .env --profile dev down
|
||||||
|
# 或者
|
||||||
|
docker compose -f docker/docker-compose.yaml --env-file .env --profile dev_local_backend down
|
||||||
|
```
|
||||||
|
|
||||||
|
常见小贴士:
|
||||||
|
|
||||||
|
- 🧹 需要彻底清理依赖时,别忘了追加 `-v` 清除持久化数据卷。
|
||||||
|
- 🪄 仅切换 Profile 时通常无需重新 `build`,除非你更新了镜像依赖。
|
||||||
|
- 🧪 如需确认前端容器访问的是本机后端,可在 IDE 控制台查看请求日志或执行 `curl http://localhost:8080/actuator/health` 进行自检。
|
||||||
|
|
||||||
## 启动后端服务
|
## 启动后端服务
|
||||||
|
|
||||||
@@ -43,190 +127,81 @@ IDEA 打开 `backend/` 文件夹。
|
|||||||
|
|
||||||
#### 配置环境变量
|
#### 配置环境变量
|
||||||
|
|
||||||
1. 生成环境变量文件
|
1. 生成环境变量文件:
|
||||||
|
```shell
|
||||||
|
cp open-isle.env.example open-isle.env
|
||||||
|
```
|
||||||
|
`open-isle.env` 才是实际被读取的文件。可在其中补充数据库、第三方服务等配置,`open-isle.env` 已被 Git 忽略,放心修改。
|
||||||
|
2. 在 IDEA 中配置「Environment file」:将 `Run/Debug Configuration` 的 `Environment variables` 指向刚刚复制的 `open-isle.env`,即可让 IDE 读取该文件。
|
||||||
|
3. 需要调整端口或功能开关时,优先修改 `open-isle.env`,例如:
|
||||||
|
```ini
|
||||||
|
SERVER_PORT=8081
|
||||||
|
LOG_LEVEL=DEBUG
|
||||||
|
```
|
||||||
|
|
||||||
```shell
|
> [!WARNING]
|
||||||
cp open-isle.env.example open-isle.env
|
> 如果你通过 `dev_local_backend` Profile 启动了数据库/缓存等依赖,却让后端由 IDEA 在宿主机运行,请务必将 `open-isle.env`(或 IDEA 的环境变量面板)中的主机名改成 `localhost`:
|
||||||
```
|
>
|
||||||
|
> ```ini
|
||||||
|
> MYSQL_HOST=localhost
|
||||||
|
> REDIS_HOST=localhost
|
||||||
|
> RABBITMQ_HOST=localhost
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> 对应的容器端口均已映射到宿主机,无需额外配置。若仍保留默认的 `mysql`、`redis`、`rabbitmq`,IDEA 将尝试解析容器网络内的别名而导致连接失败。
|
||||||
|
|
||||||
`open-isle.env.example` 是环境变量模板,`open-isle.env` 才是真正读取的内容
|
也可以修改 `src/main/resources/application.properties`,但该文件会被 Git 追踪,通常不推荐。
|
||||||
|
|
||||||
2. 修改环境变量,留下需要的,比如你要开发 Google 登录业务,就需要谷歌相关的变量,数据库是一定要的
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
3. 应用环境文件,选择刚刚的 `open-isle.env`
|
|
||||||
|
|
||||||
可以在 `open-isle.env` 按需填写个性化的配置,该文件不会被 Git 追踪。比如你想把服务跑在 `8082`(默认为 `8080`),那么直接改 `open-isle.env` 即可:
|
|
||||||
|
|
||||||
```ini
|
|
||||||
SERVER_PORT=8082
|
|
||||||
```
|
|
||||||
|
|
||||||
另一种方式是修改 `.properities` 文件(但不建议),位于 `src/main/application.properties`,该配置同样来源于 `open-isle.env`,但修改 `.properties` 文件会被 Git 追踪。
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### 配置 IDEA 参数
|
#### 配置 IDEA 参数
|
||||||
|
|
||||||
- 设置 JDK 版本为 java 17
|
- 设置 JDK 版本为 Java 17。
|
||||||
|
- 设置 VM Option,最好运行在其他端口(例如 `8081`)。若已经在 `open-isle.env` 中调整端口,可省略此步骤。
|
||||||
- 设置 VM Option,最好运行在其他端口,非 `8080`,这里设置 `8081`
|
```shell
|
||||||
若上面在环境变量中设置了端口,那这里就不需要再额外设置
|
-Dserver.port=8081
|
||||||
|
```
|
||||||
```shell
|
|
||||||
-Dserver.port=8081
|
|
||||||
```
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### 配置 MySQL
|
完成环境变量和运行参数设置后,即可启动 Spring Boot 应用。
|
||||||
|
|
||||||
> [!TIP]
|
|
||||||
> 如果不知道怎么配置数据库可以参考 [Docker 环境](#docker-环境) 章节
|
|
||||||
|
|
||||||
1. 本机配置 MySQL 服务(网上很多教程,忽略)
|
|
||||||
|
|
||||||
+ 可以用 Laragon,自带 MySQL 包括 Nodejs,版本建议 `6.x`,`7` 以后需要 Lisence
|
|
||||||
+ [下载地址](https://github.com/leokhoa/laragon/releases)
|
|
||||||
|
|
||||||
2. 填写环境变量
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
```ini
|
|
||||||
MYSQL_URL=jdbc:mysql://<数据库地址>:<端口>/<数据库名>?useUnicode=yes&characterEncoding=UTF-8&useInformationSchema=true&useSSL=false&serverTimezone=UTC
|
|
||||||
MYSQL_USER=<数据库用户名>
|
|
||||||
MYSQL_PASSWORD=<数据库密码>
|
|
||||||
```
|
|
||||||
|
|
||||||
3. 执行 [`db/init/init_script.sql`](backend/src/main/resources/db/init/init_script.sql) 脚本,导入基本的数据
|
|
||||||
管理员:**admin/123456**
|
|
||||||
普通用户1:**user1/123456**
|
|
||||||
普通用户2:**user2/123456**
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
#### 配置 Redis
|
|
||||||
|
|
||||||
填写环境变量 `.env` 中的 Redis 相关配置并启动 Redis
|
|
||||||
|
|
||||||
```ini
|
|
||||||
REDIS_HOST=<Redis 地址>
|
|
||||||
REDIS_PORT=<Redis 端口>
|
|
||||||
```
|
|
||||||
|
|
||||||
处理完环境问题直接跑起来就能通了
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### Docker 环境
|
## 前端连接预发或正式环境
|
||||||
|
|
||||||
#### 配置环境变量
|
前端默认读取 `.env` 中的接口地址,可通过修改以下变量快速切换到预发或正式环境:
|
||||||
|
|
||||||
```shell
|
1. 按需覆盖关键变量:
|
||||||
cd docker/
|
|
||||||
```
|
|
||||||
|
|
||||||
主要配置两个 `.env` 文件
|
```ini
|
||||||
|
NUXT_PUBLIC_API_BASE_URL=https://www.staging.open-isle.com
|
||||||
|
NUXT_PUBLIC_WEBSOCKET_URL=https://www.staging.open-isle.com
|
||||||
|
```
|
||||||
|
将 `staging` 替换为 `www` 即可连接正式环境。其他变量(如 OAuth Client ID、站点地址等)可根据需求调整。
|
||||||
|
|
||||||
- `backend/open-isle.env`:后端环境变量,配置同上,见 [配置环境变量](#配置环境变量)。
|
|
||||||
- `docker/.env`:Docker Compose 环境变量,主要配置 MySQL 相关
|
|
||||||
```shell
|
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
> [!TIP]
|
|
||||||
> 使用单独的 `.env` 文件是为了兼容线上环境或已启用 MySQL 服务的情况,如果只是想快速体验或者启动统一的环境,则推荐使用本方式。
|
|
||||||
|
|
||||||
在指定 `docker/.env` 后,`backend/open-isle.env` 中以下配置会被覆盖,这样就确保使用了同一份配置。
|
|
||||||
|
|
||||||
```ini
|
|
||||||
MYSQL_URL=
|
|
||||||
MYSQL_USER=
|
|
||||||
MYSQL_PASSWORD=
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 构建并启动镜像
|
|
||||||
|
|
||||||
```shell
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
如果想了解启动过程发生了什么可以查看日志
|
|
||||||
|
|
||||||
```shell
|
|
||||||
docker compose logs
|
|
||||||
```
|
|
||||||
|
|
||||||
## 启动前端服务
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> **⚠️ 环境要求:Node.js 版本最低 20.0.0(因为 Nuxt 框架要求)**
|
|
||||||
|
|
||||||
```shell
|
|
||||||
cd frontend_nuxt/
|
|
||||||
```
|
|
||||||
|
|
||||||
### 配置环境变量
|
|
||||||
|
|
||||||
前端可以依赖本机部署的后端,也可以直接调用线上的后端接口。
|
|
||||||
|
|
||||||
- 利用预发环境:**(⚠️ 强烈推荐只开发前端的朋友使用该环境)**
|
|
||||||
|
|
||||||
```shell
|
|
||||||
cp .env.staging.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
- 利用生产环境
|
|
||||||
|
|
||||||
```shell
|
|
||||||
cp .env.production.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
- 利用本地环境
|
|
||||||
|
|
||||||
```shell
|
|
||||||
cp .env.dev.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
若依赖本机部署的后端,需要修改 `.env` 中的 `NUXT_PUBLIC_API_BASE_URL` 值与后端服务端口一致
|
|
||||||
|
|
||||||
### 安装依赖和运行
|
|
||||||
|
|
||||||
前端安装依赖并启动服务。
|
|
||||||
|
|
||||||
```shell
|
|
||||||
# 安装依赖
|
|
||||||
npm install --verbose
|
|
||||||
|
|
||||||
# 运行前端服务
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
如此一来,浏览器访问 http://127.0.0.1:3000 即可访问前端页面。
|
|
||||||
|
|
||||||
## 其他配置
|
## 其他配置
|
||||||
|
|
||||||
### 配置第三方登录,这里以 GitHub 为例:
|
### 配置第三方登录以GitHub为例
|
||||||
|
|
||||||
- 修改 `application.properties` 配置
|
- 修改 `application.properties` 配置
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
- 修改 `.env` 配置
|
- 修改 `.env` 配置
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
- 配置第三方登录回调地址
|
- 配置第三方登录回调地址
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### 配置 Resend 邮箱服务
|
### 配置Resend邮箱服务
|
||||||
|
|
||||||
https://resend.com/emails 创建账号并登录
|
https://resend.com/emails 创建账号并登录
|
||||||
|
|
||||||
@@ -247,8 +222,42 @@ https://resend.com/emails 创建账号并登录
|
|||||||
`RESEND_API_KEY`:**刚刚复制的 Key**
|
`RESEND_API_KEY`:**刚刚复制的 Key**
|
||||||

|

|
||||||
|
|
||||||
## 开源共建和API文档
|
## API文档
|
||||||
|
|
||||||
|
### OpenAPI文档
|
||||||
|
https://docs.open-isle.com
|
||||||
|
|
||||||
|
### 部署时间线以及文档时效性
|
||||||
|
|
||||||
|
我已经将API Docs的部署融合进本站CI & CD中,目前如下
|
||||||
|
|
||||||
|
- 每次合入main之后,都会构建预发环境 http://staging.open-isle.com/ ,现在文档是紧随其后进行部署,也就是说代码合入main之后,如果是新增后台接口,就可以立即通过OpenAPI文档页面进行查看和调试,但是如果想通过OpenAPI调试需要选择预发环境的
|
||||||
|
- 每日凌晨三点会构建并重新部署正式环境,届时当日合入main的新后台API也可以通过OpenAPI文档页面调试
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
👆如图是合入main之后构建预发+docs的情形,总大约耗时4分钟左右
|
||||||
|
|
||||||
|
### OpenAPI文档使用
|
||||||
|
|
||||||
|
- 预发环境/正式环境切换,以通过如下位置切换API环境
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- API分两种,一种是需要鉴权(需登录后的token),另一种是直接访问,可以直接访问的GET请求,直接点击Send即可调试,如下👇,比如本站的推荐流rss: /api/rss: https://docs.open-isle.com/openapi/feed
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- 需要登陆的API,比如关注,取消关注,发帖等,则需要提供token,目前在“API与调试”可获取自身token,可点击link看看👉 https://www.open-isle.com/about?tab=api
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
copy完token之后,粘贴到Bear之后, 即可发送调试, 如下👇,大家亦可自行尝试:https://docs.open-isle.com/openapi/me
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### OpenAPI文档应用场景
|
||||||
|
|
||||||
|
- 方便大部分前端调试的需求,如果有只想做前端/客户端的同学参与本项目,该平台会大大提高效率
|
||||||
|
- 自动化:有自动化发帖/自动化操作的需求,亦可通过该平台实现或调试
|
||||||
- API文档: https://docs.open-isle.com/openapi
|
- API文档: https://docs.open-isle.com/openapi
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
176
SECURITY.md
Normal file
176
SECURITY.md
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
We take the security of OpenIsle seriously. The following versions are currently being supported with security updates:
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| 0.0.x | :white_check_mark: |
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
We appreciate your efforts to responsibly disclose your findings and will make every effort to acknowledge your contributions.
|
||||||
|
|
||||||
|
### How to Report a Security Vulnerability
|
||||||
|
|
||||||
|
**Please do not report security vulnerabilities through public GitHub issues.**
|
||||||
|
|
||||||
|
Instead, please report them via one of the following methods:
|
||||||
|
|
||||||
|
1. **Email**: Send a detailed report to the project maintainer (check the repository for contact information)
|
||||||
|
2. **GitHub Security Advisory**: Use GitHub's private vulnerability reporting feature at https://github.com/nagisa77/OpenIsle/security/advisories/new
|
||||||
|
|
||||||
|
### What to Include in Your Report
|
||||||
|
|
||||||
|
To help us better understand the nature and scope of the issue, please include as much of the following information as possible:
|
||||||
|
|
||||||
|
- Type of issue (e.g., SQL injection, XSS, authentication bypass, etc.)
|
||||||
|
- Full paths of source file(s) related to the manifestation of the issue
|
||||||
|
- The location of the affected source code (tag/branch/commit or direct URL)
|
||||||
|
- Any special configuration required to reproduce the issue
|
||||||
|
- Step-by-step instructions to reproduce the issue
|
||||||
|
- Proof-of-concept or exploit code (if possible)
|
||||||
|
- Impact of the issue, including how an attacker might exploit it
|
||||||
|
|
||||||
|
### Response Timeline
|
||||||
|
|
||||||
|
- **Initial Response**: We will acknowledge your report within 48 hours
|
||||||
|
- **Status Updates**: We will provide status updates at least every 5 business days
|
||||||
|
- **Resolution**: We aim to resolve critical vulnerabilities within 30 days of disclosure
|
||||||
|
|
||||||
|
### What to Expect
|
||||||
|
|
||||||
|
After you submit a report:
|
||||||
|
|
||||||
|
1. We will confirm receipt of your vulnerability report and may ask for additional information
|
||||||
|
2. We will investigate the issue and determine its impact and severity
|
||||||
|
3. We will work on a fix and coordinate disclosure timing with you
|
||||||
|
4. Once the fix is ready, we will release it and publicly acknowledge your contribution (unless you prefer to remain anonymous)
|
||||||
|
|
||||||
|
## Security Considerations for Deployment
|
||||||
|
|
||||||
|
### Authentication & Authorization
|
||||||
|
|
||||||
|
- **JWT Tokens**: Ensure `JWT_SECRET` environment variable is set to a strong, random value (minimum 256 bits)
|
||||||
|
- **OAuth Credentials**: Keep OAuth client secrets secure and never commit them to version control
|
||||||
|
- **Session Management**: Configure appropriate session timeout values
|
||||||
|
|
||||||
|
### Database Security
|
||||||
|
|
||||||
|
- Use strong database passwords
|
||||||
|
- Never expose database ports publicly
|
||||||
|
- Use database connection encryption when available
|
||||||
|
- Regularly backup your database
|
||||||
|
|
||||||
|
### API Security
|
||||||
|
|
||||||
|
- Enable rate limiting to prevent abuse
|
||||||
|
- Validate all user inputs on both client and server side
|
||||||
|
- Use HTTPS in production environments
|
||||||
|
- Configure CORS properly to restrict origins
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
The following sensitive environment variables should be kept secure:
|
||||||
|
|
||||||
|
- `JWT_SECRET` - JWT signing key
|
||||||
|
- `GOOGLE_CLIENT_SECRET` - Google OAuth credentials
|
||||||
|
- `GITHUB_CLIENT_SECRET` - GitHub OAuth credentials
|
||||||
|
- `DISCORD_CLIENT_SECRET` - Discord OAuth credentials
|
||||||
|
- `TWITTER_CLIENT_SECRET` - Twitter OAuth credentials
|
||||||
|
- `WEBPUSH_PRIVATE_KEY` - Web push notification private key
|
||||||
|
- Database connection strings and credentials
|
||||||
|
- Cloud storage credentials (Tencent COS)
|
||||||
|
|
||||||
|
**Never commit these values to version control or expose them in logs.**
|
||||||
|
|
||||||
|
### File Upload Security
|
||||||
|
|
||||||
|
- Validate file types and sizes
|
||||||
|
- Scan uploaded files for malware
|
||||||
|
- Store uploaded files outside the web root
|
||||||
|
- Use cloud storage with proper access controls
|
||||||
|
|
||||||
|
### Password Security
|
||||||
|
|
||||||
|
- Configure password strength requirements via environment variables
|
||||||
|
- Use bcrypt or similar strong hashing algorithms (already implemented in Spring Security)
|
||||||
|
- Implement account lockout after failed login attempts
|
||||||
|
|
||||||
|
### Web Push Notifications
|
||||||
|
|
||||||
|
- Keep `WEBPUSH_PRIVATE_KEY` secret and secure
|
||||||
|
- Only send notifications to users who have explicitly opted in
|
||||||
|
- Validate notification payloads
|
||||||
|
|
||||||
|
### Dependency Management
|
||||||
|
|
||||||
|
- Regularly update dependencies to patch known vulnerabilities
|
||||||
|
- Run `mvn dependency-check:check` to scan for vulnerable dependencies
|
||||||
|
- Monitor GitHub security advisories for this project
|
||||||
|
|
||||||
|
### Production Deployment Checklist
|
||||||
|
|
||||||
|
- [ ] Use HTTPS/TLS for all connections
|
||||||
|
- [ ] Set strong, unique secrets for all environment variables
|
||||||
|
- [ ] Enable CSRF protection
|
||||||
|
- [ ] Configure secure headers (CSP, X-Frame-Options, etc.)
|
||||||
|
- [ ] Disable debug mode and verbose error messages
|
||||||
|
- [ ] Set up proper logging and monitoring
|
||||||
|
- [ ] Implement rate limiting and DDoS protection
|
||||||
|
- [ ] Regular security updates and patches
|
||||||
|
- [ ] Database backups and disaster recovery plan
|
||||||
|
- [ ] Restrict admin access to trusted IPs when possible
|
||||||
|
|
||||||
|
## Known Security Features
|
||||||
|
|
||||||
|
OpenIsle includes the following security features:
|
||||||
|
|
||||||
|
- JWT-based authentication with configurable expiration
|
||||||
|
- OAuth 2.0 integration with major providers
|
||||||
|
- Password strength validation
|
||||||
|
- Protection codes for sensitive operations
|
||||||
|
- Input validation and sanitization
|
||||||
|
- SQL injection prevention through ORM (JPA/Hibernate)
|
||||||
|
- XSS protection in Vue.js templates
|
||||||
|
- CSRF protection (Spring Security)
|
||||||
|
|
||||||
|
## Security Best Practices for Contributors
|
||||||
|
|
||||||
|
- Never commit credentials, API keys, or secrets
|
||||||
|
- Follow secure coding practices (OWASP Top 10)
|
||||||
|
- Validate and sanitize all user inputs
|
||||||
|
- Use parameterized queries for database operations
|
||||||
|
- Implement proper error handling without exposing sensitive information
|
||||||
|
- Write security tests for new features
|
||||||
|
- Review code for security issues before submitting PRs
|
||||||
|
|
||||||
|
## Disclosure Policy
|
||||||
|
|
||||||
|
When we receive a security bug report, we will:
|
||||||
|
|
||||||
|
1. Confirm the problem and determine affected versions
|
||||||
|
2. Audit code to find any similar problems
|
||||||
|
3. Prepare fixes for all supported versions
|
||||||
|
4. Release patches as soon as possible
|
||||||
|
|
||||||
|
We appreciate your help in keeping OpenIsle and its users safe!
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
We believe in recognizing security researchers who help improve OpenIsle's security. With your permission, we will acknowledge your contribution in:
|
||||||
|
|
||||||
|
- Security advisory
|
||||||
|
- Release notes
|
||||||
|
- A security hall of fame (if established)
|
||||||
|
|
||||||
|
If you prefer to remain anonymous, we will respect your wishes.
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
For any security-related questions or concerns, please reach out through the channels mentioned above.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Thank you for helping keep OpenIsle secure!
|
||||||
23
backend/.prettierrc
Normal file
23
backend/.prettierrc
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 100,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"proseWrap": "preserve",
|
||||||
|
"plugins": ["prettier-plugin-java"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.java",
|
||||||
|
"options": {
|
||||||
|
"printWidth": 100,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"trailingComma": "es5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
# 所有环境变量已集中在仓库根目录的 .env.*.example 文件。
|
||||||
|
# 此文件保留作参考用途,如需在 Docker 之外手动配置,可按需复制。
|
||||||
|
|
||||||
# === Spring Boot ===
|
# === Spring Boot ===
|
||||||
SERVER_PORT=8080
|
SERVER_PORT=8080
|
||||||
|
|
||||||
@@ -16,6 +19,7 @@ JWT_EXPIRATION=2592000000
|
|||||||
# === Redis ===
|
# === Redis ===
|
||||||
REDIS_HOST=<Redis 地址>
|
REDIS_HOST=<Redis 地址>
|
||||||
REDIS_PORT=<Redis 端口>
|
REDIS_PORT=<Redis 端口>
|
||||||
|
REDIS_PASS=<Redis 密码>
|
||||||
|
|
||||||
# === Resend ===
|
# === Resend ===
|
||||||
RESEND_API_KEY=<你的resend-api-key>
|
RESEND_API_KEY=<你的resend-api-key>
|
||||||
|
|||||||
@@ -132,6 +132,23 @@
|
|||||||
<artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
|
<artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
|
||||||
<version>2.2.0</version>
|
<version>2.2.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<!-- 高阶 Java 客户端 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.opensearch.client</groupId>
|
||||||
|
<artifactId>opensearch-java</artifactId>
|
||||||
|
<version>3.2.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 低阶 RestClient,提供 org.opensearch.client.RestClient 给你的 RestClientTransport 用 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.opensearch.client</groupId>
|
||||||
|
<artifactId>opensearch-rest-client</artifactId>
|
||||||
|
<version>3.2.0</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import org.springframework.scheduling.annotation.EnableScheduling;
|
|||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@EnableScheduling
|
@EnableScheduling
|
||||||
public class OpenIsleApplication {
|
public class OpenIsleApplication {
|
||||||
public static void main(String[] args) {
|
|
||||||
SpringApplication.run(OpenIsleApplication.class, args);
|
public static void main(String[] args) {
|
||||||
}
|
SpringApplication.run(OpenIsleApplication.class, args);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,37 +3,40 @@ package com.openisle.config;
|
|||||||
import com.openisle.model.Activity;
|
import com.openisle.model.Activity;
|
||||||
import com.openisle.model.ActivityType;
|
import com.openisle.model.ActivityType;
|
||||||
import com.openisle.repository.ActivityRepository;
|
import com.openisle.repository.ActivityRepository;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.boot.CommandLineRunner;
|
import org.springframework.boot.CommandLineRunner;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ActivityInitializer implements CommandLineRunner {
|
public class ActivityInitializer implements CommandLineRunner {
|
||||||
private final ActivityRepository activityRepository;
|
|
||||||
|
|
||||||
@Override
|
private final ActivityRepository activityRepository;
|
||||||
public void run(String... args) {
|
|
||||||
if (activityRepository.findByType(ActivityType.MILK_TEA) == null) {
|
|
||||||
Activity a = new Activity();
|
|
||||||
a.setTitle("🎡建站送奶茶活动");
|
|
||||||
a.setType(ActivityType.MILK_TEA);
|
|
||||||
a.setIcon("https://icons.veryicon.com/png/o/food--drinks/delicious-food-1/coffee-36.png");
|
|
||||||
a.setContent("为了有利于建站推广以及激励发布内容,我们推出了建站送奶茶的活动,前50名达到level 1的用户,可以联系站长获取奶茶/咖啡一杯");
|
|
||||||
activityRepository.save(a);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activityRepository.findByType(ActivityType.INVITE_POINTS) == null) {
|
@Override
|
||||||
Activity a = new Activity();
|
public void run(String... args) {
|
||||||
a.setTitle("🎁邀请码送积分活动");
|
if (activityRepository.findByType(ActivityType.MILK_TEA) == null) {
|
||||||
a.setType(ActivityType.INVITE_POINTS);
|
Activity a = new Activity();
|
||||||
a.setIcon("https://img.icons8.com/color/96/gift.png");
|
a.setTitle("🎡建站送奶茶活动");
|
||||||
a.setContent("使用邀请码注册或邀请好友即可获得积分奖励,快来参与吧!");
|
a.setType(ActivityType.MILK_TEA);
|
||||||
a.setStartTime(LocalDateTime.now());
|
a.setIcon("https://icons.veryicon.com/png/o/food--drinks/delicious-food-1/coffee-36.png");
|
||||||
a.setEndTime(LocalDate.of(LocalDate.now().getYear(), 10, 1).atStartOfDay());
|
a.setContent(
|
||||||
activityRepository.save(a);
|
"为了有利于建站推广以及激励发布内容,我们推出了建站送奶茶的活动,前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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
package com.openisle.config;
|
package com.openisle.config;
|
||||||
|
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.scheduling.annotation.EnableAsync;
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
|
|
||||||
import java.util.concurrent.Executor;
|
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableAsync
|
@EnableAsync
|
||||||
public class AsyncConfig {
|
public class AsyncConfig {
|
||||||
@Bean(name = "notificationExecutor")
|
|
||||||
public Executor notificationExecutor() {
|
@Bean(name = "notificationExecutor")
|
||||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
public Executor notificationExecutor() {
|
||||||
executor.setCorePoolSize(2);
|
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||||
executor.setMaxPoolSize(10);
|
executor.setCorePoolSize(2);
|
||||||
executor.setQueueCapacity(100);
|
executor.setMaxPoolSize(10);
|
||||||
executor.setThreadNamePrefix("notification-");
|
executor.setQueueCapacity(100);
|
||||||
executor.initialize();
|
executor.setThreadNamePrefix("notification-");
|
||||||
return executor;
|
executor.initialize();
|
||||||
}
|
return executor;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
|
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
|
||||||
import com.fasterxml.jackson.datatype.hibernate6.Hibernate6Module;
|
import com.fasterxml.jackson.datatype.hibernate6.Hibernate6Module;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
import org.springframework.cache.CacheManager;
|
import org.springframework.cache.CacheManager;
|
||||||
import org.springframework.cache.annotation.EnableCaching;
|
import org.springframework.cache.annotation.EnableCaching;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
@@ -21,10 +24,6 @@ import org.springframework.data.redis.serializer.RedisSerializationContext;
|
|||||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redis 缓存配置类
|
* Redis 缓存配置类
|
||||||
* @author smallclover
|
* @author smallclover
|
||||||
@@ -34,89 +33,107 @@ import java.util.Map;
|
|||||||
@EnableCaching
|
@EnableCaching
|
||||||
public class CachingConfig {
|
public class CachingConfig {
|
||||||
|
|
||||||
// 标签缓存名
|
// 标签缓存名
|
||||||
public static final String TAG_CACHE_NAME="openisle_tags";
|
public static final String TAG_CACHE_NAME = "openisle_tags";
|
||||||
// 分类缓存名
|
// 分类缓存名
|
||||||
public static final String CATEGORY_CACHE_NAME="openisle_categories";
|
public static final String CATEGORY_CACHE_NAME = "openisle_categories";
|
||||||
// 在线人数缓存名
|
// 在线人数缓存名
|
||||||
public static final String ONLINE_CACHE_NAME="openisle_online";
|
public static final String ONLINE_CACHE_NAME = "openisle_online";
|
||||||
// 注册验证码
|
// 注册验证码
|
||||||
public static final String VERIFY_CACHE_NAME="openisle_verify";
|
public static final String VERIFY_CACHE_NAME = "openisle_verify";
|
||||||
// 发帖频率限制
|
// 发帖频率限制
|
||||||
public static final String LIMIT_CACHE_NAME="openisle_limit";
|
public static final String LIMIT_CACHE_NAME = "openisle_limit";
|
||||||
// 用户访问统计
|
// 用户访问统计
|
||||||
public static final String VISIT_CACHE_NAME="openisle_visit";
|
public static final String VISIT_CACHE_NAME = "openisle_visit";
|
||||||
|
// 文章缓存
|
||||||
|
public static final String POST_CACHE_NAME = "openisle_posts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 自定义Redis的序列化器
|
* 自定义Redis的序列化器
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
@Bean()
|
@Bean
|
||||||
@Primary
|
@Primary
|
||||||
public RedisSerializer<Object> redisSerializer() {
|
public RedisSerializer<Object> redisSerializer() {
|
||||||
// 注册 JavaTimeModule 來支持 Java 8 的日期和时间 API,否则回报一下错误,同时还要引入jsr310
|
// 注册 JavaTimeModule 來支持 Java 8 的日期和时间 API,否则回报一下错误,同时还要引入jsr310
|
||||||
|
|
||||||
// org.springframework.data.redis.serializer.SerializationException: Could not write JSON: Java 8 date/time type `java.time.LocalDateTime` not supported by default:
|
// org.springframework.data.redis.serializer.SerializationException: Could not write JSON: Java 8 date/time type `java.time.LocalDateTime` not supported by default:
|
||||||
// add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling
|
// add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling
|
||||||
// (through reference chain: java.util.ArrayList[0]->com.openisle.dto.TagDto["createdAt"])
|
// (through reference chain: java.util.ArrayList[0]->com.openisle.dto.TagDto["createdAt"])
|
||||||
// 设置可见性,允许序列化所有元素
|
// 设置可见性,允许序列化所有元素
|
||||||
ObjectMapper objectMapper = new ObjectMapper();
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
objectMapper.registerModule(new JavaTimeModule());
|
objectMapper.registerModule(new JavaTimeModule());
|
||||||
// Hibernate6Module 可以自动处理懒加载代理对象。
|
// Hibernate6Module 可以自动处理懒加载代理对象。
|
||||||
// Tag对象的creator是FetchType.LAZY
|
// Tag对象的creator是FetchType.LAZY
|
||||||
objectMapper.registerModule(new Hibernate6Module()
|
objectMapper.registerModule(
|
||||||
.disable(Hibernate6Module.Feature.USE_TRANSIENT_ANNOTATION));
|
new Hibernate6Module()
|
||||||
// service的时候带上类型信息
|
.disable(Hibernate6Module.Feature.USE_TRANSIENT_ANNOTATION)
|
||||||
// 启用类型信息,避免 LinkedHashMap 问题
|
// 将 Hibernate 特有的集合类型转换为标准 Java 集合类型
|
||||||
objectMapper.activateDefaultTyping(
|
// 避免序列化时出现 org.hibernate.collection.spi.PersistentSet 这样的类型信息
|
||||||
LaissezFaireSubTypeValidator.instance,
|
.configure(Hibernate6Module.Feature.REPLACE_PERSISTENT_COLLECTIONS, true)
|
||||||
ObjectMapper.DefaultTyping.NON_FINAL,
|
);
|
||||||
JsonTypeInfo.As.PROPERTY
|
// service的时候带上类型信息
|
||||||
);
|
// 启用类型信息,避免 LinkedHashMap 问题
|
||||||
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
|
objectMapper.activateDefaultTyping(
|
||||||
return new GenericJackson2JsonRedisSerializer(objectMapper);
|
LaissezFaireSubTypeValidator.instance,
|
||||||
}
|
ObjectMapper.DefaultTyping.NON_FINAL,
|
||||||
|
JsonTypeInfo.As.PROPERTY
|
||||||
|
);
|
||||||
|
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
|
||||||
|
return new GenericJackson2JsonRedisSerializer(objectMapper);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 配置 Spring Cache 使用 RedisCacheManager
|
* 配置 Spring Cache 使用 RedisCacheManager
|
||||||
*/
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
public CacheManager cacheManager(RedisConnectionFactory connectionFactory, RedisSerializer<Object> redisSerializer) {
|
public CacheManager cacheManager(
|
||||||
|
RedisConnectionFactory connectionFactory,
|
||||||
|
RedisSerializer<Object> redisSerializer
|
||||||
|
) {
|
||||||
|
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
|
||||||
|
.entryTtl(Duration.ZERO) // 默认缓存不过期
|
||||||
|
.serializeKeysWith(
|
||||||
|
RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())
|
||||||
|
)
|
||||||
|
.serializeValuesWith(
|
||||||
|
RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer)
|
||||||
|
)
|
||||||
|
.disableCachingNullValues(); // 禁止缓存 null 值
|
||||||
|
|
||||||
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
|
// 个别缓存单独设置 TTL 时间
|
||||||
.entryTtl(Duration.ZERO) // 默认缓存不过期
|
Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
|
||||||
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
|
RedisCacheConfiguration oneHourConfig = config.entryTtl(Duration.ofHours(1));
|
||||||
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
|
RedisCacheConfiguration tenMinutesConfig = config.entryTtl(Duration.ofMinutes(10));
|
||||||
.disableCachingNullValues(); // 禁止缓存 null 值
|
cacheConfigs.put(TAG_CACHE_NAME, oneHourConfig);
|
||||||
|
cacheConfigs.put(CATEGORY_CACHE_NAME, oneHourConfig);
|
||||||
|
cacheConfigs.put(POST_CACHE_NAME, tenMinutesConfig);
|
||||||
|
|
||||||
// 个别缓存单独设置 TTL 时间
|
return RedisCacheManager.builder(connectionFactory)
|
||||||
Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
|
.cacheDefaults(config)
|
||||||
RedisCacheConfiguration oneHourConfig = config.entryTtl(Duration.ofHours(1));
|
.withInitialCacheConfigurations(cacheConfigs)
|
||||||
cacheConfigs.put(TAG_CACHE_NAME, oneHourConfig);
|
.build();
|
||||||
cacheConfigs.put(CATEGORY_CACHE_NAME, oneHourConfig);
|
}
|
||||||
|
|
||||||
return RedisCacheManager.builder(connectionFactory)
|
/**
|
||||||
.cacheDefaults(config)
|
* 配置 RedisTemplate,支持直接操作 Redis
|
||||||
.withInitialCacheConfigurations(cacheConfigs)
|
*/
|
||||||
.build();
|
@Bean
|
||||||
}
|
public RedisTemplate<String, Object> redisTemplate(
|
||||||
|
RedisConnectionFactory connectionFactory,
|
||||||
|
RedisSerializer<Object> redisSerializer
|
||||||
|
) {
|
||||||
|
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||||
|
template.setConnectionFactory(connectionFactory);
|
||||||
|
|
||||||
/**
|
// key 和 hashKey 使用 String 序列化
|
||||||
* 配置 RedisTemplate,支持直接操作 Redis
|
template.setKeySerializer(new StringRedisSerializer());
|
||||||
*/
|
template.setHashKeySerializer(new StringRedisSerializer());
|
||||||
@Bean
|
|
||||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory, RedisSerializer<Object> redisSerializer) {
|
|
||||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
|
||||||
template.setConnectionFactory(connectionFactory);
|
|
||||||
|
|
||||||
// key 和 hashKey 使用 String 序列化
|
// value 和 hashValue 使用 JSON 序列化
|
||||||
template.setKeySerializer(new StringRedisSerializer());
|
template.setValueSerializer(redisSerializer);
|
||||||
template.setHashKeySerializer(new StringRedisSerializer());
|
template.setHashValueSerializer(redisSerializer);
|
||||||
|
|
||||||
// value 和 hashValue 使用 JSON 序列化
|
return template;
|
||||||
template.setValueSerializer(redisSerializer);
|
}
|
||||||
template.setHashValueSerializer(redisSerializer);
|
|
||||||
|
|
||||||
return template;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,24 +9,29 @@ import org.springframework.stereotype.Component;
|
|||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ChannelInitializer implements CommandLineRunner {
|
public class ChannelInitializer implements CommandLineRunner {
|
||||||
private final MessageConversationRepository conversationRepository;
|
|
||||||
|
|
||||||
@Override
|
private final MessageConversationRepository conversationRepository;
|
||||||
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();
|
@Override
|
||||||
tech.setChannel(true);
|
public void run(String... args) {
|
||||||
tech.setName("技术讨论群");
|
if (conversationRepository.countByChannelTrue() == 0) {
|
||||||
tech.setDescription("讨论技术相关话题");
|
MessageConversation chat = new MessageConversation();
|
||||||
tech.setAvatar("https://openisle-1307107697.cos.accelerate.myqcloud.com/dynamic_assert/5edde9a5864e471caa32491dbcdaa8b2.png");
|
chat.setChannel(true);
|
||||||
conversationRepository.save(tech);
|
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);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,23 +3,25 @@ package com.openisle.config;
|
|||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
import org.springframework.security.access.AccessDeniedException;
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
import org.springframework.security.web.access.AccessDeniedHandler;
|
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns 401 Unauthorized when an authenticated user lacks required privileges.
|
* Returns 401 Unauthorized when an authenticated user lacks required privileges.
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
|
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
|
||||||
@Override
|
|
||||||
public void handle(HttpServletRequest request,
|
@Override
|
||||||
HttpServletResponse response,
|
public void handle(
|
||||||
AccessDeniedException accessDeniedException) throws IOException, ServletException {
|
HttpServletRequest request,
|
||||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
HttpServletResponse response,
|
||||||
response.setContentType("application/json");
|
AccessDeniedException accessDeniedException
|
||||||
response.getWriter().write("{\"error\": \"Unauthorized\"}");
|
) throws IOException, ServletException {
|
||||||
}
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
|
response.setContentType("application/json");
|
||||||
|
response.getWriter().write("{\"error\": \"Unauthorized\"}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import io.swagger.v3.oas.models.info.Info;
|
|||||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||||
import io.swagger.v3.oas.models.servers.Server;
|
import io.swagger.v3.oas.models.servers.Server;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -18,43 +17,42 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class OpenApiConfig {
|
public class OpenApiConfig {
|
||||||
|
|
||||||
private final SpringDocProperties springDocProperties;
|
private final SpringDocProperties springDocProperties;
|
||||||
|
|
||||||
@Value("${springdoc.info.title}")
|
@Value("${springdoc.info.title}")
|
||||||
private String title;
|
private String title;
|
||||||
|
|
||||||
@Value("${springdoc.info.description}")
|
@Value("${springdoc.info.description}")
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
@Value("${springdoc.info.version}")
|
@Value("${springdoc.info.version}")
|
||||||
private String version;
|
private String version;
|
||||||
|
|
||||||
@Value("${springdoc.info.scheme}")
|
@Value("${springdoc.info.scheme}")
|
||||||
private String scheme;
|
private String scheme;
|
||||||
|
|
||||||
@Value("${springdoc.info.header}")
|
@Value("${springdoc.info.header}")
|
||||||
private String header;
|
private String header;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public OpenAPI openAPI() {
|
public OpenAPI openAPI() {
|
||||||
SecurityScheme securityScheme = new SecurityScheme()
|
SecurityScheme securityScheme = new SecurityScheme()
|
||||||
.type(SecurityScheme.Type.HTTP)
|
.type(SecurityScheme.Type.HTTP)
|
||||||
.scheme(scheme.toLowerCase())
|
.scheme(scheme.toLowerCase())
|
||||||
.bearerFormat("JWT")
|
.bearerFormat("JWT")
|
||||||
.in(SecurityScheme.In.HEADER)
|
.in(SecurityScheme.In.HEADER)
|
||||||
.name(header);
|
.name(header);
|
||||||
|
|
||||||
List<Server> servers = springDocProperties.getServers().stream()
|
List<Server> servers = springDocProperties
|
||||||
.map(s -> new Server().url(s.getUrl()).description(s.getDescription()))
|
.getServers()
|
||||||
.collect(Collectors.toList());
|
.stream()
|
||||||
|
.map(s -> new Server().url(s.getUrl()).description(s.getDescription()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
return new OpenAPI()
|
return new OpenAPI()
|
||||||
.servers(servers)
|
.servers(servers)
|
||||||
.info(new Info()
|
.info(new Info().title(title).description(description).version(version))
|
||||||
.title(title)
|
.components(new Components().addSecuritySchemes("JWT", securityScheme))
|
||||||
.description(description)
|
.addSecurityItem(new SecurityRequirement().addList("JWT"));
|
||||||
.version(version))
|
}
|
||||||
.components(new Components().addSecuritySchemes("JWT", securityScheme))
|
|
||||||
.addSecurityItem(new SecurityRequirement().addList("JWT"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,22 +10,27 @@ import org.springframework.stereotype.Component;
|
|||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class PointGoodInitializer implements CommandLineRunner {
|
public class PointGoodInitializer implements CommandLineRunner {
|
||||||
private final PointGoodRepository pointGoodRepository;
|
|
||||||
|
|
||||||
@Override
|
private final PointGoodRepository pointGoodRepository;
|
||||||
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();
|
@Override
|
||||||
g2.setName("奶茶");
|
public void run(String... args) {
|
||||||
g2.setCost(5000);
|
if (pointGoodRepository.count() == 0) {
|
||||||
g2.setImage("https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/icons/coffee.png");
|
PointGood g1 = new PointGood();
|
||||||
pointGoodRepository.save(g2);
|
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);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package com.openisle.config;
|
package com.openisle.config;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.amqp.core.Binding;
|
import org.springframework.amqp.core.Binding;
|
||||||
@@ -7,199 +11,209 @@ import org.springframework.amqp.core.BindingBuilder;
|
|||||||
import org.springframework.amqp.core.Queue;
|
import org.springframework.amqp.core.Queue;
|
||||||
import org.springframework.amqp.core.TopicExchange;
|
import org.springframework.amqp.core.TopicExchange;
|
||||||
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
|
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
|
||||||
|
import org.springframework.amqp.rabbit.core.RabbitAdmin;
|
||||||
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||||
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
|
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.boot.CommandLineRunner;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import org.springframework.amqp.rabbit.core.RabbitAdmin;
|
|
||||||
import org.springframework.boot.CommandLineRunner;
|
|
||||||
import org.springframework.context.annotation.DependsOn;
|
import org.springframework.context.annotation.DependsOn;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class RabbitMQConfig {
|
public class RabbitMQConfig {
|
||||||
|
|
||||||
public static final String EXCHANGE_NAME = "openisle-exchange";
|
public static final String EXCHANGE_NAME = "openisle-exchange";
|
||||||
// 保持向后兼容的常量
|
// 保持向后兼容的常量
|
||||||
public static final String QUEUE_NAME = "notifications-queue";
|
public static final String QUEUE_NAME = "notifications-queue";
|
||||||
public static final String ROUTING_KEY = "notifications.routingkey";
|
public static final String ROUTING_KEY = "notifications.routingkey";
|
||||||
|
|
||||||
// 硬编码为16以匹配ShardingStrategy中的十六进制分片逻辑
|
|
||||||
private final int queueCount = 16;
|
|
||||||
|
|
||||||
@Value("${rabbitmq.queue.durable}")
|
|
||||||
private boolean queueDurable;
|
|
||||||
|
|
||||||
@PostConstruct
|
// 硬编码为16以匹配ShardingStrategy中的十六进制分片逻辑
|
||||||
public void init() {
|
private final int queueCount = 16;
|
||||||
log.info("RabbitMQ配置初始化: 队列数量={}, 持久化={}", queueCount, queueDurable);
|
|
||||||
|
@Value("${rabbitmq.queue.durable}")
|
||||||
|
private boolean queueDurable;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
log.info("RabbitMQ配置初始化: 队列数量={}, 持久化={}", queueCount, queueDurable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public TopicExchange exchange() {
|
||||||
|
return new TopicExchange(EXCHANGE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建所有分片队列, 使用十六进制后缀 (0-f)
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public List<Queue> shardedQueues() {
|
||||||
|
log.info("开始创建分片队列 Bean...");
|
||||||
|
|
||||||
|
List<Queue> queues = new ArrayList<>();
|
||||||
|
for (int i = 0; i < queueCount; i++) {
|
||||||
|
String shardKey = Integer.toHexString(i);
|
||||||
|
String queueName = "notifications-queue-" + shardKey;
|
||||||
|
Queue queue = new Queue(queueName, queueDurable);
|
||||||
|
queues.add(queue);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
log.info("分片队列 Bean 创建完成,总数: {}", queues.size());
|
||||||
public TopicExchange exchange() {
|
return queues;
|
||||||
return new TopicExchange(EXCHANGE_NAME);
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建所有分片绑定, 使用十六进制路由键 (notifications.shard.0 - notifications.shard.f)
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public List<Binding> shardedBindings(
|
||||||
|
TopicExchange exchange,
|
||||||
|
@Qualifier("shardedQueues") List<Queue> shardedQueues
|
||||||
|
) {
|
||||||
|
log.info("开始创建分片绑定 Bean...");
|
||||||
|
List<Binding> bindings = new ArrayList<>();
|
||||||
|
if (shardedQueues != null) {
|
||||||
|
for (Queue queue : shardedQueues) {
|
||||||
|
String queueName = queue.getName();
|
||||||
|
String shardKey = queueName.substring("notifications-queue-".length());
|
||||||
|
String routingKey = "notifications.shard." + shardKey;
|
||||||
|
Binding binding = BindingBuilder.bind(queue).to(exchange).with(routingKey);
|
||||||
|
bindings.add(binding);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
log.info("分片绑定 Bean 创建完成,总数: {}", bindings.size());
|
||||||
* 创建所有分片队列, 使用十六进制后缀 (0-f)
|
return bindings;
|
||||||
*/
|
}
|
||||||
@Bean
|
|
||||||
public List<Queue> shardedQueues() {
|
|
||||||
log.info("开始创建分片队列 Bean...");
|
|
||||||
|
|
||||||
List<Queue> queues = new ArrayList<>();
|
|
||||||
for (int i = 0; i < queueCount; i++) {
|
|
||||||
String shardKey = Integer.toHexString(i);
|
|
||||||
String queueName = "notifications-queue-" + shardKey;
|
|
||||||
Queue queue = new Queue(queueName, queueDurable);
|
|
||||||
queues.add(queue);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("分片队列 Bean 创建完成,总数: {}", queues.size());
|
|
||||||
return queues;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建所有分片绑定, 使用十六进制路由键 (notifications.shard.0 - notifications.shard.f)
|
* 保持向后兼容的单队列配置(可选)
|
||||||
*/
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
public List<Binding> shardedBindings(TopicExchange exchange, @Qualifier("shardedQueues") List<Queue> shardedQueues) {
|
public Queue legacyQueue() {
|
||||||
log.info("开始创建分片绑定 Bean...");
|
return new Queue(QUEUE_NAME, queueDurable);
|
||||||
List<Binding> bindings = new ArrayList<>();
|
}
|
||||||
if (shardedQueues != null) {
|
|
||||||
for (Queue queue : shardedQueues) {
|
/**
|
||||||
String queueName = queue.getName();
|
* 保持向后兼容的单队列绑定(可选)
|
||||||
String shardKey = queueName.substring("notifications-queue-".length());
|
*/
|
||||||
String routingKey = "notifications.shard." + shardKey;
|
@Bean
|
||||||
Binding binding = BindingBuilder.bind(queue).to(exchange).with(routingKey);
|
public Binding legacyBinding(Queue legacyQueue, TopicExchange exchange) {
|
||||||
bindings.add(binding);
|
return BindingBuilder.bind(legacyQueue).to(exchange).with(ROUTING_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Jackson2JsonMessageConverter messageConverter() {
|
||||||
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
objectMapper.registerModule(new com.fasterxml.jackson.datatype.jsr310.JavaTimeModule());
|
||||||
|
objectMapper.disable(
|
||||||
|
com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS
|
||||||
|
);
|
||||||
|
return new Jackson2JsonMessageConverter(objectMapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory) {
|
||||||
|
return new RabbitAdmin(connectionFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
|
||||||
|
RabbitTemplate template = new RabbitTemplate(connectionFactory);
|
||||||
|
template.setMessageConverter(messageConverter());
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 CommandLineRunner 确保在应用完全启动后声明队列到 RabbitMQ
|
||||||
|
* 这样可以确保 RabbitAdmin 和所有 Bean 都已正确初始化
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
@DependsOn({ "rabbitAdmin", "shardedQueues", "exchange" })
|
||||||
|
public CommandLineRunner queueDeclarationRunner(
|
||||||
|
RabbitAdmin rabbitAdmin,
|
||||||
|
@Qualifier("shardedQueues") List<Queue> shardedQueues,
|
||||||
|
TopicExchange exchange,
|
||||||
|
Queue legacyQueue,
|
||||||
|
@Qualifier("shardedBindings") List<Binding> shardedBindings,
|
||||||
|
Binding legacyBinding
|
||||||
|
) {
|
||||||
|
return args -> {
|
||||||
|
log.info("=== 开始主动声明 RabbitMQ 组件 ===");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 声明交换
|
||||||
|
rabbitAdmin.declareExchange(exchange);
|
||||||
|
|
||||||
|
// 声明分片队列 - 检查存在性
|
||||||
|
log.info("开始检查并声明 {} 个分片队列...", shardedQueues.size());
|
||||||
|
int successCount = 0;
|
||||||
|
int skippedCount = 0;
|
||||||
|
|
||||||
|
for (Queue queue : shardedQueues) {
|
||||||
|
String queueName = queue.getName();
|
||||||
|
try {
|
||||||
|
// 使用 declareQueue 的返回值判断队列是否已存在
|
||||||
|
// 如果队列已存在且配置匹配,declareQueue 会返回现有队列信息
|
||||||
|
// 如果不匹配或不存在,会创建新队列
|
||||||
|
rabbitAdmin.declareQueue(queue);
|
||||||
|
successCount++;
|
||||||
|
} catch (org.springframework.amqp.AmqpIOException e) {
|
||||||
|
if (
|
||||||
|
e.getMessage().contains("PRECONDITION_FAILED") && e.getMessage().contains("durable")
|
||||||
|
) {
|
||||||
|
skippedCount++;
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("队列声明失败: {}, 错误: {}", queueName, e.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
log.info(
|
||||||
log.info("分片绑定 Bean 创建完成,总数: {}", bindings.size());
|
"分片队列处理完成: 成功 {}, 跳过 {}, 总数 {}",
|
||||||
return bindings;
|
successCount,
|
||||||
}
|
skippedCount,
|
||||||
|
shardedQueues.size()
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
// 声明分片绑定
|
||||||
* 保持向后兼容的单队列配置(可选)
|
log.info("开始声明 {} 个分片绑定...", shardedBindings.size());
|
||||||
*/
|
int bindingSuccessCount = 0;
|
||||||
@Bean
|
for (Binding binding : shardedBindings) {
|
||||||
public Queue legacyQueue() {
|
try {
|
||||||
return new Queue(QUEUE_NAME, queueDurable);
|
rabbitAdmin.declareBinding(binding);
|
||||||
}
|
bindingSuccessCount++;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("绑定声明失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.info("分片绑定声明完成: 成功 {}/{}", bindingSuccessCount, shardedBindings.size());
|
||||||
|
|
||||||
/**
|
// 声明遗留队列和绑定 - 检查存在性
|
||||||
* 保持向后兼容的单队列绑定(可选)
|
try {
|
||||||
*/
|
rabbitAdmin.declareQueue(legacyQueue);
|
||||||
@Bean
|
rabbitAdmin.declareBinding(legacyBinding);
|
||||||
public Binding legacyBinding(Queue legacyQueue, TopicExchange exchange) {
|
log.info("遗留队列和绑定就绪: {} (已存在或新创建)", QUEUE_NAME);
|
||||||
return BindingBuilder.bind(legacyQueue).to(exchange).with(ROUTING_KEY);
|
} catch (org.springframework.amqp.AmqpIOException e) {
|
||||||
}
|
if (
|
||||||
|
e.getMessage().contains("PRECONDITION_FAILED") && e.getMessage().contains("durable")
|
||||||
|
) {
|
||||||
|
log.warn("遗留队列已存在但 durable 设置不匹配: {}, 保持现有队列", QUEUE_NAME);
|
||||||
|
} else {
|
||||||
|
log.error("遗留队列声明失败: {}, 错误: {}", QUEUE_NAME, e.getMessage());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("遗留队列声明失败: {}, 错误: {}", QUEUE_NAME, e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
log.info("=== RabbitMQ 组件声明完成 ===");
|
||||||
public Jackson2JsonMessageConverter messageConverter() {
|
log.info("请检查 RabbitMQ 管理界面确认队列已正确创建");
|
||||||
ObjectMapper objectMapper = new ObjectMapper();
|
} catch (Exception e) {
|
||||||
objectMapper.registerModule(new com.fasterxml.jackson.datatype.jsr310.JavaTimeModule());
|
log.error("RabbitMQ 组件声明过程中发生严重错误", e);
|
||||||
objectMapper.disable(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
}
|
||||||
return new Jackson2JsonMessageConverter(objectMapper);
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@Bean
|
|
||||||
public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory) {
|
|
||||||
return new RabbitAdmin(connectionFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
|
|
||||||
RabbitTemplate template = new RabbitTemplate(connectionFactory);
|
|
||||||
template.setMessageConverter(messageConverter());
|
|
||||||
return template;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 使用 CommandLineRunner 确保在应用完全启动后声明队列到 RabbitMQ
|
|
||||||
* 这样可以确保 RabbitAdmin 和所有 Bean 都已正确初始化
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
@DependsOn({"rabbitAdmin", "shardedQueues", "exchange"})
|
|
||||||
public CommandLineRunner queueDeclarationRunner(RabbitAdmin rabbitAdmin,
|
|
||||||
@Qualifier("shardedQueues") List<Queue> shardedQueues,
|
|
||||||
TopicExchange exchange,
|
|
||||||
Queue legacyQueue,
|
|
||||||
@Qualifier("shardedBindings") List<Binding> shardedBindings,
|
|
||||||
Binding legacyBinding) {
|
|
||||||
return args -> {
|
|
||||||
log.info("=== 开始主动声明 RabbitMQ 组件 ===");
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 声明交换
|
|
||||||
rabbitAdmin.declareExchange(exchange);
|
|
||||||
|
|
||||||
// 声明分片队列 - 检查存在性
|
|
||||||
log.info("开始检查并声明 {} 个分片队列...", shardedQueues.size());
|
|
||||||
int successCount = 0;
|
|
||||||
int skippedCount = 0;
|
|
||||||
|
|
||||||
for (Queue queue : shardedQueues) {
|
|
||||||
String queueName = queue.getName();
|
|
||||||
try {
|
|
||||||
// 使用 declareQueue 的返回值判断队列是否已存在
|
|
||||||
// 如果队列已存在且配置匹配,declareQueue 会返回现有队列信息
|
|
||||||
// 如果不匹配或不存在,会创建新队列
|
|
||||||
rabbitAdmin.declareQueue(queue);
|
|
||||||
successCount++;
|
|
||||||
} catch (org.springframework.amqp.AmqpIOException e) {
|
|
||||||
if (e.getMessage().contains("PRECONDITION_FAILED") && e.getMessage().contains("durable")) {
|
|
||||||
skippedCount++;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("队列声明失败: {}, 错误: {}", queueName, e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.info("分片队列处理完成: 成功 {}, 跳过 {}, 总数 {}", successCount, skippedCount, shardedQueues.size());
|
|
||||||
|
|
||||||
// 声明分片绑定
|
|
||||||
log.info("开始声明 {} 个分片绑定...", shardedBindings.size());
|
|
||||||
int bindingSuccessCount = 0;
|
|
||||||
for (Binding binding : shardedBindings) {
|
|
||||||
try {
|
|
||||||
rabbitAdmin.declareBinding(binding);
|
|
||||||
bindingSuccessCount++;
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("绑定声明失败: {}", e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.info("分片绑定声明完成: 成功 {}/{}", bindingSuccessCount, shardedBindings.size());
|
|
||||||
|
|
||||||
// 声明遗留队列和绑定 - 检查存在性
|
|
||||||
try {
|
|
||||||
rabbitAdmin.declareQueue(legacyQueue);
|
|
||||||
rabbitAdmin.declareBinding(legacyBinding);
|
|
||||||
log.info("遗留队列和绑定就绪: {} (已存在或新创建)", QUEUE_NAME);
|
|
||||||
} catch (org.springframework.amqp.AmqpIOException e) {
|
|
||||||
if (e.getMessage().contains("PRECONDITION_FAILED") && e.getMessage().contains("durable")) {
|
|
||||||
log.warn("遗留队列已存在但 durable 设置不匹配: {}, 保持现有队列", QUEUE_NAME);
|
|
||||||
} else {
|
|
||||||
log.error("遗留队列声明失败: {}, 错误: {}", QUEUE_NAME, e.getMessage());
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("遗留队列声明失败: {}, 错误: {}", QUEUE_NAME, e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("=== RabbitMQ 组件声明完成 ===");
|
|
||||||
log.info("请检查 RabbitMQ 管理界面确认队列已正确创建");
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("RabbitMQ 组件声明过程中发生严重错误", e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -13,23 +13,23 @@ import org.springframework.stereotype.Component;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class RedisConnectionLogger implements InitializingBean {
|
public class RedisConnectionLogger implements InitializingBean {
|
||||||
|
|
||||||
private final RedisConnectionFactory connectionFactory;
|
private final RedisConnectionFactory connectionFactory;
|
||||||
|
|
||||||
public RedisConnectionLogger(RedisConnectionFactory connectionFactory) {
|
public RedisConnectionLogger(RedisConnectionFactory connectionFactory) {
|
||||||
this.connectionFactory = connectionFactory;
|
this.connectionFactory = connectionFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterPropertiesSet() {
|
public void afterPropertiesSet() {
|
||||||
try (var connection = connectionFactory.getConnection()) {
|
try (var connection = connectionFactory.getConnection()) {
|
||||||
connection.ping();
|
connection.ping();
|
||||||
if (connectionFactory instanceof LettuceConnectionFactory lettuce) {
|
if (connectionFactory instanceof LettuceConnectionFactory lettuce) {
|
||||||
log.info("Redis connection established at {}:{}", lettuce.getHostName(), lettuce.getPort());
|
log.info("Redis connection established at {}:{}", lettuce.getHostName(), lettuce.getPort());
|
||||||
} else {
|
} else {
|
||||||
log.info("Redis connection established");
|
log.info("Redis connection established");
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Failed to connect to Redis", e);
|
log.error("Failed to connect to Redis", e);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,19 +2,20 @@ package com.openisle.config;
|
|||||||
|
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.scheduling.TaskScheduler;
|
||||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||||
import org.springframework.scheduling.TaskScheduler;
|
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableScheduling
|
@EnableScheduling
|
||||||
public class SchedulerConfig {
|
public class SchedulerConfig {
|
||||||
@Bean
|
|
||||||
public TaskScheduler taskScheduler() {
|
@Bean
|
||||||
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
public TaskScheduler taskScheduler() {
|
||||||
scheduler.setPoolSize(2);
|
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
||||||
scheduler.setThreadNamePrefix("lottery-");
|
scheduler.setPoolSize(2);
|
||||||
scheduler.initialize();
|
scheduler.setThreadNamePrefix("lottery-");
|
||||||
return scheduler;
|
scheduler.initialize();
|
||||||
}
|
return scheduler;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
package com.openisle.config;
|
package com.openisle.config;
|
||||||
|
|
||||||
|
import com.openisle.repository.UserRepository;
|
||||||
import com.openisle.service.JwtService;
|
import com.openisle.service.JwtService;
|
||||||
import com.openisle.service.UserVisitService;
|
import com.openisle.service.UserVisitService;
|
||||||
import com.openisle.repository.UserRepository;
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
@@ -22,202 +30,273 @@ import org.springframework.security.crypto.password.PasswordEncoder;
|
|||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.access.AccessDeniedHandler;
|
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
|
||||||
import org.springframework.web.cors.CorsConfiguration;
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
import org.springframework.web.cors.CorsConfigurationSource;
|
import org.springframework.web.cors.CorsConfigurationSource;
|
||||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import jakarta.servlet.FilterChain;
|
|
||||||
import jakarta.servlet.ServletException;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class SecurityConfig {
|
public class SecurityConfig {
|
||||||
private final JwtService jwtService;
|
|
||||||
private final UserRepository userRepository;
|
|
||||||
private final AccessDeniedHandler customAccessDeniedHandler;
|
|
||||||
private final UserVisitService userVisitService;
|
|
||||||
@Value("${app.website-url}")
|
|
||||||
private String websiteUrl;
|
|
||||||
|
|
||||||
private final RedisTemplate redisTemplate;
|
private final JwtService jwtService;
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final AccessDeniedHandler customAccessDeniedHandler;
|
||||||
|
private final UserVisitService userVisitService;
|
||||||
|
|
||||||
@Bean
|
@Value("${app.website-url}")
|
||||||
public PasswordEncoder passwordEncoder() {
|
private String websiteUrl;
|
||||||
return new BCryptPasswordEncoder();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
private final RedisTemplate redisTemplate;
|
||||||
public UserDetailsService userDetailsService() {
|
|
||||||
return username -> userRepository.findByUsername(username)
|
|
||||||
.<UserDetails>map(user -> org.springframework.security.core.userdetails.User
|
|
||||||
.withUsername(user.getUsername())
|
|
||||||
.password(user.getPassword())
|
|
||||||
.authorities(user.getRole().name())
|
|
||||||
.build())
|
|
||||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public AuthenticationManager authenticationManager(HttpSecurity http, PasswordEncoder passwordEncoder, UserDetailsService userDetailsService) throws Exception {
|
public PasswordEncoder passwordEncoder() {
|
||||||
return http.getSharedObject(AuthenticationManagerBuilder.class)
|
return new BCryptPasswordEncoder();
|
||||||
.userDetailsService(userDetailsService)
|
}
|
||||||
.passwordEncoder(passwordEncoder)
|
|
||||||
.and()
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public CorsConfigurationSource corsConfigurationSource() {
|
public UserDetailsService userDetailsService() {
|
||||||
CorsConfiguration cfg = new CorsConfiguration();
|
return username ->
|
||||||
cfg.setAllowedOrigins(List.of(
|
userRepository
|
||||||
"http://127.0.0.1:8080",
|
.findByUsername(username)
|
||||||
"http://127.0.0.1:8081",
|
.<UserDetails>map(user ->
|
||||||
"http://127.0.0.1:8082",
|
org.springframework.security.core.userdetails.User.withUsername(user.getUsername())
|
||||||
"http://127.0.0.1:3000",
|
.password(user.getPassword())
|
||||||
"http://127.0.0.1:3001",
|
.authorities(user.getRole().name())
|
||||||
"http://127.0.0.1",
|
.build()
|
||||||
"http://localhost:8080",
|
)
|
||||||
"http://localhost:8081",
|
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||||
"http://localhost:8082",
|
}
|
||||||
"http://localhost:3000",
|
|
||||||
"http://localhost:3001",
|
|
||||||
"http://localhost",
|
|
||||||
"http://30.211.97.238:3000",
|
|
||||||
"http://30.211.97.238",
|
|
||||||
"http://192.168.7.98",
|
|
||||||
"http://192.168.7.98:3000",
|
|
||||||
"https://petstore.swagger.io",
|
|
||||||
// 允许自建OpenAPI地址
|
|
||||||
"https://docs.open-isle.com",
|
|
||||||
"https://www.docs.open-isle.com",
|
|
||||||
websiteUrl,
|
|
||||||
websiteUrl.replace("://www.", "://")
|
|
||||||
));
|
|
||||||
cfg.setAllowedMethods(List.of("GET","POST","PUT","DELETE","OPTIONS"));
|
|
||||||
cfg.setAllowedHeaders(List.of("*"));
|
|
||||||
cfg.setAllowCredentials(true);
|
|
||||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
|
||||||
source.registerCorsConfiguration("/api/**", cfg);
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
public AuthenticationManager authenticationManager(
|
||||||
http.csrf(csrf -> csrf.disable())
|
HttpSecurity http,
|
||||||
.cors(Customizer.withDefaults())
|
PasswordEncoder passwordEncoder,
|
||||||
.headers(h -> h.frameOptions(f -> f.sameOrigin()))
|
UserDetailsService userDetailsService
|
||||||
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
) throws Exception {
|
||||||
.exceptionHandling(eh -> eh.accessDeniedHandler(customAccessDeniedHandler))
|
return http
|
||||||
.authorizeHttpRequests(auth -> auth
|
.getSharedObject(AuthenticationManagerBuilder.class)
|
||||||
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
|
.userDetailsService(userDetailsService)
|
||||||
.requestMatchers("/api/ws/**", "/api/sockjs/**").permitAll()
|
.passwordEncoder(passwordEncoder)
|
||||||
.requestMatchers("/api/v3/api-docs/**").permitAll()
|
.and()
|
||||||
.requestMatchers(HttpMethod.POST, "/api/auth/**").permitAll()
|
.build();
|
||||||
.requestMatchers(HttpMethod.GET, "/api/posts/**").permitAll()
|
}
|
||||||
.requestMatchers(HttpMethod.GET, "/api/comments/**").permitAll()
|
|
||||||
.requestMatchers(HttpMethod.GET, "/api/categories/**").permitAll()
|
|
||||||
.requestMatchers(HttpMethod.GET, "/api/tags/**").permitAll()
|
|
||||||
.requestMatchers(HttpMethod.GET, "/api/config/**").permitAll()
|
|
||||||
.requestMatchers(HttpMethod.POST,"/api/auth/google").permitAll()
|
|
||||||
.requestMatchers(HttpMethod.POST,"/api/auth/reason").permitAll()
|
|
||||||
.requestMatchers(HttpMethod.GET, "/api/search/**").permitAll()
|
|
||||||
.requestMatchers(HttpMethod.GET, "/api/users/**").permitAll()
|
|
||||||
.requestMatchers(HttpMethod.GET, "/api/medals/**").permitAll()
|
|
||||||
.requestMatchers(HttpMethod.GET, "/api/push/public-key").permitAll()
|
|
||||||
.requestMatchers(HttpMethod.GET, "/api/reaction-types").permitAll()
|
|
||||||
.requestMatchers(HttpMethod.GET, "/api/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/online/**").permitAll()
|
|
||||||
.requestMatchers(HttpMethod.POST, "/api/online/**").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")
|
|
||||||
.requestMatchers(HttpMethod.DELETE, "/api/tags/**").hasAuthority("ADMIN")
|
|
||||||
.requestMatchers(HttpMethod.GET, "/api/stats/**").hasAuthority("ADMIN")
|
|
||||||
.requestMatchers("/api/admin/**").hasAuthority("ADMIN")
|
|
||||||
.anyRequest().authenticated()
|
|
||||||
)
|
|
||||||
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
|
|
||||||
.addFilterAfter(userVisitFilter(), UsernamePasswordAuthenticationFilter.class);
|
|
||||||
return http.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public OncePerRequestFilter jwtAuthenticationFilter() {
|
public CorsConfigurationSource corsConfigurationSource() {
|
||||||
return new OncePerRequestFilter() {
|
CorsConfiguration cfg = new CorsConfiguration();
|
||||||
@Override
|
cfg.setAllowedOrigins(
|
||||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
List.of(
|
||||||
// 让预检请求直接通过
|
"http://127.0.0.1:8080",
|
||||||
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
|
"http://127.0.0.1:8081",
|
||||||
filterChain.doFilter(request, response);
|
"http://127.0.0.1:8082",
|
||||||
return;
|
"http://127.0.0.1:3000",
|
||||||
}
|
"http://127.0.0.1:3001",
|
||||||
String authHeader = request.getHeader("Authorization");
|
"http://127.0.0.1",
|
||||||
String uri = request.getRequestURI();
|
"http://localhost:8080",
|
||||||
|
"http://localhost:8081",
|
||||||
|
"http://localhost:8082",
|
||||||
|
"http://localhost:3000",
|
||||||
|
"http://frontend_dev:3000",
|
||||||
|
"http://frontend_service:3000",
|
||||||
|
"http://localhost:3001",
|
||||||
|
"http://localhost",
|
||||||
|
"http://30.211.97.238:3000",
|
||||||
|
"http://30.211.97.238",
|
||||||
|
"http://192.168.7.90",
|
||||||
|
"http://192.168.7.90:3000",
|
||||||
|
"https://petstore.swagger.io",
|
||||||
|
// 允许自建OpenAPI地址
|
||||||
|
"https://docs.open-isle.com",
|
||||||
|
"https://www.docs.open-isle.com",
|
||||||
|
websiteUrl,
|
||||||
|
websiteUrl.replace("://www.", "://")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
cfg.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||||
|
cfg.setAllowedHeaders(List.of("*"));
|
||||||
|
cfg.setAllowCredentials(true);
|
||||||
|
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||||
|
source.registerCorsConfiguration("/api/**", cfg);
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
boolean publicGet = "GET".equalsIgnoreCase(request.getMethod()) &&
|
@Bean
|
||||||
(uri.startsWith("/api/posts") || uri.startsWith("/api/comments") ||
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
uri.startsWith("/api/categories") || uri.startsWith("/api/tags") ||
|
http
|
||||||
uri.startsWith("/api/search") || uri.startsWith("/api/users") ||
|
.csrf(csrf -> csrf.disable())
|
||||||
uri.startsWith("/api/reaction-types") || uri.startsWith("/api/config") ||
|
.cors(Customizer.withDefaults())
|
||||||
uri.startsWith("/api/activities") || uri.startsWith("/api/push/public-key") ||
|
.headers(h -> h.frameOptions(f -> f.sameOrigin()))
|
||||||
uri.startsWith("/api/point-goods") || uri.startsWith("/api/channels") ||
|
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
uri.startsWith("/api/sitemap.xml") || uri.startsWith("/api/medals") ||
|
.exceptionHandling(eh -> eh.accessDeniedHandler(customAccessDeniedHandler))
|
||||||
uri.startsWith("/api/rss"));
|
.authorizeHttpRequests(auth ->
|
||||||
|
auth
|
||||||
|
.requestMatchers(HttpMethod.OPTIONS, "/**")
|
||||||
|
.permitAll()
|
||||||
|
.requestMatchers("/api/ws/**", "/api/sockjs/**")
|
||||||
|
.permitAll()
|
||||||
|
.requestMatchers("/api/v3/api-docs/**")
|
||||||
|
.permitAll()
|
||||||
|
.requestMatchers(HttpMethod.POST, "/api/auth/**")
|
||||||
|
.permitAll()
|
||||||
|
.requestMatchers(HttpMethod.GET, "/api/posts/**")
|
||||||
|
.permitAll()
|
||||||
|
.requestMatchers(HttpMethod.GET, "/api/comments/**")
|
||||||
|
.permitAll()
|
||||||
|
.requestMatchers(HttpMethod.GET, "/api/categories/**")
|
||||||
|
.permitAll()
|
||||||
|
.requestMatchers(HttpMethod.GET, "/api/tags/**")
|
||||||
|
.permitAll()
|
||||||
|
.requestMatchers(HttpMethod.GET, "/api/config/**")
|
||||||
|
.permitAll()
|
||||||
|
.requestMatchers(HttpMethod.POST, "/api/auth/google")
|
||||||
|
.permitAll()
|
||||||
|
.requestMatchers(HttpMethod.POST, "/api/auth/reason")
|
||||||
|
.permitAll()
|
||||||
|
.requestMatchers(HttpMethod.GET, "/api/search/**")
|
||||||
|
.permitAll()
|
||||||
|
.requestMatchers(HttpMethod.GET, "/api/users/**")
|
||||||
|
.permitAll()
|
||||||
|
.requestMatchers(HttpMethod.GET, "/api/medals/**")
|
||||||
|
.permitAll()
|
||||||
|
.requestMatchers(HttpMethod.GET, "/api/push/public-key")
|
||||||
|
.permitAll()
|
||||||
|
.requestMatchers(HttpMethod.GET, "/api/reaction-types")
|
||||||
|
.permitAll()
|
||||||
|
.requestMatchers(HttpMethod.GET, "/api/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/online/**")
|
||||||
|
.permitAll()
|
||||||
|
.requestMatchers(HttpMethod.POST, "/api/online/**")
|
||||||
|
.permitAll()
|
||||||
|
.requestMatchers(HttpMethod.GET, "/api/point-goods")
|
||||||
|
.permitAll()
|
||||||
|
.requestMatchers(HttpMethod.POST, "/api/point-goods")
|
||||||
|
.permitAll()
|
||||||
|
.requestMatchers("/actuator/**")
|
||||||
|
.permitAll()
|
||||||
|
.requestMatchers(HttpMethod.POST, "/api/categories/**")
|
||||||
|
.hasAuthority("ADMIN")
|
||||||
|
.requestMatchers(HttpMethod.POST, "/api/tags/**")
|
||||||
|
.authenticated()
|
||||||
|
.requestMatchers(HttpMethod.DELETE, "/api/categories/**")
|
||||||
|
.hasAuthority("ADMIN")
|
||||||
|
.requestMatchers(HttpMethod.DELETE, "/api/tags/**")
|
||||||
|
.hasAuthority("ADMIN")
|
||||||
|
.requestMatchers(HttpMethod.GET, "/api/stats/**")
|
||||||
|
.hasAuthority("ADMIN")
|
||||||
|
.requestMatchers("/api/admin/**")
|
||||||
|
.hasAuthority("ADMIN")
|
||||||
|
.anyRequest()
|
||||||
|
.authenticated()
|
||||||
|
)
|
||||||
|
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
|
||||||
|
.addFilterAfter(userVisitFilter(), UsernamePasswordAuthenticationFilter.class);
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
@Bean
|
||||||
String token = authHeader.substring(7);
|
public OncePerRequestFilter jwtAuthenticationFilter() {
|
||||||
try {
|
return new OncePerRequestFilter() {
|
||||||
String username = jwtService.validateAndGetSubject(token);
|
@Override
|
||||||
UserDetails userDetails = userDetailsService().loadUserByUsername(username);
|
protected void doFilterInternal(
|
||||||
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
|
HttpServletRequest request,
|
||||||
userDetails, null, userDetails.getAuthorities());
|
HttpServletResponse response,
|
||||||
org.springframework.security.core.context.SecurityContextHolder.getContext().setAuthentication(authToken);
|
FilterChain filterChain
|
||||||
} catch (Exception e) {
|
) throws ServletException, IOException {
|
||||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
// 让预检请求直接通过
|
||||||
response.setContentType("application/json");
|
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
|
||||||
response.getWriter().write("{\"error\": \"Invalid or expired token\"}");
|
filterChain.doFilter(request, response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (!uri.startsWith("/api/auth") && !publicGet
|
String authHeader = request.getHeader("Authorization");
|
||||||
&& !uri.startsWith("/api/ws") && !uri.startsWith("/api/sockjs")
|
String uri = request.getRequestURI();
|
||||||
&& !uri.startsWith("/api/v3/api-docs")
|
|
||||||
&& !uri.startsWith("/api/online")) {
|
|
||||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
|
||||||
response.setContentType("application/json");
|
|
||||||
response.getWriter().write("{\"error\": \"Missing token\"}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
filterChain.doFilter(request, response);
|
boolean publicGet =
|
||||||
}
|
"GET".equalsIgnoreCase(request.getMethod()) &&
|
||||||
};
|
(uri.startsWith("/api/posts") ||
|
||||||
}
|
uri.startsWith("/api/comments") ||
|
||||||
|
uri.startsWith("/api/categories") ||
|
||||||
|
uri.startsWith("/api/tags") ||
|
||||||
|
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/point-goods") ||
|
||||||
|
uri.startsWith("/api/channels") ||
|
||||||
|
uri.startsWith("/api/sitemap.xml") ||
|
||||||
|
uri.startsWith("/api/medals") ||
|
||||||
|
uri.startsWith("/actuator") ||
|
||||||
|
uri.startsWith("/api/rss"));
|
||||||
|
|
||||||
@Bean
|
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
||||||
public OncePerRequestFilter userVisitFilter() {
|
String token = authHeader.substring(7);
|
||||||
return new OncePerRequestFilter() {
|
try {
|
||||||
@Override
|
String username = jwtService.validateAndGetSubject(token);
|
||||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
UserDetails userDetails = userDetailsService().loadUserByUsername(username);
|
||||||
var auth = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication();
|
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
|
||||||
if (auth != null && auth.isAuthenticated() && !(auth instanceof org.springframework.security.authentication.AnonymousAuthenticationToken)) {
|
userDetails,
|
||||||
String key = CachingConfig.VISIT_CACHE_NAME+":"+ LocalDate.now();
|
null,
|
||||||
redisTemplate.opsForSet().add(key, auth.getName());
|
userDetails.getAuthorities()
|
||||||
}
|
);
|
||||||
filterChain.doFilter(request, response);
|
org.springframework.security.core.context.SecurityContextHolder.getContext().setAuthentication(
|
||||||
}
|
authToken
|
||||||
};
|
);
|
||||||
}
|
} catch (Exception e) {
|
||||||
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
|
response.setContentType("application/json");
|
||||||
|
response.getWriter().write("{\"error\": \"Invalid or expired token\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
!uri.startsWith("/api/auth") &&
|
||||||
|
!publicGet &&
|
||||||
|
!uri.startsWith("/api/ws") &&
|
||||||
|
!uri.startsWith("/api/sockjs") &&
|
||||||
|
!uri.startsWith("/api/v3/api-docs") &&
|
||||||
|
!uri.startsWith("/api/online")
|
||||||
|
) {
|
||||||
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
|
response.setContentType("application/json");
|
||||||
|
response.getWriter().write("{\"error\": \"Missing token\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public OncePerRequestFilter userVisitFilter() {
|
||||||
|
return new OncePerRequestFilter() {
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
FilterChain filterChain
|
||||||
|
) throws ServletException, IOException {
|
||||||
|
var auth =
|
||||||
|
org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
if (
|
||||||
|
auth != null &&
|
||||||
|
auth.isAuthenticated() &&
|
||||||
|
!(auth instanceof
|
||||||
|
org.springframework.security.authentication.AnonymousAuthenticationToken)
|
||||||
|
) {
|
||||||
|
String key = CachingConfig.VISIT_CACHE_NAME + ":" + LocalDate.now();
|
||||||
|
redisTemplate.opsForSet().add(key, auth.getName());
|
||||||
|
}
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import lombok.NoArgsConstructor;
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
public class ShardInfo {
|
public class ShardInfo {
|
||||||
private int shardIndex;
|
|
||||||
private String queueName;
|
private int shardIndex;
|
||||||
private String routingKey;
|
private String queueName;
|
||||||
}
|
private String routingKey;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,84 +1,87 @@
|
|||||||
package com.openisle.config;
|
package com.openisle.config;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class ShardingStrategy {
|
public class ShardingStrategy {
|
||||||
|
|
||||||
// 固定为16以匹配RabbitMQConfig中的十六进制分片逻辑
|
// 固定为16以匹配RabbitMQConfig中的十六进制分片逻辑
|
||||||
private static final int QUEUE_COUNT = 16;
|
private static final int QUEUE_COUNT = 16;
|
||||||
|
|
||||||
// 分片分布统计
|
// 分片分布统计
|
||||||
private final Map<Integer, AtomicLong> shardCounts = new ConcurrentHashMap<>();
|
private final Map<Integer, AtomicLong> shardCounts = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据用户名获取分片信息(基于哈希值首字符)
|
* 根据用户名获取分片信息(基于哈希值首字符)
|
||||||
*/
|
*/
|
||||||
public ShardInfo getShardInfo(String username) {
|
public ShardInfo getShardInfo(String username) {
|
||||||
if (username == null || username.isEmpty()) {
|
if (username == null || username.isEmpty()) {
|
||||||
// 空用户名默认分到第0个分片
|
// 空用户名默认分到第0个分片
|
||||||
return getShardInfoByIndex(0);
|
return getShardInfoByIndex(0);
|
||||||
}
|
|
||||||
|
|
||||||
// 计算用户名的哈希值并转为十六进制字符串
|
|
||||||
String hash = Integer.toHexString(Math.abs(username.hashCode()));
|
|
||||||
|
|
||||||
// 取哈希值的第一个字符 (0-9, a-f)
|
|
||||||
char firstChar = hash.charAt(0);
|
|
||||||
|
|
||||||
// 十六进制字符映射到队列
|
|
||||||
int shard = getShardFromHexChar(firstChar);
|
|
||||||
recordShardUsage(shard);
|
|
||||||
|
|
||||||
log.debug("Username '{}' -> hash '{}' -> firstChar '{}' -> shard {}",
|
|
||||||
username, hash, firstChar, shard);
|
|
||||||
|
|
||||||
return getShardInfoByIndex(shard);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将十六进制字符映射到分片索引
|
|
||||||
*/
|
|
||||||
private int getShardFromHexChar(char hexChar) {
|
|
||||||
int charValue;
|
|
||||||
if (hexChar >= '0' && hexChar <= '9') {
|
|
||||||
charValue = hexChar - '0'; // 0-9
|
|
||||||
} else if (hexChar >= 'a' && hexChar <= 'f') {
|
|
||||||
charValue = hexChar - 'a' + 10; // 10-15
|
|
||||||
} else {
|
|
||||||
// 异常情况,默认为0
|
|
||||||
charValue = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 映射到队列数量范围内
|
|
||||||
return charValue % QUEUE_COUNT;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据分片索引获取分片信息
|
|
||||||
*/
|
|
||||||
private ShardInfo getShardInfoByIndex(int shard) {
|
|
||||||
String shardKey = Integer.toHexString(shard);
|
|
||||||
return new ShardInfo(
|
|
||||||
shard,
|
|
||||||
"notifications-queue-" + shardKey,
|
|
||||||
"notifications.shard." + shardKey
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录分片使用统计
|
|
||||||
*/
|
|
||||||
private void recordShardUsage(int shard) {
|
|
||||||
shardCounts.computeIfAbsent(shard, k -> new AtomicLong(0)).incrementAndGet();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
// 计算用户名的哈希值并转为十六进制字符串
|
||||||
|
String hash = Integer.toHexString(Math.abs(username.hashCode()));
|
||||||
|
|
||||||
|
// 取哈希值的第一个字符 (0-9, a-f)
|
||||||
|
char firstChar = hash.charAt(0);
|
||||||
|
|
||||||
|
// 十六进制字符映射到队列
|
||||||
|
int shard = getShardFromHexChar(firstChar);
|
||||||
|
recordShardUsage(shard);
|
||||||
|
|
||||||
|
log.debug(
|
||||||
|
"Username '{}' -> hash '{}' -> firstChar '{}' -> shard {}",
|
||||||
|
username,
|
||||||
|
hash,
|
||||||
|
firstChar,
|
||||||
|
shard
|
||||||
|
);
|
||||||
|
|
||||||
|
return getShardInfoByIndex(shard);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将十六进制字符映射到分片索引
|
||||||
|
*/
|
||||||
|
private int getShardFromHexChar(char hexChar) {
|
||||||
|
int charValue;
|
||||||
|
if (hexChar >= '0' && hexChar <= '9') {
|
||||||
|
charValue = hexChar - '0'; // 0-9
|
||||||
|
} else if (hexChar >= 'a' && hexChar <= 'f') {
|
||||||
|
charValue = hexChar - 'a' + 10; // 10-15
|
||||||
|
} else {
|
||||||
|
// 异常情况,默认为0
|
||||||
|
charValue = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 映射到队列数量范围内
|
||||||
|
return charValue % QUEUE_COUNT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据分片索引获取分片信息
|
||||||
|
*/
|
||||||
|
private ShardInfo getShardInfoByIndex(int shard) {
|
||||||
|
String shardKey = Integer.toHexString(shard);
|
||||||
|
return new ShardInfo(
|
||||||
|
shard,
|
||||||
|
"notifications-queue-" + shardKey,
|
||||||
|
"notifications.shard." + shardKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录分片使用统计
|
||||||
|
*/
|
||||||
|
private void recordShardUsage(int shard) {
|
||||||
|
shardCounts.computeIfAbsent(shard, k -> new AtomicLong(0)).incrementAndGet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ import org.springframework.stereotype.Component;
|
|||||||
@Component
|
@Component
|
||||||
@ConfigurationProperties(prefix = "springdoc.api-docs")
|
@ConfigurationProperties(prefix = "springdoc.api-docs")
|
||||||
public class SpringDocProperties {
|
public class SpringDocProperties {
|
||||||
private List<ServerConfig> servers = new ArrayList<>();
|
|
||||||
|
|
||||||
@Data
|
private List<ServerConfig> servers = new ArrayList<>();
|
||||||
public static class ServerConfig {
|
|
||||||
private String url;
|
@Data
|
||||||
private String description;
|
public static class ServerConfig {
|
||||||
}
|
|
||||||
|
private String url;
|
||||||
|
private String description;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,23 +14,27 @@ import org.springframework.stereotype.Component;
|
|||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class SystemUserInitializer implements CommandLineRunner {
|
public class SystemUserInitializer implements CommandLineRunner {
|
||||||
private final UserRepository userRepository;
|
|
||||||
private final PasswordEncoder passwordEncoder;
|
|
||||||
|
|
||||||
@Override
|
private final UserRepository userRepository;
|
||||||
public void run(String... args) {
|
private final PasswordEncoder passwordEncoder;
|
||||||
userRepository.findByUsername("system").orElseGet(() -> {
|
|
||||||
User system = new User();
|
@Override
|
||||||
system.setUsername("system");
|
public void run(String... args) {
|
||||||
system.setEmail("system@openisle.local");
|
userRepository
|
||||||
// todo(tim): raw password 采用环境变量
|
.findByUsername("system")
|
||||||
system.setPassword(passwordEncoder.encode("system"));
|
.orElseGet(() -> {
|
||||||
system.setRole(Role.USER);
|
User system = new User();
|
||||||
system.setVerified(true);
|
system.setUsername("system");
|
||||||
system.setApproved(true);
|
system.setEmail("system@openisle.local");
|
||||||
system.setAvatar("https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/image.png");
|
// todo(tim): raw password 采用环境变量
|
||||||
return userRepository.save(system);
|
system.setPassword(passwordEncoder.encode("system"));
|
||||||
});
|
system.setRole(Role.USER);
|
||||||
}
|
system.setVerified(true);
|
||||||
|
system.setApproved(true);
|
||||||
|
system.setAvatar(
|
||||||
|
"https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/image.png"
|
||||||
|
);
|
||||||
|
return userRepository.save(system);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,65 +9,75 @@ import com.openisle.model.ActivityType;
|
|||||||
import com.openisle.model.User;
|
import com.openisle.model.User;
|
||||||
import com.openisle.service.ActivityService;
|
import com.openisle.service.ActivityService;
|
||||||
import com.openisle.service.UserService;
|
import com.openisle.service.UserService;
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/activities")
|
@RequestMapping("/api/activities")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ActivityController {
|
public class ActivityController {
|
||||||
private final ActivityService activityService;
|
|
||||||
private final UserService userService;
|
|
||||||
private final ActivityMapper activityMapper;
|
|
||||||
|
|
||||||
@GetMapping
|
private final ActivityService activityService;
|
||||||
@Operation(summary = "List activities", description = "Retrieve all activities")
|
private final UserService userService;
|
||||||
@ApiResponse(responseCode = "200", description = "List of activities",
|
private final ActivityMapper activityMapper;
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ActivityDto.class))))
|
|
||||||
public List<ActivityDto> list() {
|
|
||||||
return activityService.list().stream()
|
|
||||||
.map(activityMapper::toDto)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/milk-tea")
|
@GetMapping
|
||||||
@Operation(summary = "Milk tea info", description = "Get milk tea activity information")
|
@Operation(summary = "List activities", description = "Retrieve all activities")
|
||||||
@ApiResponse(responseCode = "200", description = "Milk tea info",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = MilkTeaInfoDto.class)))
|
responseCode = "200",
|
||||||
public MilkTeaInfoDto milkTea() {
|
description = "List of activities",
|
||||||
Activity a = activityService.getByType(ActivityType.MILK_TEA);
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ActivityDto.class)))
|
||||||
long count = activityService.countParticipants(a);
|
)
|
||||||
if (!a.isEnded() && count >= 50) {
|
public List<ActivityDto> list() {
|
||||||
activityService.end(a);
|
return activityService.list().stream().map(activityMapper::toDto).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
MilkTeaInfoDto info = new MilkTeaInfoDto();
|
|
||||||
info.setRedeemCount(count);
|
|
||||||
info.setEnded(a.isEnded());
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/milk-tea/redeem")
|
@GetMapping("/milk-tea")
|
||||||
@Operation(summary = "Redeem milk tea", description = "Redeem milk tea activity reward")
|
@Operation(summary = "Milk tea info", description = "Get milk tea activity information")
|
||||||
@ApiResponse(responseCode = "200", description = "Redeem result",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = java.util.Map.class)))
|
responseCode = "200",
|
||||||
@SecurityRequirement(name = "JWT")
|
description = "Milk tea info",
|
||||||
public java.util.Map<String, String> redeemMilkTea(@RequestBody MilkTeaRedeemRequest req, Authentication auth) {
|
content = @Content(schema = @Schema(implementation = MilkTeaInfoDto.class))
|
||||||
User user = userService.findByIdentifier(auth.getName()).orElseThrow();
|
)
|
||||||
Activity a = activityService.getByType(ActivityType.MILK_TEA);
|
public MilkTeaInfoDto milkTea() {
|
||||||
boolean first = activityService.redeem(a, user, req.getContact());
|
Activity a = activityService.getByType(ActivityType.MILK_TEA);
|
||||||
if (first) {
|
long count = activityService.countParticipants(a);
|
||||||
return java.util.Map.of("message", "redeemed");
|
if (!a.isEnded() && count >= 50) {
|
||||||
}
|
activityService.end(a);
|
||||||
return java.util.Map.of("message", "updated");
|
|
||||||
}
|
}
|
||||||
|
MilkTeaInfoDto info = new MilkTeaInfoDto();
|
||||||
|
info.setRedeemCount(count);
|
||||||
|
info.setEnded(a.isEnded());
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/milk-tea/redeem")
|
||||||
|
@Operation(summary = "Redeem milk tea", description = "Redeem milk tea activity reward")
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Redeem result",
|
||||||
|
content = @Content(schema = @Schema(implementation = java.util.Map.class))
|
||||||
|
)
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
public java.util.Map<String, String> redeemMilkTea(
|
||||||
|
@RequestBody MilkTeaRedeemRequest req,
|
||||||
|
Authentication auth
|
||||||
|
) {
|
||||||
|
User user = userService.findByIdentifier(auth.getName()).orElseThrow();
|
||||||
|
Activity a = activityService.getByType(ActivityType.MILK_TEA);
|
||||||
|
boolean first = activityService.redeem(a, user, req.getContact());
|
||||||
|
if (first) {
|
||||||
|
return java.util.Map.of("message", "redeemed");
|
||||||
|
}
|
||||||
|
return java.util.Map.of("message", "updated");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,24 +19,31 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
@RequestMapping("/api/admin/comments")
|
@RequestMapping("/api/admin/comments")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AdminCommentController {
|
public class AdminCommentController {
|
||||||
private final CommentService commentService;
|
|
||||||
private final CommentMapper commentMapper;
|
|
||||||
|
|
||||||
@PostMapping("/{id}/pin")
|
private final CommentService commentService;
|
||||||
@SecurityRequirement(name = "JWT")
|
private final CommentMapper commentMapper;
|
||||||
@Operation(summary = "Pin comment", description = "Pin a comment by its id")
|
|
||||||
@ApiResponse(responseCode = "200", description = "Pinned comment",
|
|
||||||
content = @Content(schema = @Schema(implementation = CommentDto.class)))
|
|
||||||
public CommentDto pin(@PathVariable Long id, Authentication auth) {
|
|
||||||
return commentMapper.toDto(commentService.pinComment(auth.getName(), id));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/{id}/unpin")
|
@PostMapping("/{id}/pin")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "Unpin comment", description = "Remove pin from a comment")
|
@Operation(summary = "Pin comment", description = "Pin a comment by its id")
|
||||||
@ApiResponse(responseCode = "200", description = "Unpinned comment",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = CommentDto.class)))
|
responseCode = "200",
|
||||||
public CommentDto unpin(@PathVariable Long id, Authentication auth) {
|
description = "Pinned comment",
|
||||||
return commentMapper.toDto(commentService.unpinComment(auth.getName(), id));
|
content = @Content(schema = @Schema(implementation = CommentDto.class))
|
||||||
}
|
)
|
||||||
|
public CommentDto pin(@PathVariable Long id, Authentication auth) {
|
||||||
|
return commentMapper.toDto(commentService.pinComment(auth.getName(), id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/unpin")
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
@Operation(summary = "Unpin comment", description = "Remove pin from a comment")
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Unpinned comment",
|
||||||
|
content = @Content(schema = @Schema(implementation = CommentDto.class))
|
||||||
|
)
|
||||||
|
public CommentDto unpin(@PathVariable Long id, Authentication auth) {
|
||||||
|
return commentMapper.toDto(commentService.unpinComment(auth.getName(), id));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,44 +17,56 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
@RequestMapping("/api/admin/config")
|
@RequestMapping("/api/admin/config")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AdminConfigController {
|
public class AdminConfigController {
|
||||||
private final PostService postService;
|
|
||||||
private final PasswordValidator passwordValidator;
|
|
||||||
private final AiUsageService aiUsageService;
|
|
||||||
private final RegisterModeService registerModeService;
|
|
||||||
|
|
||||||
@GetMapping
|
private final PostService postService;
|
||||||
@SecurityRequirement(name = "JWT")
|
private final PasswordValidator passwordValidator;
|
||||||
@Operation(summary = "Get configuration", description = "Retrieve application configuration settings")
|
private final AiUsageService aiUsageService;
|
||||||
@ApiResponse(responseCode = "200", description = "Current configuration",
|
private final RegisterModeService registerModeService;
|
||||||
content = @Content(schema = @Schema(implementation = ConfigDto.class)))
|
|
||||||
public ConfigDto getConfig() {
|
@GetMapping
|
||||||
ConfigDto dto = new ConfigDto();
|
@SecurityRequirement(name = "JWT")
|
||||||
dto.setPublishMode(postService.getPublishMode());
|
@Operation(
|
||||||
dto.setPasswordStrength(passwordValidator.getStrength());
|
summary = "Get configuration",
|
||||||
dto.setAiFormatLimit(aiUsageService.getFormatLimit());
|
description = "Retrieve application configuration settings"
|
||||||
dto.setRegisterMode(registerModeService.getRegisterMode());
|
)
|
||||||
return dto;
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Current configuration",
|
||||||
|
content = @Content(schema = @Schema(implementation = ConfigDto.class))
|
||||||
|
)
|
||||||
|
public ConfigDto getConfig() {
|
||||||
|
ConfigDto dto = new ConfigDto();
|
||||||
|
dto.setPublishMode(postService.getPublishMode());
|
||||||
|
dto.setPasswordStrength(passwordValidator.getStrength());
|
||||||
|
dto.setAiFormatLimit(aiUsageService.getFormatLimit());
|
||||||
|
dto.setRegisterMode(registerModeService.getRegisterMode());
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
@Operation(
|
||||||
|
summary = "Update configuration",
|
||||||
|
description = "Update application configuration settings"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Updated configuration",
|
||||||
|
content = @Content(schema = @Schema(implementation = ConfigDto.class))
|
||||||
|
)
|
||||||
|
public ConfigDto updateConfig(@RequestBody ConfigDto dto) {
|
||||||
|
if (dto.getPublishMode() != null) {
|
||||||
|
postService.setPublishMode(dto.getPublishMode());
|
||||||
}
|
}
|
||||||
|
if (dto.getPasswordStrength() != null) {
|
||||||
@PostMapping
|
passwordValidator.setStrength(dto.getPasswordStrength());
|
||||||
@SecurityRequirement(name = "JWT")
|
|
||||||
@Operation(summary = "Update configuration", description = "Update application configuration settings")
|
|
||||||
@ApiResponse(responseCode = "200", description = "Updated configuration",
|
|
||||||
content = @Content(schema = @Schema(implementation = ConfigDto.class)))
|
|
||||||
public ConfigDto updateConfig(@RequestBody ConfigDto dto) {
|
|
||||||
if (dto.getPublishMode() != null) {
|
|
||||||
postService.setPublishMode(dto.getPublishMode());
|
|
||||||
}
|
|
||||||
if (dto.getPasswordStrength() != null) {
|
|
||||||
passwordValidator.setStrength(dto.getPasswordStrength());
|
|
||||||
}
|
|
||||||
if (dto.getAiFormatLimit() != null) {
|
|
||||||
aiUsageService.setFormatLimit(dto.getAiFormatLimit());
|
|
||||||
}
|
|
||||||
if (dto.getRegisterMode() != null) {
|
|
||||||
registerModeService.setRegisterMode(dto.getRegisterMode());
|
|
||||||
}
|
|
||||||
return getConfig();
|
|
||||||
}
|
}
|
||||||
|
if (dto.getAiFormatLimit() != null) {
|
||||||
|
aiUsageService.setFormatLimit(dto.getAiFormatLimit());
|
||||||
|
}
|
||||||
|
if (dto.getRegisterMode() != null) {
|
||||||
|
registerModeService.setRegisterMode(dto.getRegisterMode());
|
||||||
|
}
|
||||||
|
return getConfig();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,21 +5,25 @@ import io.swagger.v3.oas.annotations.media.Content;
|
|||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
|
import java.util.Map;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple admin demo endpoint.
|
* Simple admin demo endpoint.
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
public class AdminController {
|
public class AdminController {
|
||||||
@GetMapping("/api/admin/hello")
|
|
||||||
@SecurityRequirement(name = "JWT")
|
@GetMapping("/api/admin/hello")
|
||||||
@Operation(summary = "Admin greeting", description = "Returns a greeting for admin users")
|
@SecurityRequirement(name = "JWT")
|
||||||
@ApiResponse(responseCode = "200", description = "Greeting payload",
|
@Operation(summary = "Admin greeting", description = "Returns a greeting for admin users")
|
||||||
content = @Content(schema = @Schema(implementation = Map.class)))
|
@ApiResponse(
|
||||||
public Map<String, String> adminHello() {
|
responseCode = "200",
|
||||||
return Map.of("message", "Hello, Admin User");
|
description = "Greeting payload",
|
||||||
}
|
content = @Content(schema = @Schema(implementation = Map.class))
|
||||||
|
)
|
||||||
|
public Map<String, String> adminHello() {
|
||||||
|
return Map.of("message", "Hello, Admin User");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,10 @@ import io.swagger.v3.oas.annotations.media.Content;
|
|||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Endpoints for administrators to manage posts.
|
* Endpoints for administrators to manage posts.
|
||||||
@@ -22,71 +21,109 @@ import java.util.stream.Collectors;
|
|||||||
@RequestMapping("/api/admin/posts")
|
@RequestMapping("/api/admin/posts")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AdminPostController {
|
public class AdminPostController {
|
||||||
private final PostService postService;
|
|
||||||
private final PostMapper postMapper;
|
|
||||||
|
|
||||||
@GetMapping("/pending")
|
private final PostService postService;
|
||||||
@SecurityRequirement(name = "JWT")
|
private final PostMapper postMapper;
|
||||||
@Operation(summary = "List pending posts", description = "Retrieve posts awaiting approval")
|
|
||||||
@ApiResponse(responseCode = "200", description = "Pending posts",
|
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
|
|
||||||
public List<PostSummaryDto> pendingPosts() {
|
|
||||||
return postService.listPendingPosts().stream()
|
|
||||||
.map(postMapper::toSummaryDto)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/{id}/approve")
|
@GetMapping("/pending")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "Approve post", description = "Approve a pending post")
|
@Operation(summary = "List pending posts", description = "Retrieve posts awaiting approval")
|
||||||
@ApiResponse(responseCode = "200", description = "Approved post",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = PostSummaryDto.class)))
|
responseCode = "200",
|
||||||
public PostSummaryDto approve(@PathVariable Long id) {
|
description = "Pending posts",
|
||||||
return postMapper.toSummaryDto(postService.approvePost(id));
|
content = @Content(
|
||||||
}
|
array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
public List<PostSummaryDto> pendingPosts() {
|
||||||
|
return postService
|
||||||
|
.listPendingPosts()
|
||||||
|
.stream()
|
||||||
|
.map(postMapper::toSummaryDto)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/{id}/reject")
|
@PostMapping("/{id}/approve")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "Reject post", description = "Reject a pending post")
|
@Operation(summary = "Approve post", description = "Approve a pending post")
|
||||||
@ApiResponse(responseCode = "200", description = "Rejected post",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = PostSummaryDto.class)))
|
responseCode = "200",
|
||||||
public PostSummaryDto reject(@PathVariable Long id) {
|
description = "Approved post",
|
||||||
return postMapper.toSummaryDto(postService.rejectPost(id));
|
content = @Content(schema = @Schema(implementation = PostSummaryDto.class))
|
||||||
}
|
)
|
||||||
|
public PostSummaryDto approve(@PathVariable Long id) {
|
||||||
|
return postMapper.toSummaryDto(postService.approvePost(id));
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/{id}/pin")
|
@PostMapping("/{id}/reject")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "Pin post", description = "Pin a post to the top")
|
@Operation(summary = "Reject post", description = "Reject a pending post")
|
||||||
@ApiResponse(responseCode = "200", description = "Pinned post",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = PostSummaryDto.class)))
|
responseCode = "200",
|
||||||
public PostSummaryDto pin(@PathVariable Long id, org.springframework.security.core.Authentication auth) {
|
description = "Rejected post",
|
||||||
return postMapper.toSummaryDto(postService.pinPost(id, auth.getName()));
|
content = @Content(schema = @Schema(implementation = PostSummaryDto.class))
|
||||||
}
|
)
|
||||||
|
public PostSummaryDto reject(@PathVariable Long id) {
|
||||||
|
return postMapper.toSummaryDto(postService.rejectPost(id));
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/{id}/unpin")
|
@PostMapping("/{id}/pin")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "Unpin post", description = "Remove a post from the top")
|
@Operation(summary = "Pin post", description = "Pin a post to the top")
|
||||||
@ApiResponse(responseCode = "200", description = "Unpinned post",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = PostSummaryDto.class)))
|
responseCode = "200",
|
||||||
public PostSummaryDto unpin(@PathVariable Long id, org.springframework.security.core.Authentication auth) {
|
description = "Pinned post",
|
||||||
return postMapper.toSummaryDto(postService.unpinPost(id, auth.getName()));
|
content = @Content(schema = @Schema(implementation = PostSummaryDto.class))
|
||||||
}
|
)
|
||||||
|
public PostSummaryDto pin(
|
||||||
|
@PathVariable Long id,
|
||||||
|
org.springframework.security.core.Authentication auth
|
||||||
|
) {
|
||||||
|
return postMapper.toSummaryDto(postService.pinPost(id, auth.getName()));
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/{id}/rss-exclude")
|
@PostMapping("/{id}/unpin")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "Exclude from RSS", description = "Exclude a post from RSS feed")
|
@Operation(summary = "Unpin post", description = "Remove a post from the top")
|
||||||
@ApiResponse(responseCode = "200", description = "Updated post",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = PostSummaryDto.class)))
|
responseCode = "200",
|
||||||
public PostSummaryDto excludeFromRss(@PathVariable Long id, org.springframework.security.core.Authentication auth) {
|
description = "Unpinned post",
|
||||||
return postMapper.toSummaryDto(postService.excludeFromRss(id, auth.getName()));
|
content = @Content(schema = @Schema(implementation = PostSummaryDto.class))
|
||||||
}
|
)
|
||||||
|
public PostSummaryDto unpin(
|
||||||
|
@PathVariable Long id,
|
||||||
|
org.springframework.security.core.Authentication auth
|
||||||
|
) {
|
||||||
|
return postMapper.toSummaryDto(postService.unpinPost(id, auth.getName()));
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/{id}/rss-include")
|
@PostMapping("/{id}/rss-exclude")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "Include in RSS", description = "Include a post in the RSS feed")
|
@Operation(summary = "Exclude from RSS", description = "Exclude a post from RSS feed")
|
||||||
@ApiResponse(responseCode = "200", description = "Updated post",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = PostSummaryDto.class)))
|
responseCode = "200",
|
||||||
public PostSummaryDto includeInRss(@PathVariable Long id, org.springframework.security.core.Authentication auth) {
|
description = "Updated post",
|
||||||
return postMapper.toSummaryDto(postService.includeInRss(id, auth.getName()));
|
content = @Content(schema = @Schema(implementation = PostSummaryDto.class))
|
||||||
}
|
)
|
||||||
|
public PostSummaryDto excludeFromRss(
|
||||||
|
@PathVariable Long id,
|
||||||
|
org.springframework.security.core.Authentication auth
|
||||||
|
) {
|
||||||
|
return postMapper.toSummaryDto(postService.excludeFromRss(id, auth.getName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/rss-include")
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
@Operation(summary = "Include in RSS", description = "Include a post in the RSS feed")
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Updated post",
|
||||||
|
content = @Content(schema = @Schema(implementation = PostSummaryDto.class))
|
||||||
|
)
|
||||||
|
public PostSummaryDto includeInRss(
|
||||||
|
@PathVariable Long id,
|
||||||
|
org.springframework.security.core.Authentication auth
|
||||||
|
) {
|
||||||
|
return postMapper.toSummaryDto(postService.includeInRss(id, auth.getName()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,39 +11,47 @@ import io.swagger.v3.oas.annotations.media.Content;
|
|||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/admin/tags")
|
@RequestMapping("/api/admin/tags")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AdminTagController {
|
public class AdminTagController {
|
||||||
private final TagService tagService;
|
|
||||||
private final PostService postService;
|
|
||||||
private final TagMapper tagMapper;
|
|
||||||
|
|
||||||
@GetMapping("/pending")
|
private final TagService tagService;
|
||||||
@SecurityRequirement(name = "JWT")
|
private final PostService postService;
|
||||||
@Operation(summary = "List pending tags", description = "Retrieve tags awaiting approval")
|
private final TagMapper tagMapper;
|
||||||
@ApiResponse(responseCode = "200", description = "Pending tags",
|
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TagDto.class))))
|
|
||||||
public List<TagDto> pendingTags() {
|
|
||||||
return tagService.listPendingTags().stream()
|
|
||||||
.map(t -> tagMapper.toDto(t, postService.countPostsByTag(t.getId())))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/{id}/approve")
|
@GetMapping("/pending")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "Approve tag", description = "Approve a pending tag")
|
@Operation(summary = "List pending tags", description = "Retrieve tags awaiting approval")
|
||||||
@ApiResponse(responseCode = "200", description = "Approved tag",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = TagDto.class)))
|
responseCode = "200",
|
||||||
public TagDto approve(@PathVariable Long id) {
|
description = "Pending tags",
|
||||||
Tag tag = tagService.approveTag(id);
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TagDto.class)))
|
||||||
long count = postService.countPostsByTag(tag.getId());
|
)
|
||||||
return tagMapper.toDto(tag, count);
|
public List<TagDto> pendingTags() {
|
||||||
}
|
return tagService
|
||||||
|
.listPendingTags()
|
||||||
|
.stream()
|
||||||
|
.map(t -> tagMapper.toDto(t, postService.countPostsByTag(t.getId())))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/approve")
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
@Operation(summary = "Approve tag", description = "Approve a pending tag")
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Approved tag",
|
||||||
|
content = @Content(schema = @Schema(implementation = TagDto.class))
|
||||||
|
)
|
||||||
|
public TagDto approve(@PathVariable Long id) {
|
||||||
|
Tag tag = tagService.approveTag(id);
|
||||||
|
long count = postService.countPostsByTag(tag.getId());
|
||||||
|
return tagMapper.toDto(tag, count);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ package com.openisle.controller;
|
|||||||
import com.openisle.model.Notification;
|
import com.openisle.model.Notification;
|
||||||
import com.openisle.model.NotificationType;
|
import com.openisle.model.NotificationType;
|
||||||
import com.openisle.model.User;
|
import com.openisle.model.User;
|
||||||
import com.openisle.service.EmailSender;
|
|
||||||
import com.openisle.repository.NotificationRepository;
|
import com.openisle.repository.NotificationRepository;
|
||||||
import com.openisle.repository.UserRepository;
|
import com.openisle.repository.UserRepository;
|
||||||
|
import com.openisle.service.EmailSender;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
@@ -18,46 +18,56 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
@RequestMapping("/api/admin/users")
|
@RequestMapping("/api/admin/users")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AdminUserController {
|
public class AdminUserController {
|
||||||
private final UserRepository userRepository;
|
|
||||||
private final NotificationRepository notificationRepository;
|
|
||||||
private final EmailSender emailSender;
|
|
||||||
@Value("${app.website-url}")
|
|
||||||
private String websiteUrl;
|
|
||||||
|
|
||||||
@PostMapping("/{id}/approve")
|
private final UserRepository userRepository;
|
||||||
@SecurityRequirement(name = "JWT")
|
private final NotificationRepository notificationRepository;
|
||||||
@Operation(summary = "Approve user", description = "Approve a pending user registration")
|
private final EmailSender emailSender;
|
||||||
@ApiResponse(responseCode = "200", description = "User approved")
|
|
||||||
public ResponseEntity<?> approve(@PathVariable Long id) {
|
|
||||||
User user = userRepository.findById(id).orElseThrow();
|
|
||||||
user.setApproved(true);
|
|
||||||
userRepository.save(user);
|
|
||||||
markRegisterRequestNotificationsRead(user);
|
|
||||||
emailSender.sendEmail(user.getEmail(), "您的注册已审核通过",
|
|
||||||
"🎉您的注册已经审核通过, 点击以访问网站: " + websiteUrl);
|
|
||||||
return ResponseEntity.ok().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/{id}/reject")
|
@Value("${app.website-url}")
|
||||||
@SecurityRequirement(name = "JWT")
|
private String websiteUrl;
|
||||||
@Operation(summary = "Reject user", description = "Reject a pending user registration")
|
|
||||||
@ApiResponse(responseCode = "200", description = "User rejected")
|
|
||||||
public ResponseEntity<?> reject(@PathVariable Long id) {
|
|
||||||
User user = userRepository.findById(id).orElseThrow();
|
|
||||||
user.setApproved(false);
|
|
||||||
userRepository.save(user);
|
|
||||||
markRegisterRequestNotificationsRead(user);
|
|
||||||
emailSender.sendEmail(user.getEmail(), "您的注册已被管理员拒绝",
|
|
||||||
"您的注册被管理员拒绝, 点击链接可以重新填写理由申请: " + websiteUrl);
|
|
||||||
return ResponseEntity.ok().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void markRegisterRequestNotificationsRead(User applicant) {
|
@PostMapping("/{id}/approve")
|
||||||
java.util.List<Notification> notifs =
|
@SecurityRequirement(name = "JWT")
|
||||||
notificationRepository.findByTypeAndFromUser(NotificationType.REGISTER_REQUEST, applicant);
|
@Operation(summary = "Approve user", description = "Approve a pending user registration")
|
||||||
for (Notification n : notifs) {
|
@ApiResponse(responseCode = "200", description = "User approved")
|
||||||
n.setRead(true);
|
public ResponseEntity<?> approve(@PathVariable Long id) {
|
||||||
}
|
User user = userRepository.findById(id).orElseThrow();
|
||||||
notificationRepository.saveAll(notifs);
|
user.setApproved(true);
|
||||||
|
userRepository.save(user);
|
||||||
|
markRegisterRequestNotificationsRead(user);
|
||||||
|
emailSender.sendEmail(
|
||||||
|
user.getEmail(),
|
||||||
|
"您的注册已审核通过",
|
||||||
|
"🎉您的注册已经审核通过, 点击以访问网站: " + websiteUrl
|
||||||
|
);
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/reject")
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
@Operation(summary = "Reject user", description = "Reject a pending user registration")
|
||||||
|
@ApiResponse(responseCode = "200", description = "User rejected")
|
||||||
|
public ResponseEntity<?> reject(@PathVariable Long id) {
|
||||||
|
User user = userRepository.findById(id).orElseThrow();
|
||||||
|
user.setApproved(false);
|
||||||
|
userRepository.save(user);
|
||||||
|
markRegisterRequestNotificationsRead(user);
|
||||||
|
emailSender.sendEmail(
|
||||||
|
user.getEmail(),
|
||||||
|
"您的注册已被管理员拒绝",
|
||||||
|
"您的注册被管理员拒绝, 点击链接可以重新填写理由申请: " + websiteUrl
|
||||||
|
);
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markRegisterRequestNotificationsRead(User applicant) {
|
||||||
|
java.util.List<Notification> notifs = notificationRepository.findByTypeAndFromUser(
|
||||||
|
NotificationType.REGISTER_REQUEST,
|
||||||
|
applicant
|
||||||
|
);
|
||||||
|
for (Notification n : notifs) {
|
||||||
|
n.setRead(true);
|
||||||
}
|
}
|
||||||
|
notificationRepository.saveAll(notifs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
package com.openisle.controller;
|
package com.openisle.controller;
|
||||||
|
|
||||||
import com.openisle.service.OpenAiService;
|
|
||||||
import com.openisle.service.AiUsageService;
|
import com.openisle.service.AiUsageService;
|
||||||
|
import com.openisle.service.OpenAiService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
|
import java.util.Map;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
@@ -9,41 +15,40 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/ai")
|
@RequestMapping("/api/ai")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AiController {
|
public class AiController {
|
||||||
|
|
||||||
private final OpenAiService openAiService;
|
private final OpenAiService openAiService;
|
||||||
private final AiUsageService aiUsageService;
|
private final AiUsageService aiUsageService;
|
||||||
|
|
||||||
@PostMapping("/format")
|
@PostMapping("/format")
|
||||||
@Operation(summary = "Format markdown", description = "Format text via AI")
|
@Operation(summary = "Format markdown", description = "Format text via AI")
|
||||||
@ApiResponse(responseCode = "200", description = "Formatted content",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = Map.class)))
|
responseCode = "200",
|
||||||
@SecurityRequirement(name = "JWT")
|
description = "Formatted content",
|
||||||
public ResponseEntity<Map<String, String>> format(@RequestBody Map<String, String> req,
|
content = @Content(schema = @Schema(implementation = Map.class))
|
||||||
Authentication auth) {
|
)
|
||||||
String text = req.get("text");
|
@SecurityRequirement(name = "JWT")
|
||||||
if (text == null) {
|
public ResponseEntity<Map<String, String>> format(
|
||||||
return ResponseEntity.badRequest().build();
|
@RequestBody Map<String, String> req,
|
||||||
}
|
Authentication auth
|
||||||
int limit = aiUsageService.getFormatLimit();
|
) {
|
||||||
int used = aiUsageService.getCount(auth.getName());
|
String text = req.get("text");
|
||||||
if (limit > 0 && used >= limit) {
|
if (text == null) {
|
||||||
return ResponseEntity.status(429).build();
|
return ResponseEntity.badRequest().build();
|
||||||
}
|
|
||||||
aiUsageService.incrementAndGetCount(auth.getName());
|
|
||||||
return openAiService.formatMarkdown(text)
|
|
||||||
.map(t -> ResponseEntity.ok(Map.of("content", t)))
|
|
||||||
.orElse(ResponseEntity.status(500).build());
|
|
||||||
}
|
}
|
||||||
|
int limit = aiUsageService.getFormatLimit();
|
||||||
|
int used = aiUsageService.getCount(auth.getName());
|
||||||
|
if (limit > 0 && used >= limit) {
|
||||||
|
return ResponseEntity.status(429).build();
|
||||||
|
}
|
||||||
|
aiUsageService.incrementAndGetCount(auth.getName());
|
||||||
|
return openAiService
|
||||||
|
.formatMarkdown(text)
|
||||||
|
.map(t -> ResponseEntity.ok(Map.of("content", t)))
|
||||||
|
.orElse(ResponseEntity.status(500).build());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -8,88 +8,120 @@ import com.openisle.mapper.PostMapper;
|
|||||||
import com.openisle.model.Category;
|
import com.openisle.model.Category;
|
||||||
import com.openisle.service.CategoryService;
|
import com.openisle.service.CategoryService;
|
||||||
import com.openisle.service.PostService;
|
import com.openisle.service.PostService;
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/categories")
|
@RequestMapping("/api/categories")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class CategoryController {
|
public class CategoryController {
|
||||||
private final CategoryService categoryService;
|
|
||||||
private final PostService postService;
|
|
||||||
private final PostMapper postMapper;
|
|
||||||
private final CategoryMapper categoryMapper;
|
|
||||||
|
|
||||||
@PostMapping
|
private final CategoryService categoryService;
|
||||||
@Operation(summary = "Create category", description = "Create a new category")
|
private final PostService postService;
|
||||||
@ApiResponse(responseCode = "200", description = "Created category",
|
private final PostMapper postMapper;
|
||||||
content = @Content(schema = @Schema(implementation = CategoryDto.class)))
|
private final CategoryMapper categoryMapper;
|
||||||
public CategoryDto create(@RequestBody CategoryRequest req) {
|
|
||||||
Category c = categoryService.createCategory(req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon());
|
|
||||||
long count = postService.countPostsByCategory(c.getId());
|
|
||||||
return categoryMapper.toDto(c, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PutMapping("/{id}")
|
@PostMapping
|
||||||
@Operation(summary = "Update category", description = "Update an existing category")
|
@Operation(summary = "Create category", description = "Create a new category")
|
||||||
@ApiResponse(responseCode = "200", description = "Updated category",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = CategoryDto.class)))
|
responseCode = "200",
|
||||||
public CategoryDto update(@PathVariable Long id, @RequestBody CategoryRequest req) {
|
description = "Created category",
|
||||||
Category c = categoryService.updateCategory(id, req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon());
|
content = @Content(schema = @Schema(implementation = CategoryDto.class))
|
||||||
long count = postService.countPostsByCategory(c.getId());
|
)
|
||||||
return categoryMapper.toDto(c, count);
|
public CategoryDto create(@RequestBody CategoryRequest req) {
|
||||||
}
|
Category c = categoryService.createCategory(
|
||||||
|
req.getName(),
|
||||||
|
req.getDescription(),
|
||||||
|
req.getIcon(),
|
||||||
|
req.getSmallIcon()
|
||||||
|
);
|
||||||
|
long count = postService.countPostsByCategory(c.getId());
|
||||||
|
return categoryMapper.toDto(c, count);
|
||||||
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{id}")
|
@PutMapping("/{id}")
|
||||||
@Operation(summary = "Delete category", description = "Remove a category by id")
|
@Operation(summary = "Update category", description = "Update an existing category")
|
||||||
@ApiResponse(responseCode = "200", description = "Category deleted")
|
@ApiResponse(
|
||||||
public void delete(@PathVariable Long id) {
|
responseCode = "200",
|
||||||
categoryService.deleteCategory(id);
|
description = "Updated category",
|
||||||
}
|
content = @Content(schema = @Schema(implementation = CategoryDto.class))
|
||||||
|
)
|
||||||
|
public CategoryDto update(@PathVariable Long id, @RequestBody CategoryRequest req) {
|
||||||
|
Category c = categoryService.updateCategory(
|
||||||
|
id,
|
||||||
|
req.getName(),
|
||||||
|
req.getDescription(),
|
||||||
|
req.getIcon(),
|
||||||
|
req.getSmallIcon()
|
||||||
|
);
|
||||||
|
long count = postService.countPostsByCategory(c.getId());
|
||||||
|
return categoryMapper.toDto(c, count);
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping
|
@DeleteMapping("/{id}")
|
||||||
@Operation(summary = "List categories", description = "Get all categories")
|
@Operation(summary = "Delete category", description = "Remove a category by id")
|
||||||
@ApiResponse(responseCode = "200", description = "List of categories",
|
@ApiResponse(responseCode = "200", description = "Category deleted")
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = CategoryDto.class))))
|
public void delete(@PathVariable Long id) {
|
||||||
public List<CategoryDto> list() {
|
categoryService.deleteCategory(id);
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
@GetMapping
|
||||||
@Operation(summary = "Get category", description = "Get category by id")
|
@Operation(summary = "List categories", description = "Get all categories")
|
||||||
@ApiResponse(responseCode = "200", description = "Category detail",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = CategoryDto.class)))
|
responseCode = "200",
|
||||||
public CategoryDto get(@PathVariable Long id) {
|
description = "List of categories",
|
||||||
Category c = categoryService.getCategory(id);
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = CategoryDto.class)))
|
||||||
long count = postService.countPostsByCategory(c.getId());
|
)
|
||||||
return categoryMapper.toDto(c, count);
|
public List<CategoryDto> list() {
|
||||||
}
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}/posts")
|
@GetMapping("/{id}")
|
||||||
@Operation(summary = "List posts by category", description = "Get posts under a category")
|
@Operation(summary = "Get category", description = "Get category by id")
|
||||||
@ApiResponse(responseCode = "200", description = "List of posts",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
|
responseCode = "200",
|
||||||
public List<PostSummaryDto> listPostsByCategory(@PathVariable Long id,
|
description = "Category detail",
|
||||||
@RequestParam(value = "page", required = false) Integer page,
|
content = @Content(schema = @Schema(implementation = CategoryDto.class))
|
||||||
@RequestParam(value = "pageSize", required = false) Integer pageSize) {
|
)
|
||||||
return postService.listPostsByCategories(java.util.List.of(id), page, pageSize)
|
public CategoryDto get(@PathVariable Long id) {
|
||||||
.stream()
|
Category c = categoryService.getCategory(id);
|
||||||
.map(postMapper::toSummaryDto)
|
long count = postService.countPostsByCategory(c.getId());
|
||||||
.collect(Collectors.toList());
|
return categoryMapper.toDto(c, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/posts")
|
||||||
|
@Operation(summary = "List posts by category", description = "Get posts under a category")
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "List of posts",
|
||||||
|
content = @Content(
|
||||||
|
array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
public List<PostSummaryDto> listPostsByCategory(
|
||||||
|
@PathVariable Long id,
|
||||||
|
@RequestParam(value = "page", required = false) Integer page,
|
||||||
|
@RequestParam(value = "pageSize", required = false) Integer pageSize
|
||||||
|
) {
|
||||||
|
return postService
|
||||||
|
.listPostsByCategories(java.util.List.of(id), page, pageSize)
|
||||||
|
.stream()
|
||||||
|
.map(postMapper::toSummaryDto)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,56 +5,66 @@ import com.openisle.model.User;
|
|||||||
import com.openisle.repository.UserRepository;
|
import com.openisle.repository.UserRepository;
|
||||||
import com.openisle.service.ChannelService;
|
import com.openisle.service.ChannelService;
|
||||||
import com.openisle.service.MessageService;
|
import com.openisle.service.MessageService;
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/channels")
|
@RequestMapping("/api/channels")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ChannelController {
|
public class ChannelController {
|
||||||
private final ChannelService channelService;
|
|
||||||
private final MessageService messageService;
|
|
||||||
private final UserRepository userRepository;
|
|
||||||
|
|
||||||
private Long getCurrentUserId(Authentication auth) {
|
private final ChannelService channelService;
|
||||||
User user = userRepository.findByUsername(auth.getName())
|
private final MessageService messageService;
|
||||||
.orElseThrow(() -> new IllegalArgumentException("User not found"));
|
private final UserRepository userRepository;
|
||||||
return user.getId();
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping
|
private Long getCurrentUserId(Authentication auth) {
|
||||||
@Operation(summary = "List channels", description = "List channels for the current user")
|
User user = userRepository
|
||||||
@ApiResponse(responseCode = "200", description = "Channels",
|
.findByUsername(auth.getName())
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ChannelDto.class))))
|
.orElseThrow(() -> new IllegalArgumentException("User not found"));
|
||||||
@SecurityRequirement(name = "JWT")
|
return user.getId();
|
||||||
public List<ChannelDto> listChannels(Authentication auth) {
|
}
|
||||||
return channelService.listChannels(getCurrentUserId(auth));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/{channelId}/join")
|
@GetMapping
|
||||||
@Operation(summary = "Join channel", description = "Join a channel")
|
@Operation(summary = "List channels", description = "List channels for the current user")
|
||||||
@ApiResponse(responseCode = "200", description = "Joined channel",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = ChannelDto.class)))
|
responseCode = "200",
|
||||||
@SecurityRequirement(name = "JWT")
|
description = "Channels",
|
||||||
public ChannelDto joinChannel(@PathVariable Long channelId, Authentication auth) {
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ChannelDto.class)))
|
||||||
return channelService.joinChannel(channelId, getCurrentUserId(auth));
|
)
|
||||||
}
|
@SecurityRequirement(name = "JWT")
|
||||||
|
public List<ChannelDto> listChannels(Authentication auth) {
|
||||||
|
return channelService.listChannels(getCurrentUserId(auth));
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/unread-count")
|
@PostMapping("/{channelId}/join")
|
||||||
@Operation(summary = "Unread count", description = "Get unread channel count")
|
@Operation(summary = "Join channel", description = "Join a channel")
|
||||||
@ApiResponse(responseCode = "200", description = "Unread count",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = Long.class)))
|
responseCode = "200",
|
||||||
@SecurityRequirement(name = "JWT")
|
description = "Joined channel",
|
||||||
public long unreadCount(Authentication auth) {
|
content = @Content(schema = @Schema(implementation = ChannelDto.class))
|
||||||
return messageService.getUnreadChannelCount(getCurrentUserId(auth));
|
)
|
||||||
}
|
@SecurityRequirement(name = "JWT")
|
||||||
|
public ChannelDto joinChannel(@PathVariable Long channelId, Authentication auth) {
|
||||||
|
return channelService.joinChannel(channelId, getCurrentUserId(auth));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/unread-count")
|
||||||
|
@Operation(summary = "Unread count", description = "Get unread channel count")
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Unread count",
|
||||||
|
content = @Content(schema = @Schema(implementation = Long.class))
|
||||||
|
)
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
public long unreadCount(Authentication auth) {
|
||||||
|
return messageService.getUnreadChannelCount(getCurrentUserId(auth));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,128 +1,256 @@
|
|||||||
package com.openisle.controller;
|
package com.openisle.controller;
|
||||||
|
|
||||||
import com.openisle.model.Comment;
|
import com.openisle.dto.CommentContextDto;
|
||||||
import com.openisle.dto.CommentDto;
|
import com.openisle.dto.CommentDto;
|
||||||
import com.openisle.dto.CommentRequest;
|
import com.openisle.dto.CommentRequest;
|
||||||
|
import com.openisle.dto.PostChangeLogDto;
|
||||||
|
import com.openisle.dto.TimelineItemDto;
|
||||||
import com.openisle.mapper.CommentMapper;
|
import com.openisle.mapper.CommentMapper;
|
||||||
import com.openisle.service.CaptchaService;
|
import com.openisle.mapper.PostChangeLogMapper;
|
||||||
import com.openisle.service.CommentService;
|
import com.openisle.mapper.PostMapper;
|
||||||
import com.openisle.service.LevelService;
|
import com.openisle.model.Comment;
|
||||||
import com.openisle.service.PointService;
|
import com.openisle.model.CommentSort;
|
||||||
import lombok.RequiredArgsConstructor;
|
import com.openisle.service.*;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api")
|
@RequestMapping("/api")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class CommentController {
|
public class CommentController {
|
||||||
private final CommentService commentService;
|
|
||||||
private final LevelService levelService;
|
|
||||||
private final CaptchaService captchaService;
|
|
||||||
private final CommentMapper commentMapper;
|
|
||||||
private final PointService pointService;
|
|
||||||
|
|
||||||
@Value("${app.captcha.enabled:false}")
|
private final CommentService commentService;
|
||||||
private boolean captchaEnabled;
|
private final LevelService levelService;
|
||||||
|
private final CaptchaService captchaService;
|
||||||
|
private final CommentMapper commentMapper;
|
||||||
|
private final PointService pointService;
|
||||||
|
private final PostChangeLogService changeLogService;
|
||||||
|
private final PostChangeLogMapper postChangeLogMapper;
|
||||||
|
private final PostMapper postMapper;
|
||||||
|
|
||||||
@Value("${app.captcha.comment-enabled:false}")
|
@Value("${app.captcha.enabled:false}")
|
||||||
private boolean commentCaptchaEnabled;
|
private boolean captchaEnabled;
|
||||||
|
|
||||||
@PostMapping("/posts/{postId}/comments")
|
@Value("${app.captcha.comment-enabled:false}")
|
||||||
@Operation(summary = "Create comment", description = "Add a comment to a post")
|
private boolean commentCaptchaEnabled;
|
||||||
@ApiResponse(responseCode = "200", description = "Created comment",
|
|
||||||
content = @Content(schema = @Schema(implementation = CommentDto.class)))
|
@PostMapping("/posts/{postId}/comments")
|
||||||
@SecurityRequirement(name = "JWT")
|
@Operation(summary = "Create comment", description = "Add a comment to a post")
|
||||||
public ResponseEntity<CommentDto> createComment(@PathVariable Long postId,
|
@ApiResponse(
|
||||||
@RequestBody CommentRequest req,
|
responseCode = "200",
|
||||||
Authentication auth) {
|
description = "Created comment",
|
||||||
log.debug("createComment called by user {} for post {}", auth.getName(), postId);
|
content = @Content(schema = @Schema(implementation = CommentDto.class))
|
||||||
if (captchaEnabled && commentCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
|
)
|
||||||
log.debug("Captcha verification failed for user {} on post {}", auth.getName(), postId);
|
@SecurityRequirement(name = "JWT")
|
||||||
return ResponseEntity.badRequest().build();
|
public ResponseEntity<CommentDto> createComment(
|
||||||
}
|
@PathVariable Long postId,
|
||||||
Comment comment = commentService.addComment(auth.getName(), postId, req.getContent());
|
@RequestBody CommentRequest req,
|
||||||
CommentDto dto = commentMapper.toDto(comment);
|
Authentication auth
|
||||||
dto.setReward(levelService.awardForComment(auth.getName()));
|
) {
|
||||||
dto.setPointReward(pointService.awardForComment(auth.getName(), postId, comment.getId()));
|
log.debug("createComment called by user {} for post {}", auth.getName(), postId);
|
||||||
log.debug("createComment succeeded for comment {}", comment.getId());
|
if (captchaEnabled && commentCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
|
||||||
return ResponseEntity.ok(dto);
|
log.debug("Captcha verification failed for user {} on post {}", auth.getName(), postId);
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
}
|
}
|
||||||
|
Comment comment = commentService.addComment(auth.getName(), postId, req.getContent());
|
||||||
|
CommentDto dto = commentMapper.toDto(comment);
|
||||||
|
dto.setReward(levelService.awardForComment(auth.getName()));
|
||||||
|
dto.setPointReward(pointService.awardForComment(auth.getName(), postId, comment.getId()));
|
||||||
|
log.debug("createComment succeeded for comment {}", comment.getId());
|
||||||
|
return ResponseEntity.ok(dto);
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/comments/{commentId}/replies")
|
@PostMapping("/comments/{commentId}/replies")
|
||||||
@Operation(summary = "Reply to comment", description = "Reply to an existing comment")
|
@Operation(summary = "Reply to comment", description = "Reply to an existing comment")
|
||||||
@ApiResponse(responseCode = "200", description = "Reply created",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = CommentDto.class)))
|
responseCode = "200",
|
||||||
@SecurityRequirement(name = "JWT")
|
description = "Reply created",
|
||||||
public ResponseEntity<CommentDto> replyComment(@PathVariable Long commentId,
|
content = @Content(schema = @Schema(implementation = CommentDto.class))
|
||||||
@RequestBody CommentRequest req,
|
)
|
||||||
Authentication auth) {
|
@SecurityRequirement(name = "JWT")
|
||||||
log.debug("replyComment called by user {} for comment {}", auth.getName(), commentId);
|
public ResponseEntity<CommentDto> replyComment(
|
||||||
if (captchaEnabled && commentCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
|
@PathVariable Long commentId,
|
||||||
log.debug("Captcha verification failed for user {} on comment {}", auth.getName(), commentId);
|
@RequestBody CommentRequest req,
|
||||||
return ResponseEntity.badRequest().build();
|
Authentication auth
|
||||||
}
|
) {
|
||||||
Comment comment = commentService.addReply(auth.getName(), commentId, req.getContent());
|
log.debug("replyComment called by user {} for comment {}", auth.getName(), commentId);
|
||||||
CommentDto dto = commentMapper.toDto(comment);
|
if (captchaEnabled && commentCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
|
||||||
dto.setReward(levelService.awardForComment(auth.getName()));
|
log.debug("Captcha verification failed for user {} on comment {}", auth.getName(), commentId);
|
||||||
log.debug("replyComment succeeded for comment {}", comment.getId());
|
return ResponseEntity.badRequest().build();
|
||||||
return ResponseEntity.ok(dto);
|
|
||||||
}
|
}
|
||||||
|
Comment comment = commentService.addReply(auth.getName(), commentId, req.getContent());
|
||||||
|
CommentDto dto = commentMapper.toDto(comment);
|
||||||
|
dto.setReward(levelService.awardForComment(auth.getName()));
|
||||||
|
log.debug("replyComment succeeded for comment {}", comment.getId());
|
||||||
|
return ResponseEntity.ok(dto);
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/posts/{postId}/comments")
|
@GetMapping("/posts/{postId}/comments")
|
||||||
@Operation(summary = "List comments", description = "List comments for a post")
|
@Operation(summary = "List comments", description = "List comments for a post")
|
||||||
@ApiResponse(responseCode = "200", description = "Comments",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = CommentDto.class))))
|
responseCode = "200",
|
||||||
public List<CommentDto> listComments(@PathVariable Long postId,
|
description = "Comments",
|
||||||
@RequestParam(value = "sort", required = false, defaultValue = "OLDEST") com.openisle.model.CommentSort sort) {
|
content = @Content(
|
||||||
log.debug("listComments called for post {} with sort {}", postId, sort);
|
array = @ArraySchema(schema = @Schema(implementation = TimelineItemDto.class))
|
||||||
List<CommentDto> list = commentService.getCommentsForPost(postId, sort).stream()
|
)
|
||||||
.map(commentMapper::toDtoWithReplies)
|
)
|
||||||
.collect(Collectors.toList());
|
public List<TimelineItemDto<?>> listComments(
|
||||||
log.debug("listComments returning {} comments", list.size());
|
@PathVariable Long postId,
|
||||||
return list;
|
@RequestParam(value = "sort", required = false, defaultValue = "OLDEST") CommentSort sort
|
||||||
}
|
) {
|
||||||
|
log.debug("listComments called for post {} with sort {}", postId, sort);
|
||||||
|
List<CommentDto> commentDtoList = commentService
|
||||||
|
.getCommentsForPost(postId, sort)
|
||||||
|
.stream()
|
||||||
|
.map(commentMapper::toDtoWithReplies)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
List<PostChangeLogDto> postChangeLogDtoList = changeLogService
|
||||||
|
.listLogs(postId)
|
||||||
|
.stream()
|
||||||
|
.map(postChangeLogMapper::toDto)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
List<TimelineItemDto<?>> itemDtoList = new ArrayList<>();
|
||||||
|
|
||||||
@DeleteMapping("/comments/{id}")
|
itemDtoList.addAll(
|
||||||
@Operation(summary = "Delete comment", description = "Delete a comment")
|
commentDtoList
|
||||||
@ApiResponse(responseCode = "200", description = "Deleted")
|
.stream()
|
||||||
@SecurityRequirement(name = "JWT")
|
.map(c ->
|
||||||
public void deleteComment(@PathVariable Long id, Authentication auth) {
|
new TimelineItemDto<>(
|
||||||
log.debug("deleteComment called by user {} for comment {}", auth.getName(), id);
|
c.getId(),
|
||||||
commentService.deleteComment(auth.getName(), id);
|
"comment",
|
||||||
log.debug("deleteComment completed for comment {}", id);
|
c.getCreatedAt(),
|
||||||
}
|
c.getPinnedAt(),
|
||||||
|
c // payload 是 CommentDto
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.toList()
|
||||||
|
);
|
||||||
|
|
||||||
@PostMapping("/comments/{id}/pin")
|
itemDtoList.addAll(
|
||||||
@Operation(summary = "Pin comment", description = "Pin a comment")
|
postChangeLogDtoList
|
||||||
@ApiResponse(responseCode = "200", description = "Pinned comment",
|
.stream()
|
||||||
content = @Content(schema = @Schema(implementation = CommentDto.class)))
|
.map(l ->
|
||||||
@SecurityRequirement(name = "JWT")
|
new TimelineItemDto<>(
|
||||||
public CommentDto pinComment(@PathVariable Long id, Authentication auth) {
|
l.getId(),
|
||||||
log.debug("pinComment called by user {} for comment {}", auth.getName(), id);
|
"log",
|
||||||
return commentMapper.toDto(commentService.pinComment(auth.getName(), id));
|
l.getTime(), // 注意字段名不一样
|
||||||
}
|
null,
|
||||||
|
l // payload 是 PostChangeLogDto
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.toList()
|
||||||
|
);
|
||||||
|
// 排序
|
||||||
|
Comparator<TimelineItemDto<?>> pinnedOrderComparator = (a, b) -> {
|
||||||
|
LocalDateTime aPinned = a.getPinnedAt();
|
||||||
|
LocalDateTime bPinned = b.getPinnedAt();
|
||||||
|
if (aPinned == null && bPinned == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (aPinned == null) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (bPinned == null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return bPinned.compareTo(aPinned);
|
||||||
|
};
|
||||||
|
|
||||||
@PostMapping("/comments/{id}/unpin")
|
Comparator<TimelineItemDto<?>> comparator = Comparator.<TimelineItemDto<?>, Boolean>comparing(
|
||||||
@Operation(summary = "Unpin comment", description = "Unpin a comment")
|
item -> item.getPinnedAt() == null
|
||||||
@ApiResponse(responseCode = "200", description = "Unpinned comment",
|
).thenComparing(pinnedOrderComparator);
|
||||||
content = @Content(schema = @Schema(implementation = CommentDto.class)))
|
|
||||||
@SecurityRequirement(name = "JWT")
|
Comparator<TimelineItemDto<?>> createdAtComparator = Comparator.comparing(
|
||||||
public CommentDto unpinComment(@PathVariable Long id, Authentication auth) {
|
TimelineItemDto::getCreatedAt
|
||||||
log.debug("unpinComment called by user {} for comment {}", auth.getName(), id);
|
);
|
||||||
return commentMapper.toDto(commentService.unpinComment(auth.getName(), id));
|
if (CommentSort.NEWEST.equals(sort)) {
|
||||||
|
createdAtComparator = createdAtComparator.reversed();
|
||||||
}
|
}
|
||||||
|
itemDtoList.sort(comparator.thenComparing(createdAtComparator));
|
||||||
|
log.debug("listComments returning {} comments", itemDtoList.size());
|
||||||
|
return itemDtoList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/comments/{commentId}/context")
|
||||||
|
@Operation(
|
||||||
|
summary = "Comment context",
|
||||||
|
description = "Get a comment along with its previous comments and related post"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Comment context",
|
||||||
|
content = @Content(schema = @Schema(implementation = CommentContextDto.class))
|
||||||
|
)
|
||||||
|
public ResponseEntity<CommentContextDto> getCommentContext(@PathVariable Long commentId) {
|
||||||
|
log.debug("getCommentContext called for comment {}", commentId);
|
||||||
|
Comment comment = commentService.getComment(commentId);
|
||||||
|
CommentContextDto dto = new CommentContextDto();
|
||||||
|
dto.setPost(postMapper.toSummaryDto(comment.getPost()));
|
||||||
|
dto.setTargetComment(commentMapper.toDtoWithReplies(comment));
|
||||||
|
dto.setPreviousComments(
|
||||||
|
commentService
|
||||||
|
.getCommentsBefore(comment)
|
||||||
|
.stream()
|
||||||
|
.map(commentMapper::toDtoWithReplies)
|
||||||
|
.collect(Collectors.toList())
|
||||||
|
);
|
||||||
|
log.debug(
|
||||||
|
"getCommentContext returning {} previous comments for comment {}",
|
||||||
|
dto.getPreviousComments().size(),
|
||||||
|
commentId
|
||||||
|
);
|
||||||
|
return ResponseEntity.ok(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/comments/{id}")
|
||||||
|
@Operation(summary = "Delete comment", description = "Delete a comment")
|
||||||
|
@ApiResponse(responseCode = "200", description = "Deleted")
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
public void deleteComment(@PathVariable Long id, Authentication auth) {
|
||||||
|
log.debug("deleteComment called by user {} for comment {}", auth.getName(), id);
|
||||||
|
commentService.deleteComment(auth.getName(), id);
|
||||||
|
log.debug("deleteComment completed for comment {}", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/comments/{id}/pin")
|
||||||
|
@Operation(summary = "Pin comment", description = "Pin a comment")
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Pinned comment",
|
||||||
|
content = @Content(schema = @Schema(implementation = CommentDto.class))
|
||||||
|
)
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
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")
|
||||||
|
@Operation(summary = "Unpin comment", description = "Unpin a comment")
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Unpinned comment",
|
||||||
|
content = @Content(schema = @Schema(implementation = CommentDto.class))
|
||||||
|
)
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,53 +2,56 @@ package com.openisle.controller;
|
|||||||
|
|
||||||
import com.openisle.dto.SiteConfigDto;
|
import com.openisle.dto.SiteConfigDto;
|
||||||
import com.openisle.service.RegisterModeService;
|
import com.openisle.service.RegisterModeService;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api")
|
@RequestMapping("/api")
|
||||||
@lombok.RequiredArgsConstructor
|
@lombok.RequiredArgsConstructor
|
||||||
public class ConfigController {
|
public class ConfigController {
|
||||||
|
|
||||||
@Value("${app.captcha.enabled:false}")
|
@Value("${app.captcha.enabled:false}")
|
||||||
private boolean captchaEnabled;
|
private boolean captchaEnabled;
|
||||||
|
|
||||||
@Value("${app.captcha.register-enabled:false}")
|
@Value("${app.captcha.register-enabled:false}")
|
||||||
private boolean registerCaptchaEnabled;
|
private boolean registerCaptchaEnabled;
|
||||||
|
|
||||||
@Value("${app.captcha.login-enabled:false}")
|
@Value("${app.captcha.login-enabled:false}")
|
||||||
private boolean loginCaptchaEnabled;
|
private boolean loginCaptchaEnabled;
|
||||||
|
|
||||||
@Value("${app.captcha.post-enabled:false}")
|
@Value("${app.captcha.post-enabled:false}")
|
||||||
private boolean postCaptchaEnabled;
|
private boolean postCaptchaEnabled;
|
||||||
|
|
||||||
@Value("${app.captcha.comment-enabled:false}")
|
@Value("${app.captcha.comment-enabled:false}")
|
||||||
private boolean commentCaptchaEnabled;
|
private boolean commentCaptchaEnabled;
|
||||||
|
|
||||||
@Value("${app.ai.format-limit:3}")
|
@Value("${app.ai.format-limit:3}")
|
||||||
private int aiFormatLimit;
|
private int aiFormatLimit;
|
||||||
|
|
||||||
private final RegisterModeService registerModeService;
|
private final RegisterModeService registerModeService;
|
||||||
|
|
||||||
@GetMapping("/config")
|
@GetMapping("/config")
|
||||||
@Operation(summary = "Site config", description = "Get site configuration")
|
@Operation(summary = "Site config", description = "Get site configuration")
|
||||||
@ApiResponse(responseCode = "200", description = "Site configuration",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = SiteConfigDto.class)))
|
responseCode = "200",
|
||||||
public SiteConfigDto getConfig() {
|
description = "Site configuration",
|
||||||
SiteConfigDto resp = new SiteConfigDto();
|
content = @Content(schema = @Schema(implementation = SiteConfigDto.class))
|
||||||
resp.setCaptchaEnabled(captchaEnabled);
|
)
|
||||||
resp.setRegisterCaptchaEnabled(registerCaptchaEnabled);
|
public SiteConfigDto getConfig() {
|
||||||
resp.setLoginCaptchaEnabled(loginCaptchaEnabled);
|
SiteConfigDto resp = new SiteConfigDto();
|
||||||
resp.setPostCaptchaEnabled(postCaptchaEnabled);
|
resp.setCaptchaEnabled(captchaEnabled);
|
||||||
resp.setCommentCaptchaEnabled(commentCaptchaEnabled);
|
resp.setRegisterCaptchaEnabled(registerCaptchaEnabled);
|
||||||
resp.setAiFormatLimit(aiFormatLimit);
|
resp.setLoginCaptchaEnabled(loginCaptchaEnabled);
|
||||||
resp.setRegisterMode(registerModeService.getRegisterMode());
|
resp.setPostCaptchaEnabled(postCaptchaEnabled);
|
||||||
return resp;
|
resp.setCommentCaptchaEnabled(commentCaptchaEnabled);
|
||||||
}
|
resp.setAiFormatLimit(aiFormatLimit);
|
||||||
|
resp.setRegisterMode(registerModeService.getRegisterMode());
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,50 +5,64 @@ import com.openisle.dto.DraftRequest;
|
|||||||
import com.openisle.mapper.DraftMapper;
|
import com.openisle.mapper.DraftMapper;
|
||||||
import com.openisle.model.Draft;
|
import com.openisle.model.Draft;
|
||||||
import com.openisle.service.DraftService;
|
import com.openisle.service.DraftService;
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/drafts")
|
@RequestMapping("/api/drafts")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class DraftController {
|
public class DraftController {
|
||||||
private final DraftService draftService;
|
|
||||||
private final DraftMapper draftMapper;
|
|
||||||
|
|
||||||
@PostMapping
|
private final DraftService draftService;
|
||||||
@Operation(summary = "Save draft", description = "Save a draft for current user")
|
private final DraftMapper draftMapper;
|
||||||
@ApiResponse(responseCode = "200", description = "Draft saved",
|
|
||||||
content = @Content(schema = @Schema(implementation = DraftDto.class)))
|
|
||||||
@SecurityRequirement(name = "JWT")
|
|
||||||
public ResponseEntity<DraftDto> saveDraft(@RequestBody DraftRequest req, Authentication auth) {
|
|
||||||
Draft draft = draftService.saveDraft(auth.getName(), req.getCategoryId(), req.getTitle(), req.getContent(), req.getTagIds());
|
|
||||||
return ResponseEntity.ok(draftMapper.toDto(draft));
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/me")
|
@PostMapping
|
||||||
@Operation(summary = "Get my draft", description = "Get current user's draft")
|
@Operation(summary = "Save draft", description = "Save a draft for current user")
|
||||||
@ApiResponse(responseCode = "200", description = "Draft details",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = DraftDto.class)))
|
responseCode = "200",
|
||||||
@SecurityRequirement(name = "JWT")
|
description = "Draft saved",
|
||||||
public ResponseEntity<DraftDto> getMyDraft(Authentication auth) {
|
content = @Content(schema = @Schema(implementation = DraftDto.class))
|
||||||
return draftService.getDraft(auth.getName())
|
)
|
||||||
.map(d -> ResponseEntity.ok(draftMapper.toDto(d)))
|
@SecurityRequirement(name = "JWT")
|
||||||
.orElseGet(() -> ResponseEntity.noContent().build());
|
public ResponseEntity<DraftDto> saveDraft(@RequestBody DraftRequest req, Authentication auth) {
|
||||||
}
|
Draft draft = draftService.saveDraft(
|
||||||
|
auth.getName(),
|
||||||
|
req.getCategoryId(),
|
||||||
|
req.getTitle(),
|
||||||
|
req.getContent(),
|
||||||
|
req.getTagIds()
|
||||||
|
);
|
||||||
|
return ResponseEntity.ok(draftMapper.toDto(draft));
|
||||||
|
}
|
||||||
|
|
||||||
@DeleteMapping("/me")
|
@GetMapping("/me")
|
||||||
@Operation(summary = "Delete my draft", description = "Delete current user's draft")
|
@Operation(summary = "Get my draft", description = "Get current user's draft")
|
||||||
@ApiResponse(responseCode = "200", description = "Draft deleted")
|
@ApiResponse(
|
||||||
@SecurityRequirement(name = "JWT")
|
responseCode = "200",
|
||||||
public ResponseEntity<?> deleteMyDraft(Authentication auth) {
|
description = "Draft details",
|
||||||
draftService.deleteDraft(auth.getName());
|
content = @Content(schema = @Schema(implementation = DraftDto.class))
|
||||||
return ResponseEntity.ok().build();
|
)
|
||||||
}
|
@SecurityRequirement(name = "JWT")
|
||||||
|
public ResponseEntity<DraftDto> getMyDraft(Authentication auth) {
|
||||||
|
return draftService
|
||||||
|
.getDraft(auth.getName())
|
||||||
|
.map(d -> ResponseEntity.ok(draftMapper.toDto(d)))
|
||||||
|
.orElseGet(() -> ResponseEntity.noContent().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/me")
|
||||||
|
@Operation(summary = "Delete my draft", description = "Delete current user's draft")
|
||||||
|
@ApiResponse(responseCode = "200", description = "Draft deleted")
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
public ResponseEntity<?> deleteMyDraft(Authentication auth) {
|
||||||
|
draftService.deleteDraft(auth.getName());
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,39 @@
|
|||||||
package com.openisle.controller;
|
package com.openisle.controller;
|
||||||
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
|
||||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
|
||||||
import com.openisle.exception.FieldException;
|
import com.openisle.exception.FieldException;
|
||||||
import com.openisle.exception.NotFoundException;
|
import com.openisle.exception.NotFoundException;
|
||||||
import com.openisle.exception.RateLimitException;
|
import com.openisle.exception.RateLimitException;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
@RestControllerAdvice
|
@RestControllerAdvice
|
||||||
public class GlobalExceptionHandler {
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
@ExceptionHandler(FieldException.class)
|
@ExceptionHandler(FieldException.class)
|
||||||
public ResponseEntity<?> handleFieldException(FieldException ex) {
|
public ResponseEntity<?> handleFieldException(FieldException ex) {
|
||||||
return ResponseEntity.badRequest()
|
return ResponseEntity.badRequest().body(
|
||||||
.body(Map.of("error", ex.getMessage(), "field", ex.getField()));
|
Map.of("error", ex.getMessage(), "field", ex.getField())
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@ExceptionHandler(NotFoundException.class)
|
@ExceptionHandler(NotFoundException.class)
|
||||||
public ResponseEntity<?> handleNotFoundException(NotFoundException ex) {
|
public ResponseEntity<?> handleNotFoundException(NotFoundException ex) {
|
||||||
return ResponseEntity.status(404).body(Map.of("error", ex.getMessage()));
|
return ResponseEntity.status(404).body(Map.of("error", ex.getMessage()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(RateLimitException.class)
|
@ExceptionHandler(RateLimitException.class)
|
||||||
public ResponseEntity<?> handleRateLimitException(RateLimitException ex) {
|
public ResponseEntity<?> handleRateLimitException(RateLimitException ex) {
|
||||||
return ResponseEntity.status(429).body(Map.of("error", ex.getMessage()));
|
return ResponseEntity.status(429).body(Map.of("error", ex.getMessage()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(Exception.class)
|
@ExceptionHandler(Exception.class)
|
||||||
public ResponseEntity<?> handleException(Exception ex) {
|
public ResponseEntity<?> handleException(Exception ex) {
|
||||||
String message = ex.getMessage();
|
String message = ex.getMessage();
|
||||||
if (message == null) {
|
if (message == null) {
|
||||||
message = ex.getClass().getSimpleName();
|
message = ex.getClass().getSimpleName();
|
||||||
}
|
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", message));
|
|
||||||
}
|
}
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", message));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,18 +5,22 @@ import io.swagger.v3.oas.annotations.media.Content;
|
|||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
|
import java.util.Map;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
public class HelloController {
|
public class HelloController {
|
||||||
@GetMapping("/api/hello")
|
|
||||||
@SecurityRequirement(name = "JWT")
|
@GetMapping("/api/hello")
|
||||||
@Operation(summary = "Hello endpoint", description = "Returns a greeting for authenticated users")
|
@SecurityRequirement(name = "JWT")
|
||||||
@ApiResponse(responseCode = "200", description = "Greeting payload",
|
@Operation(summary = "Hello endpoint", description = "Returns a greeting for authenticated users")
|
||||||
content = @Content(schema = @Schema(implementation = Map.class)))
|
@ApiResponse(
|
||||||
public Map<String, String> hello() {
|
responseCode = "200",
|
||||||
return Map.of("message", "Hello, Authenticated User");
|
description = "Greeting payload",
|
||||||
}
|
content = @Content(schema = @Schema(implementation = Map.class))
|
||||||
|
)
|
||||||
|
public Map<String, String> hello() {
|
||||||
|
return Map.of("message", "Hello, Authenticated User");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,35 @@
|
|||||||
package com.openisle.controller;
|
package com.openisle.controller;
|
||||||
|
|
||||||
import com.openisle.service.InviteService;
|
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 io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
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;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/invite")
|
@RequestMapping("/api/invite")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class InviteController {
|
public class InviteController {
|
||||||
private final InviteService inviteService;
|
|
||||||
|
|
||||||
@PostMapping("/generate")
|
private final InviteService inviteService;
|
||||||
@Operation(summary = "Generate invite", description = "Generate an invite token")
|
|
||||||
@ApiResponse(responseCode = "200", description = "Invite token",
|
@PostMapping("/generate")
|
||||||
content = @Content(schema = @Schema(implementation = Map.class)))
|
@Operation(summary = "Generate invite", description = "Generate an invite token")
|
||||||
@SecurityRequirement(name = "JWT")
|
@ApiResponse(
|
||||||
public Map<String, String> generate(Authentication auth) {
|
responseCode = "200",
|
||||||
String token = inviteService.generate(auth.getName());
|
description = "Invite token",
|
||||||
return Map.of("token", token);
|
content = @Content(schema = @Schema(implementation = Map.class))
|
||||||
}
|
)
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
public Map<String, String> generate(Authentication auth) {
|
||||||
|
String token = inviteService.generate(auth.getName());
|
||||||
|
return Map.of("token", token);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,43 +3,49 @@ package com.openisle.controller;
|
|||||||
import com.openisle.dto.MedalDto;
|
import com.openisle.dto.MedalDto;
|
||||||
import com.openisle.dto.MedalSelectRequest;
|
import com.openisle.dto.MedalSelectRequest;
|
||||||
import com.openisle.service.MedalService;
|
import com.openisle.service.MedalService;
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/medals")
|
@RequestMapping("/api/medals")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class MedalController {
|
public class MedalController {
|
||||||
private final MedalService medalService;
|
|
||||||
|
|
||||||
@GetMapping
|
private final MedalService medalService;
|
||||||
@Operation(summary = "List medals", description = "List medals for user or globally")
|
|
||||||
@ApiResponse(responseCode = "200", description = "List of medals",
|
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = MedalDto.class))))
|
|
||||||
public List<MedalDto> getMedals(@RequestParam(value = "userId", required = false) Long userId) {
|
|
||||||
return medalService.getMedals(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/select")
|
@GetMapping
|
||||||
@Operation(summary = "Select medal", description = "Select a medal for current user")
|
@Operation(summary = "List medals", description = "List medals for user or globally")
|
||||||
@ApiResponse(responseCode = "200", description = "Medal selected")
|
@ApiResponse(
|
||||||
@SecurityRequirement(name = "JWT")
|
responseCode = "200",
|
||||||
public ResponseEntity<Void> selectMedal(@RequestBody MedalSelectRequest req, Authentication auth) {
|
description = "List of medals",
|
||||||
try {
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = MedalDto.class)))
|
||||||
medalService.selectMedal(auth.getName(), req.getType());
|
)
|
||||||
return ResponseEntity.ok().build();
|
public List<MedalDto> getMedals(@RequestParam(value = "userId", required = false) Long userId) {
|
||||||
} catch (IllegalArgumentException e) {
|
return medalService.getMedals(userId);
|
||||||
return ResponseEntity.badRequest().build();
|
}
|
||||||
}
|
|
||||||
|
@PostMapping("/select")
|
||||||
|
@Operation(summary = "Select medal", description = "Select a medal for current user")
|
||||||
|
@ApiResponse(responseCode = "200", description = "Medal selected")
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
public ResponseEntity<Void> selectMedal(
|
||||||
|
@RequestBody MedalSelectRequest req,
|
||||||
|
Authentication auth
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
medalService.selectMedal(auth.getName(), req.getType());
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,13 @@ import com.openisle.model.MessageConversation;
|
|||||||
import com.openisle.model.User;
|
import com.openisle.model.User;
|
||||||
import com.openisle.repository.UserRepository;
|
import com.openisle.repository.UserRepository;
|
||||||
import com.openisle.service.MessageService;
|
import com.openisle.service.MessageService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
|
import java.util.List;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
@@ -18,153 +25,205 @@ import org.springframework.data.domain.Sort;
|
|||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/messages")
|
@RequestMapping("/api/messages")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class MessageController {
|
public class MessageController {
|
||||||
|
|
||||||
private final MessageService messageService;
|
private final MessageService messageService;
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
// This is a placeholder for getting the current user's ID
|
// This is a placeholder for getting the current user's ID
|
||||||
private Long getCurrentUserId(Authentication auth) {
|
private Long getCurrentUserId(Authentication auth) {
|
||||||
User user = userRepository.findByUsername(auth.getName()).orElseThrow(() -> new IllegalArgumentException("Sender not found"));
|
User user = userRepository
|
||||||
// In a real application, you would get this from the Authentication object
|
.findByUsername(auth.getName())
|
||||||
return user.getId();
|
.orElseThrow(() -> new IllegalArgumentException("Sender not found"));
|
||||||
|
// In a real application, you would get this from the Authentication object
|
||||||
|
return user.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/conversations")
|
||||||
|
@Operation(summary = "List conversations", description = "Get all conversations of current user")
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "List of conversations",
|
||||||
|
content = @Content(
|
||||||
|
array = @ArraySchema(schema = @Schema(implementation = ConversationDto.class))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
public ResponseEntity<List<ConversationDto>> getConversations(Authentication auth) {
|
||||||
|
List<ConversationDto> conversations = messageService.getConversations(getCurrentUserId(auth));
|
||||||
|
return ResponseEntity.ok(conversations);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/conversations/{conversationId}")
|
||||||
|
@Operation(summary = "Get conversation", description = "Get messages of a conversation")
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Conversation detail",
|
||||||
|
content = @Content(schema = @Schema(implementation = ConversationDetailDto.class))
|
||||||
|
)
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
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
|
||||||
|
@Operation(summary = "Send message", description = "Send a direct message to a user")
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Message sent",
|
||||||
|
content = @Content(schema = @Schema(implementation = MessageDto.class))
|
||||||
|
)
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
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")
|
||||||
|
@Operation(summary = "Send message to conversation", description = "Reply within a conversation")
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Message sent",
|
||||||
|
content = @Content(schema = @Schema(implementation = MessageDto.class))
|
||||||
|
)
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
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")
|
||||||
|
@Operation(
|
||||||
|
summary = "Mark conversation read",
|
||||||
|
description = "Mark messages in conversation as read"
|
||||||
|
)
|
||||||
|
@ApiResponse(responseCode = "200", description = "Marked as read")
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
public ResponseEntity<Void> markAsRead(@PathVariable Long conversationId, Authentication auth) {
|
||||||
|
messageService.markConversationAsRead(conversationId, getCurrentUserId(auth));
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/conversations")
|
||||||
|
@Operation(
|
||||||
|
summary = "Find or create conversation",
|
||||||
|
description = "Find existing or create new conversation with recipient"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Conversation id",
|
||||||
|
content = @Content(schema = @Schema(implementation = CreateConversationResponse.class))
|
||||||
|
)
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
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")
|
||||||
|
@Operation(
|
||||||
|
summary = "Unread message count",
|
||||||
|
description = "Get unread message count for current user"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Unread count",
|
||||||
|
content = @Content(schema = @Schema(implementation = Long.class))
|
||||||
|
)
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/conversations")
|
public void setRecipientId(Long recipientId) {
|
||||||
@Operation(summary = "List conversations", description = "Get all conversations of current user")
|
this.recipientId = recipientId;
|
||||||
@ApiResponse(responseCode = "200", description = "List of conversations",
|
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ConversationDto.class))))
|
|
||||||
@SecurityRequirement(name = "JWT")
|
|
||||||
public ResponseEntity<List<ConversationDto>> getConversations(Authentication auth) {
|
|
||||||
List<ConversationDto> conversations = messageService.getConversations(getCurrentUserId(auth));
|
|
||||||
return ResponseEntity.ok(conversations);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/conversations/{conversationId}")
|
public String getContent() {
|
||||||
@Operation(summary = "Get conversation", description = "Get messages of a conversation")
|
return content;
|
||||||
@ApiResponse(responseCode = "200", description = "Conversation detail",
|
|
||||||
content = @Content(schema = @Schema(implementation = ConversationDetailDto.class)))
|
|
||||||
@SecurityRequirement(name = "JWT")
|
|
||||||
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 void setContent(String content) {
|
||||||
@Operation(summary = "Send message", description = "Send a direct message to a user")
|
this.content = content;
|
||||||
@ApiResponse(responseCode = "200", description = "Message sent",
|
|
||||||
content = @Content(schema = @Schema(implementation = MessageDto.class)))
|
|
||||||
@SecurityRequirement(name = "JWT")
|
|
||||||
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 Long getReplyToId() {
|
||||||
@Operation(summary = "Send message to conversation", description = "Reply within a conversation")
|
return replyToId;
|
||||||
@ApiResponse(responseCode = "200", description = "Message sent",
|
|
||||||
content = @Content(schema = @Schema(implementation = MessageDto.class)))
|
|
||||||
@SecurityRequirement(name = "JWT")
|
|
||||||
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 void setReplyToId(Long replyToId) {
|
||||||
@Operation(summary = "Mark conversation read", description = "Mark messages in conversation as read")
|
this.replyToId = replyToId;
|
||||||
@ApiResponse(responseCode = "200", description = "Marked as read")
|
}
|
||||||
@SecurityRequirement(name = "JWT")
|
}
|
||||||
public ResponseEntity<Void> markAsRead(@PathVariable Long conversationId, Authentication auth) {
|
|
||||||
messageService.markConversationAsRead(conversationId, getCurrentUserId(auth));
|
static class ChannelMessageRequest {
|
||||||
return ResponseEntity.ok().build();
|
|
||||||
|
private String content;
|
||||||
|
private Long replyToId;
|
||||||
|
|
||||||
|
public String getContent() {
|
||||||
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/conversations")
|
public void setContent(String content) {
|
||||||
@Operation(summary = "Find or create conversation", description = "Find existing or create new conversation with recipient")
|
this.content = content;
|
||||||
@ApiResponse(responseCode = "200", description = "Conversation id",
|
|
||||||
content = @Content(schema = @Schema(implementation = CreateConversationResponse.class)))
|
|
||||||
@SecurityRequirement(name = "JWT")
|
|
||||||
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 Long getReplyToId() {
|
||||||
@Operation(summary = "Unread message count", description = "Get unread message count for current user")
|
return replyToId;
|
||||||
@ApiResponse(responseCode = "200", description = "Unread count",
|
|
||||||
content = @Content(schema = @Schema(implementation = Long.class)))
|
|
||||||
@SecurityRequirement(name = "JWT")
|
|
||||||
public ResponseEntity<Long> getUnreadCount(Authentication auth) {
|
|
||||||
return ResponseEntity.ok(messageService.getUnreadMessageCount(getCurrentUserId(auth)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// A simple request DTO
|
public void setReplyToId(Long replyToId) {
|
||||||
static class MessageRequest {
|
this.replyToId = replyToId;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,109 +2,158 @@ package com.openisle.controller;
|
|||||||
|
|
||||||
import com.openisle.dto.NotificationDto;
|
import com.openisle.dto.NotificationDto;
|
||||||
import com.openisle.dto.NotificationMarkReadRequest;
|
import com.openisle.dto.NotificationMarkReadRequest;
|
||||||
import com.openisle.dto.NotificationUnreadCountDto;
|
|
||||||
import com.openisle.dto.NotificationPreferenceDto;
|
import com.openisle.dto.NotificationPreferenceDto;
|
||||||
import com.openisle.dto.NotificationPreferenceUpdateRequest;
|
import com.openisle.dto.NotificationPreferenceUpdateRequest;
|
||||||
|
import com.openisle.dto.NotificationUnreadCountDto;
|
||||||
import com.openisle.mapper.NotificationMapper;
|
import com.openisle.mapper.NotificationMapper;
|
||||||
import com.openisle.service.NotificationService;
|
import com.openisle.service.NotificationService;
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
/** Endpoints for user notifications. */
|
/** Endpoints for user notifications. */
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/notifications")
|
@RequestMapping("/api/notifications")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class NotificationController {
|
public class NotificationController {
|
||||||
private final NotificationService notificationService;
|
|
||||||
private final NotificationMapper notificationMapper;
|
|
||||||
|
|
||||||
@GetMapping
|
private final NotificationService notificationService;
|
||||||
@Operation(summary = "List notifications", description = "Retrieve notifications for the current user")
|
private final NotificationMapper notificationMapper;
|
||||||
@ApiResponse(responseCode = "200", description = "Notifications",
|
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = NotificationDto.class))))
|
|
||||||
@SecurityRequirement(name = "JWT")
|
|
||||||
public List<NotificationDto> list(@RequestParam(value = "page", defaultValue = "0") int page,
|
|
||||||
@RequestParam(value = "size", defaultValue = "30") int size,
|
|
||||||
Authentication auth) {
|
|
||||||
return notificationService.listNotifications(auth.getName(), null, page, size).stream()
|
|
||||||
.map(notificationMapper::toDto)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/unread")
|
@GetMapping
|
||||||
@Operation(summary = "List unread notifications", description = "Retrieve unread notifications for the current user")
|
@Operation(
|
||||||
@ApiResponse(responseCode = "200", description = "Unread notifications",
|
summary = "List notifications",
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = NotificationDto.class))))
|
description = "Retrieve notifications for the current user"
|
||||||
@SecurityRequirement(name = "JWT")
|
)
|
||||||
public List<NotificationDto> listUnread(@RequestParam(value = "page", defaultValue = "0") int page,
|
@ApiResponse(
|
||||||
@RequestParam(value = "size", defaultValue = "30") int size,
|
responseCode = "200",
|
||||||
Authentication auth) {
|
description = "Notifications",
|
||||||
return notificationService.listNotifications(auth.getName(), false, page, size).stream()
|
content = @Content(
|
||||||
.map(notificationMapper::toDto)
|
array = @ArraySchema(schema = @Schema(implementation = NotificationDto.class))
|
||||||
.collect(Collectors.toList());
|
)
|
||||||
}
|
)
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
public List<NotificationDto> list(
|
||||||
|
@RequestParam(value = "page", defaultValue = "0") int page,
|
||||||
|
@RequestParam(value = "size", defaultValue = "30") int size,
|
||||||
|
Authentication auth
|
||||||
|
) {
|
||||||
|
return notificationService
|
||||||
|
.listNotifications(auth.getName(), null, page, size)
|
||||||
|
.stream()
|
||||||
|
.map(notificationMapper::toDto)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/unread-count")
|
@GetMapping("/unread")
|
||||||
@Operation(summary = "Unread count", description = "Get count of unread notifications")
|
@Operation(
|
||||||
@ApiResponse(responseCode = "200", description = "Unread count",
|
summary = "List unread notifications",
|
||||||
content = @Content(schema = @Schema(implementation = NotificationUnreadCountDto.class)))
|
description = "Retrieve unread notifications for the current user"
|
||||||
@SecurityRequirement(name = "JWT")
|
)
|
||||||
public NotificationUnreadCountDto unreadCount(Authentication auth) {
|
@ApiResponse(
|
||||||
long count = notificationService.countUnread(auth.getName());
|
responseCode = "200",
|
||||||
NotificationUnreadCountDto uc = new NotificationUnreadCountDto();
|
description = "Unread notifications",
|
||||||
uc.setCount(count);
|
content = @Content(
|
||||||
return uc;
|
array = @ArraySchema(schema = @Schema(implementation = NotificationDto.class))
|
||||||
}
|
)
|
||||||
|
)
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/read")
|
@GetMapping("/unread-count")
|
||||||
@Operation(summary = "Mark notifications read", description = "Mark notifications as read")
|
@Operation(summary = "Unread count", description = "Get count of unread notifications")
|
||||||
@ApiResponse(responseCode = "200", description = "Marked read")
|
@ApiResponse(
|
||||||
@SecurityRequirement(name = "JWT")
|
responseCode = "200",
|
||||||
public void markRead(@RequestBody NotificationMarkReadRequest req, Authentication auth) {
|
description = "Unread count",
|
||||||
notificationService.markRead(auth.getName(), req.getIds());
|
content = @Content(schema = @Schema(implementation = NotificationUnreadCountDto.class))
|
||||||
}
|
)
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
public NotificationUnreadCountDto unreadCount(Authentication auth) {
|
||||||
|
long count = notificationService.countUnread(auth.getName());
|
||||||
|
NotificationUnreadCountDto uc = new NotificationUnreadCountDto();
|
||||||
|
uc.setCount(count);
|
||||||
|
return uc;
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/prefs")
|
@PostMapping("/read")
|
||||||
@Operation(summary = "List preferences", description = "List notification preferences")
|
@Operation(summary = "Mark notifications read", description = "Mark notifications as read")
|
||||||
@ApiResponse(responseCode = "200", description = "Preferences",
|
@ApiResponse(responseCode = "200", description = "Marked read")
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = NotificationPreferenceDto.class))))
|
@SecurityRequirement(name = "JWT")
|
||||||
@SecurityRequirement(name = "JWT")
|
public void markRead(@RequestBody NotificationMarkReadRequest req, Authentication auth) {
|
||||||
public List<NotificationPreferenceDto> prefs(Authentication auth) {
|
notificationService.markRead(auth.getName(), req.getIds());
|
||||||
return notificationService.listPreferences(auth.getName());
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/prefs")
|
@GetMapping("/prefs")
|
||||||
@Operation(summary = "Update preference", description = "Update notification preference")
|
@Operation(summary = "List preferences", description = "List notification preferences")
|
||||||
@ApiResponse(responseCode = "200", description = "Preference updated")
|
@ApiResponse(
|
||||||
@SecurityRequirement(name = "JWT")
|
responseCode = "200",
|
||||||
public void updatePref(@RequestBody NotificationPreferenceUpdateRequest req, Authentication auth) {
|
description = "Preferences",
|
||||||
notificationService.updatePreference(auth.getName(), req.getType(), req.isEnabled());
|
content = @Content(
|
||||||
}
|
array = @ArraySchema(schema = @Schema(implementation = NotificationPreferenceDto.class))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
public List<NotificationPreferenceDto> prefs(Authentication auth) {
|
||||||
|
return notificationService.listPreferences(auth.getName());
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/email-prefs")
|
@PostMapping("/prefs")
|
||||||
@Operation(summary = "List email preferences", description = "List email notification preferences")
|
@Operation(summary = "Update preference", description = "Update notification preference")
|
||||||
@ApiResponse(responseCode = "200", description = "Email preferences",
|
@ApiResponse(responseCode = "200", description = "Preference updated")
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = NotificationPreferenceDto.class))))
|
@SecurityRequirement(name = "JWT")
|
||||||
@SecurityRequirement(name = "JWT")
|
public void updatePref(
|
||||||
public List<NotificationPreferenceDto> emailPrefs(Authentication auth) {
|
@RequestBody NotificationPreferenceUpdateRequest req,
|
||||||
return notificationService.listEmailPreferences(auth.getName());
|
Authentication auth
|
||||||
}
|
) {
|
||||||
|
notificationService.updatePreference(auth.getName(), req.getType(), req.isEnabled());
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/email-prefs")
|
@GetMapping("/email-prefs")
|
||||||
@Operation(summary = "Update email preference", description = "Update email notification preference")
|
@Operation(
|
||||||
@ApiResponse(responseCode = "200", description = "Email preference updated")
|
summary = "List email preferences",
|
||||||
@SecurityRequirement(name = "JWT")
|
description = "List email notification preferences"
|
||||||
public void updateEmailPref(@RequestBody NotificationPreferenceUpdateRequest req, Authentication auth) {
|
)
|
||||||
notificationService.updateEmailPreference(auth.getName(), req.getType(), req.isEnabled());
|
@ApiResponse(
|
||||||
}
|
responseCode = "200",
|
||||||
|
description = "Email preferences",
|
||||||
|
content = @Content(
|
||||||
|
array = @ArraySchema(schema = @Schema(implementation = NotificationPreferenceDto.class))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
public List<NotificationPreferenceDto> emailPrefs(Authentication auth) {
|
||||||
|
return notificationService.listEmailPreferences(auth.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/email-prefs")
|
||||||
|
@Operation(
|
||||||
|
summary = "Update email preference",
|
||||||
|
description = "Update email notification preference"
|
||||||
|
)
|
||||||
|
@ApiResponse(responseCode = "200", description = "Email preference updated")
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
public void updateEmailPref(
|
||||||
|
@RequestBody NotificationPreferenceUpdateRequest req,
|
||||||
|
Authentication auth
|
||||||
|
) {
|
||||||
|
notificationService.updateEmailPreference(auth.getName(), req.getType(), req.isEnabled());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
package com.openisle.controller;
|
package com.openisle.controller;
|
||||||
|
|
||||||
import com.openisle.config.CachingConfig;
|
import com.openisle.config.CachingConfig;
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author smallclover
|
* @author smallclover
|
||||||
@@ -22,21 +21,24 @@ import java.time.Duration;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class OnlineController {
|
public class OnlineController {
|
||||||
|
|
||||||
private final RedisTemplate redisTemplate;
|
private final RedisTemplate redisTemplate;
|
||||||
private static final String ONLINE_KEY = CachingConfig.ONLINE_CACHE_NAME +":";
|
private static final String ONLINE_KEY = CachingConfig.ONLINE_CACHE_NAME + ":";
|
||||||
|
|
||||||
@PostMapping("/heartbeat")
|
@PostMapping("/heartbeat")
|
||||||
@Operation(summary = "Heartbeat", description = "Record user heartbeat")
|
@Operation(summary = "Heartbeat", description = "Record user heartbeat")
|
||||||
@ApiResponse(responseCode = "200", description = "Heartbeat recorded")
|
@ApiResponse(responseCode = "200", description = "Heartbeat recorded")
|
||||||
public void ping(@RequestParam String userId){
|
public void ping(@RequestParam String userId) {
|
||||||
redisTemplate.opsForValue().set(ONLINE_KEY+userId,"1", Duration.ofSeconds(150));
|
redisTemplate.opsForValue().set(ONLINE_KEY + userId, "1", Duration.ofSeconds(150));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/count")
|
@GetMapping("/count")
|
||||||
@Operation(summary = "Online count", description = "Get current online user count")
|
@Operation(summary = "Online count", description = "Get current online user count")
|
||||||
@ApiResponse(responseCode = "200", description = "Online count",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = Long.class)))
|
responseCode = "200",
|
||||||
public long count(){
|
description = "Online count",
|
||||||
return redisTemplate.keys(ONLINE_KEY+"*").size();
|
content = @Content(schema = @Schema(implementation = Long.class))
|
||||||
}
|
)
|
||||||
|
public long count() {
|
||||||
|
return redisTemplate.keys(ONLINE_KEY + "*").size();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,48 +3,60 @@ package com.openisle.controller;
|
|||||||
import com.openisle.dto.PointHistoryDto;
|
import com.openisle.dto.PointHistoryDto;
|
||||||
import com.openisle.mapper.PointHistoryMapper;
|
import com.openisle.mapper.PointHistoryMapper;
|
||||||
import com.openisle.service.PointService;
|
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 io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
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;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/point-histories")
|
@RequestMapping("/api/point-histories")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class PointHistoryController {
|
public class PointHistoryController {
|
||||||
private final PointService pointService;
|
|
||||||
private final PointHistoryMapper pointHistoryMapper;
|
|
||||||
|
|
||||||
@GetMapping
|
private final PointService pointService;
|
||||||
@Operation(summary = "Point history", description = "List point history for current user")
|
private final PointHistoryMapper pointHistoryMapper;
|
||||||
@ApiResponse(responseCode = "200", description = "List of point histories",
|
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PointHistoryDto.class))))
|
|
||||||
@SecurityRequirement(name = "JWT")
|
|
||||||
public List<PointHistoryDto> list(Authentication auth) {
|
|
||||||
return pointService.listHistory(auth.getName()).stream()
|
|
||||||
.map(pointHistoryMapper::toDto)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/trend")
|
@GetMapping
|
||||||
@Operation(summary = "Point trend", description = "Get point trend data for current user")
|
@Operation(summary = "Point history", description = "List point history for current user")
|
||||||
@ApiResponse(responseCode = "200", description = "Trend data",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = java.util.Map.class))))
|
responseCode = "200",
|
||||||
@SecurityRequirement(name = "JWT")
|
description = "List of point histories",
|
||||||
public List<Map<String, Object>> trend(Authentication auth,
|
content = @Content(
|
||||||
@RequestParam(value = "days", defaultValue = "30") int days) {
|
array = @ArraySchema(schema = @Schema(implementation = PointHistoryDto.class))
|
||||||
return pointService.trend(auth.getName(), days);
|
)
|
||||||
}
|
)
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
public List<PointHistoryDto> list(Authentication auth) {
|
||||||
|
return pointService
|
||||||
|
.listHistory(auth.getName())
|
||||||
|
.stream()
|
||||||
|
.map(pointHistoryMapper::toDto)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/trend")
|
||||||
|
@Operation(summary = "Point trend", description = "Get point trend data for current user")
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Trend data",
|
||||||
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = java.util.Map.class)))
|
||||||
|
)
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
public List<Map<String, Object>> trend(
|
||||||
|
Authentication auth,
|
||||||
|
@RequestParam(value = "days", defaultValue = "30") int days
|
||||||
|
) {
|
||||||
|
return pointService.trend(auth.getName(), days);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,47 +6,55 @@ import com.openisle.mapper.PointGoodMapper;
|
|||||||
import com.openisle.model.User;
|
import com.openisle.model.User;
|
||||||
import com.openisle.service.PointMallService;
|
import com.openisle.service.PointMallService;
|
||||||
import com.openisle.service.UserService;
|
import com.openisle.service.UserService;
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
/** REST controller for point mall. */
|
/** REST controller for point mall. */
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/point-goods")
|
@RequestMapping("/api/point-goods")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class PointMallController {
|
public class PointMallController {
|
||||||
private final PointMallService pointMallService;
|
|
||||||
private final UserService userService;
|
|
||||||
private final PointGoodMapper pointGoodMapper;
|
|
||||||
|
|
||||||
@GetMapping
|
private final PointMallService pointMallService;
|
||||||
@Operation(summary = "List goods", description = "List all point goods")
|
private final UserService userService;
|
||||||
@ApiResponse(responseCode = "200", description = "List of goods",
|
private final PointGoodMapper pointGoodMapper;
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PointGoodDto.class))))
|
|
||||||
public List<PointGoodDto> list() {
|
|
||||||
return pointMallService.listGoods().stream()
|
|
||||||
.map(pointGoodMapper::toDto)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/redeem")
|
@GetMapping
|
||||||
@Operation(summary = "Redeem good", description = "Redeem a point good")
|
@Operation(summary = "List goods", description = "List all point goods")
|
||||||
@ApiResponse(responseCode = "200", description = "Remaining points",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = java.util.Map.class)))
|
responseCode = "200",
|
||||||
@SecurityRequirement(name = "JWT")
|
description = "List of goods",
|
||||||
public Map<String, Integer> redeem(@RequestBody PointRedeemRequest req, Authentication auth) {
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PointGoodDto.class)))
|
||||||
User user = userService.findByIdentifier(auth.getName()).orElseThrow();
|
)
|
||||||
int point = pointMallService.redeem(user, req.getGoodId(), req.getContact());
|
public List<PointGoodDto> list() {
|
||||||
return Map.of("point", point);
|
return pointMallService
|
||||||
}
|
.listGoods()
|
||||||
|
.stream()
|
||||||
|
.map(pointGoodMapper::toDto)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/redeem")
|
||||||
|
@Operation(summary = "Redeem good", description = "Redeem a point good")
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Remaining points",
|
||||||
|
content = @Content(schema = @Schema(implementation = java.util.Map.class))
|
||||||
|
)
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
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,31 +3,34 @@ package com.openisle.controller;
|
|||||||
import com.openisle.dto.PostChangeLogDto;
|
import com.openisle.dto.PostChangeLogDto;
|
||||||
import com.openisle.mapper.PostChangeLogMapper;
|
import com.openisle.mapper.PostChangeLogMapper;
|
||||||
import com.openisle.service.PostChangeLogService;
|
import com.openisle.service.PostChangeLogService;
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/posts")
|
@RequestMapping("/api/posts")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class PostChangeLogController {
|
public class PostChangeLogController {
|
||||||
private final PostChangeLogService changeLogService;
|
|
||||||
private final PostChangeLogMapper mapper;
|
|
||||||
|
|
||||||
@GetMapping("/{id}/change-logs")
|
private final PostChangeLogService changeLogService;
|
||||||
@Operation(summary = "Post change logs", description = "List change logs for a post")
|
private final PostChangeLogMapper mapper;
|
||||||
@ApiResponse(responseCode = "200", description = "Change logs",
|
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostChangeLogDto.class))))
|
@GetMapping("/{id}/change-logs")
|
||||||
public List<PostChangeLogDto> listLogs(@PathVariable Long id) {
|
@Operation(summary = "Post change logs", description = "List change logs for a post")
|
||||||
return changeLogService.listLogs(id).stream()
|
@ApiResponse(
|
||||||
.map(mapper::toDto)
|
responseCode = "200",
|
||||||
.collect(Collectors.toList());
|
description = "Change logs",
|
||||||
}
|
content = @Content(
|
||||||
|
array = @ArraySchema(schema = @Schema(implementation = PostChangeLogDto.class))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
public List<PostChangeLogDto> listLogs(@PathVariable Long id) {
|
||||||
|
return changeLogService.listLogs(id).stream().map(mapper::toDto).collect(Collectors.toList());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
package com.openisle.controller;
|
package com.openisle.controller;
|
||||||
|
|
||||||
|
import com.openisle.config.CachingConfig;
|
||||||
|
import com.openisle.dto.PollDto;
|
||||||
import com.openisle.dto.PostDetailDto;
|
import com.openisle.dto.PostDetailDto;
|
||||||
import com.openisle.dto.PostRequest;
|
import com.openisle.dto.PostRequest;
|
||||||
import com.openisle.dto.PostSummaryDto;
|
import com.openisle.dto.PostSummaryDto;
|
||||||
import com.openisle.dto.PollDto;
|
|
||||||
import com.openisle.mapper.PostMapper;
|
import com.openisle.mapper.PostMapper;
|
||||||
import com.openisle.model.Post;
|
import com.openisle.model.Post;
|
||||||
import com.openisle.service.*;
|
import com.openisle.service.*;
|
||||||
@@ -13,249 +14,329 @@ import io.swagger.v3.oas.annotations.media.Content;
|
|||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.cache.annotation.Cacheable;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/posts")
|
@RequestMapping("/api/posts")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class PostController {
|
public class PostController {
|
||||||
private final PostService postService;
|
|
||||||
private final LevelService levelService;
|
|
||||||
private final CaptchaService captchaService;
|
|
||||||
private final DraftService draftService;
|
|
||||||
private final UserVisitService userVisitService;
|
|
||||||
private final PostMapper postMapper;
|
|
||||||
private final PointService pointService;
|
|
||||||
|
|
||||||
@Value("${app.captcha.enabled:false}")
|
private final PostService postService;
|
||||||
private boolean captchaEnabled;
|
private final CategoryService categoryService;
|
||||||
|
private final TagService tagService;
|
||||||
|
private final LevelService levelService;
|
||||||
|
private final CaptchaService captchaService;
|
||||||
|
private final DraftService draftService;
|
||||||
|
private final UserVisitService userVisitService;
|
||||||
|
private final PostMapper postMapper;
|
||||||
|
private final PointService pointService;
|
||||||
|
|
||||||
@Value("${app.captcha.post-enabled:false}")
|
@Value("${app.captcha.enabled:false}")
|
||||||
private boolean postCaptchaEnabled;
|
private boolean captchaEnabled;
|
||||||
|
|
||||||
@PostMapping
|
@Value("${app.captcha.post-enabled:false}")
|
||||||
@SecurityRequirement(name = "JWT")
|
private boolean postCaptchaEnabled;
|
||||||
@Operation(summary = "Create post", description = "Create a new post")
|
|
||||||
@ApiResponse(responseCode = "200", description = "Created post",
|
@PostMapping
|
||||||
content = @Content(schema = @Schema(implementation = PostDetailDto.class)))
|
@SecurityRequirement(name = "JWT")
|
||||||
public ResponseEntity<PostDetailDto> createPost(@RequestBody PostRequest req, Authentication auth) {
|
@Operation(summary = "Create post", description = "Create a new post")
|
||||||
if (captchaEnabled && postCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
|
@ApiResponse(
|
||||||
return ResponseEntity.badRequest().build();
|
responseCode = "200",
|
||||||
}
|
description = "Created post",
|
||||||
Post post = postService.createPost(auth.getName(), req.getCategoryId(),
|
content = @Content(schema = @Schema(implementation = PostDetailDto.class))
|
||||||
req.getTitle(), req.getContent(), req.getTagIds(),
|
)
|
||||||
req.getType(), req.getPrizeDescription(), req.getPrizeIcon(),
|
public ResponseEntity<PostDetailDto> createPost(
|
||||||
req.getPrizeCount(), req.getPointCost(),
|
@RequestBody PostRequest req,
|
||||||
req.getStartTime(), req.getEndTime(),
|
Authentication auth
|
||||||
req.getOptions(), req.getMultiple());
|
) {
|
||||||
draftService.deleteDraft(auth.getName());
|
if (captchaEnabled && postCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
|
||||||
PostDetailDto dto = postMapper.toDetailDto(post, auth.getName());
|
return ResponseEntity.badRequest().build();
|
||||||
dto.setReward(levelService.awardForPost(auth.getName()));
|
|
||||||
dto.setPointReward(pointService.awardForPost(auth.getName(), post.getId()));
|
|
||||||
return ResponseEntity.ok(dto);
|
|
||||||
}
|
}
|
||||||
|
Post post = postService.createPost(
|
||||||
|
auth.getName(),
|
||||||
|
req.getCategoryId(),
|
||||||
|
req.getTitle(),
|
||||||
|
req.getContent(),
|
||||||
|
req.getTagIds(),
|
||||||
|
req.getType(),
|
||||||
|
req.getPostVisibleScopeType(),
|
||||||
|
req.getPrizeDescription(),
|
||||||
|
req.getPrizeIcon(),
|
||||||
|
req.getPrizeCount(),
|
||||||
|
req.getPointCost(),
|
||||||
|
req.getStartTime(),
|
||||||
|
req.getEndTime(),
|
||||||
|
req.getOptions(),
|
||||||
|
req.getMultiple(),
|
||||||
|
req.getProposedName(),
|
||||||
|
req.getProposalDescription()
|
||||||
|
);
|
||||||
|
draftService.deleteDraft(auth.getName());
|
||||||
|
PostDetailDto dto = postMapper.toDetailDto(post, auth.getName());
|
||||||
|
dto.setReward(levelService.awardForPost(auth.getName()));
|
||||||
|
dto.setPointReward(pointService.awardForPost(auth.getName(), post.getId()));
|
||||||
|
return ResponseEntity.ok(dto);
|
||||||
|
}
|
||||||
|
|
||||||
@PutMapping("/{id}")
|
@PutMapping("/{id}")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "Update post", description = "Update an existing post")
|
@Operation(summary = "Update post", description = "Update an existing post")
|
||||||
@ApiResponse(responseCode = "200", description = "Updated post",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = PostDetailDto.class)))
|
responseCode = "200",
|
||||||
public ResponseEntity<PostDetailDto> updatePost(@PathVariable Long id, @RequestBody PostRequest req,
|
description = "Updated post",
|
||||||
Authentication auth) {
|
content = @Content(schema = @Schema(implementation = PostDetailDto.class))
|
||||||
Post post = postService.updatePost(id, auth.getName(), req.getCategoryId(),
|
)
|
||||||
req.getTitle(), req.getContent(), req.getTagIds());
|
public ResponseEntity<PostDetailDto> updatePost(
|
||||||
return ResponseEntity.ok(postMapper.toDetailDto(post, auth.getName()));
|
@PathVariable Long id,
|
||||||
}
|
@RequestBody PostRequest req,
|
||||||
|
Authentication auth
|
||||||
|
) {
|
||||||
|
Post post = postService.updatePost(
|
||||||
|
id,
|
||||||
|
auth.getName(),
|
||||||
|
req.getCategoryId(),
|
||||||
|
req.getTitle(),
|
||||||
|
req.getContent(),
|
||||||
|
req.getTagIds(),
|
||||||
|
req.getPostVisibleScopeType()
|
||||||
|
);
|
||||||
|
return ResponseEntity.ok(postMapper.toDetailDto(post, auth.getName()));
|
||||||
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{id}")
|
@DeleteMapping("/{id}")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "Delete post", description = "Delete a post")
|
@Operation(summary = "Delete post", description = "Delete a post")
|
||||||
@ApiResponse(responseCode = "200", description = "Post deleted")
|
@ApiResponse(responseCode = "200", description = "Post deleted")
|
||||||
public void deletePost(@PathVariable Long id, Authentication auth) {
|
public void deletePost(@PathVariable Long id, Authentication auth) {
|
||||||
postService.deletePost(id, auth.getName());
|
postService.deletePost(id, auth.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/{id}/close")
|
@PostMapping("/{id}/close")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "Close post", description = "Close a post to prevent further replies")
|
@Operation(summary = "Close post", description = "Close a post to prevent further replies")
|
||||||
@ApiResponse(responseCode = "200", description = "Closed post",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = PostSummaryDto.class)))
|
responseCode = "200",
|
||||||
public PostSummaryDto close(@PathVariable Long id, Authentication auth) {
|
description = "Closed post",
|
||||||
return postMapper.toSummaryDto(postService.closePost(id, auth.getName()));
|
content = @Content(schema = @Schema(implementation = PostSummaryDto.class))
|
||||||
}
|
)
|
||||||
|
public PostSummaryDto close(@PathVariable Long id, Authentication auth) {
|
||||||
|
return postMapper.toSummaryDto(postService.closePost(id, auth.getName()));
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/{id}/reopen")
|
@PostMapping("/{id}/reopen")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "Reopen post", description = "Reopen a closed post")
|
@Operation(summary = "Reopen post", description = "Reopen a closed post")
|
||||||
@ApiResponse(responseCode = "200", description = "Reopened post",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = PostSummaryDto.class)))
|
responseCode = "200",
|
||||||
public PostSummaryDto reopen(@PathVariable Long id, Authentication auth) {
|
description = "Reopened post",
|
||||||
return postMapper.toSummaryDto(postService.reopenPost(id, auth.getName()));
|
content = @Content(schema = @Schema(implementation = PostSummaryDto.class))
|
||||||
}
|
)
|
||||||
|
public PostSummaryDto reopen(@PathVariable Long id, Authentication auth) {
|
||||||
|
return postMapper.toSummaryDto(postService.reopenPost(id, auth.getName()));
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
@Operation(summary = "Get post", description = "Get post details by id")
|
@Operation(summary = "Get post", description = "Get post details by id")
|
||||||
@ApiResponse(responseCode = "200", description = "Post detail",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = PostDetailDto.class)))
|
responseCode = "200",
|
||||||
public ResponseEntity<PostDetailDto> getPost(@PathVariable Long id, Authentication auth) {
|
description = "Post detail",
|
||||||
String viewer = auth != null ? auth.getName() : null;
|
content = @Content(schema = @Schema(implementation = PostDetailDto.class))
|
||||||
Post post = postService.viewPost(id, viewer);
|
)
|
||||||
return ResponseEntity.ok(postMapper.toDetailDto(post, viewer));
|
public ResponseEntity<PostDetailDto> getPost(@PathVariable Long id, Authentication auth) {
|
||||||
}
|
String viewer = auth != null ? auth.getName() : null;
|
||||||
|
Post post = postService.viewPost(id, viewer);
|
||||||
|
return ResponseEntity.ok(postMapper.toDetailDto(post, viewer));
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/{id}/lottery/join")
|
@PostMapping("/{id}/lottery/join")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "Join lottery", description = "Join a lottery for the post")
|
@Operation(summary = "Join lottery", description = "Join a lottery for the post")
|
||||||
@ApiResponse(responseCode = "200", description = "Joined lottery")
|
@ApiResponse(responseCode = "200", description = "Joined lottery")
|
||||||
public ResponseEntity<Void> joinLottery(@PathVariable Long id, Authentication auth) {
|
public ResponseEntity<Void> joinLottery(@PathVariable Long id, Authentication auth) {
|
||||||
postService.joinLottery(id, auth.getName());
|
postService.joinLottery(id, auth.getName());
|
||||||
return ResponseEntity.ok().build();
|
return ResponseEntity.ok().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}/poll/progress")
|
@GetMapping("/{id}/poll/progress")
|
||||||
@Operation(summary = "Poll progress", description = "Get poll progress for a post")
|
@Operation(summary = "Poll progress", description = "Get poll progress for a post")
|
||||||
@ApiResponse(responseCode = "200", description = "Poll progress",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = PollDto.class)))
|
responseCode = "200",
|
||||||
public ResponseEntity<PollDto> pollProgress(@PathVariable Long id) {
|
description = "Poll progress",
|
||||||
return ResponseEntity.ok(postMapper.toSummaryDto(postService.getPoll(id)).getPoll());
|
content = @Content(schema = @Schema(implementation = PollDto.class))
|
||||||
}
|
)
|
||||||
|
public ResponseEntity<PollDto> pollProgress(@PathVariable Long id) {
|
||||||
|
return ResponseEntity.ok(postMapper.toSummaryDto(postService.getPoll(id)).getPoll());
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/{id}/poll/vote")
|
@PostMapping("/{id}/poll/vote")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "Vote poll", description = "Vote on a poll option")
|
@Operation(summary = "Vote poll", description = "Vote on a poll option")
|
||||||
@ApiResponse(responseCode = "200", description = "Vote recorded")
|
@ApiResponse(responseCode = "200", description = "Vote recorded")
|
||||||
public ResponseEntity<Void> vote(@PathVariable Long id, @RequestParam("option") List<Integer> option, Authentication auth) {
|
public ResponseEntity<Void> vote(
|
||||||
postService.votePoll(id, auth.getName(), option);
|
@PathVariable Long id,
|
||||||
return ResponseEntity.ok().build();
|
@RequestParam("option") List<Integer> option,
|
||||||
}
|
Authentication auth
|
||||||
|
) {
|
||||||
|
postService.votePoll(id, auth.getName(), option);
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@Operation(summary = "List posts", description = "List posts by various filters")
|
@Operation(summary = "List posts", description = "List posts by various filters")
|
||||||
@ApiResponse(responseCode = "200", description = "List of posts",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
|
responseCode = "200",
|
||||||
public List<PostSummaryDto> listPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
|
description = "List of posts",
|
||||||
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
|
content = @Content(
|
||||||
@RequestParam(value = "tagId", required = false) Long tagId,
|
array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))
|
||||||
@RequestParam(value = "tagIds", required = false) List<Long> tagIds,
|
)
|
||||||
@RequestParam(value = "page", required = false) Integer page,
|
)
|
||||||
@RequestParam(value = "pageSize", required = false) Integer pageSize,
|
@Cacheable(
|
||||||
Authentication auth) {
|
value = CachingConfig.POST_CACHE_NAME,
|
||||||
List<Long> ids = categoryIds;
|
key = "new org.springframework.cache.interceptor.SimpleKey('default', #categoryId, #categoryIds, #tagId, #tagIds, #page, #pageSize)"
|
||||||
if (categoryId != null) {
|
)
|
||||||
ids = java.util.List.of(categoryId);
|
public List<PostSummaryDto> listPosts(
|
||||||
}
|
@RequestParam(value = "categoryId", required = false) Long categoryId,
|
||||||
List<Long> tids = tagIds;
|
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
|
||||||
if (tagId != null) {
|
@RequestParam(value = "tagId", required = false) Long tagId,
|
||||||
tids = java.util.List.of(tagId);
|
@RequestParam(value = "tagIds", required = false) List<Long> tagIds,
|
||||||
}
|
@RequestParam(value = "page", required = false) Integer page,
|
||||||
// 只需要在请求的一开始统计一次
|
@RequestParam(value = "pageSize", required = false) Integer pageSize,
|
||||||
// if (auth != null) {
|
Authentication auth
|
||||||
// userVisitService.recordVisit(auth.getName());
|
) {
|
||||||
// }
|
List<Long> ids = categoryService.getSearchCategoryIds(categoryIds, categoryId);
|
||||||
|
List<Long> tids = tagService.getSearchTagIds(tagIds, tagId);
|
||||||
|
// 只需要在请求的一开始统计一次
|
||||||
|
// if (auth != null) {
|
||||||
|
// userVisitService.recordVisit(auth.getName());
|
||||||
|
// }
|
||||||
|
|
||||||
boolean hasCategories = ids != null && !ids.isEmpty();
|
return postService
|
||||||
boolean hasTags = tids != null && !tids.isEmpty();
|
.defaultListPosts(ids, tids, page, pageSize)
|
||||||
|
.stream()
|
||||||
|
.map(postMapper::toSummaryDto)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
if (hasCategories && hasTags) {
|
@GetMapping("/recent")
|
||||||
return postService.listPostsByCategoriesAndTags(ids, tids, page, pageSize)
|
@Operation(
|
||||||
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
|
summary = "Recent posts",
|
||||||
}
|
description = "List posts created within the specified number of minutes"
|
||||||
if (hasTags) {
|
)
|
||||||
return postService.listPostsByTags(tids, page, pageSize)
|
@ApiResponse(
|
||||||
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
|
responseCode = "200",
|
||||||
}
|
description = "Recent posts",
|
||||||
|
content = @Content(
|
||||||
|
array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
public List<PostSummaryDto> recentPosts(@RequestParam("minutes") int minutes) {
|
||||||
|
return postService
|
||||||
|
.listRecentPosts(minutes)
|
||||||
|
.stream()
|
||||||
|
.map(postMapper::toSummaryDto)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
return postService.listPostsByCategories(ids, page, pageSize)
|
@GetMapping("/ranking")
|
||||||
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
|
@Operation(summary = "Ranking posts", description = "List posts by view rankings")
|
||||||
}
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Ranked posts",
|
||||||
|
content = @Content(
|
||||||
|
array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
public List<PostSummaryDto> rankingPosts(
|
||||||
|
@RequestParam(value = "categoryId", required = false) Long categoryId,
|
||||||
|
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
|
||||||
|
@RequestParam(value = "tagId", required = false) Long tagId,
|
||||||
|
@RequestParam(value = "tagIds", required = false) List<Long> tagIds,
|
||||||
|
@RequestParam(value = "page", required = false) Integer page,
|
||||||
|
@RequestParam(value = "pageSize", required = false) Integer pageSize,
|
||||||
|
Authentication auth
|
||||||
|
) {
|
||||||
|
List<Long> ids = categoryService.getSearchCategoryIds(categoryIds, categoryId);
|
||||||
|
List<Long> tids = tagService.getSearchTagIds(tagIds, tagId);
|
||||||
|
// 只需要在请求的一开始统计一次
|
||||||
|
// if (auth != null) {
|
||||||
|
// userVisitService.recordVisit(auth.getName());
|
||||||
|
// }
|
||||||
|
|
||||||
@GetMapping("/ranking")
|
return postService
|
||||||
@Operation(summary = "Ranking posts", description = "List posts by view rankings")
|
.listPostsByViews(ids, tids, page, pageSize)
|
||||||
@ApiResponse(responseCode = "200", description = "Ranked posts",
|
.stream()
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
|
.map(postMapper::toSummaryDto)
|
||||||
public List<PostSummaryDto> rankingPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
|
.collect(Collectors.toList());
|
||||||
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
|
}
|
||||||
@RequestParam(value = "tagId", required = false) Long tagId,
|
|
||||||
@RequestParam(value = "tagIds", required = false) List<Long> tagIds,
|
|
||||||
@RequestParam(value = "page", required = false) Integer page,
|
|
||||||
@RequestParam(value = "pageSize", required = false) Integer pageSize,
|
|
||||||
Authentication auth) {
|
|
||||||
List<Long> ids = categoryIds;
|
|
||||||
if (categoryId != null) {
|
|
||||||
ids = java.util.List.of(categoryId);
|
|
||||||
}
|
|
||||||
List<Long> tids = tagIds;
|
|
||||||
if (tagId != null) {
|
|
||||||
tids = java.util.List.of(tagId);
|
|
||||||
}
|
|
||||||
// 只需要在请求的一开始统计一次
|
|
||||||
// if (auth != null) {
|
|
||||||
// userVisitService.recordVisit(auth.getName());
|
|
||||||
// }
|
|
||||||
|
|
||||||
return postService.listPostsByViews(ids, tids, page, pageSize)
|
@GetMapping("/latest-reply")
|
||||||
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
|
@Operation(summary = "Latest reply posts", description = "List posts by latest replies")
|
||||||
}
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Posts sorted by latest reply",
|
||||||
|
content = @Content(
|
||||||
|
array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@Cacheable(
|
||||||
|
value = CachingConfig.POST_CACHE_NAME,
|
||||||
|
key = "new org.springframework.cache.interceptor.SimpleKey('latest_reply', #categoryId, #categoryIds, #tagIds, #page, #pageSize)"
|
||||||
|
)
|
||||||
|
public List<PostSummaryDto> latestReplyPosts(
|
||||||
|
@RequestParam(value = "categoryId", required = false) Long categoryId,
|
||||||
|
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
|
||||||
|
@RequestParam(value = "tagId", required = false) Long tagId,
|
||||||
|
@RequestParam(value = "tagIds", required = false) List<Long> tagIds,
|
||||||
|
@RequestParam(value = "page", required = false) Integer page,
|
||||||
|
@RequestParam(value = "pageSize", required = false) Integer pageSize,
|
||||||
|
Authentication auth
|
||||||
|
) {
|
||||||
|
List<Long> ids = categoryService.getSearchCategoryIds(categoryIds, categoryId);
|
||||||
|
List<Long> tids = tagService.getSearchTagIds(tagIds, tagId);
|
||||||
|
// 只需要在请求的一开始统计一次
|
||||||
|
// if (auth != null) {
|
||||||
|
// userVisitService.recordVisit(auth.getName());
|
||||||
|
// }
|
||||||
|
|
||||||
@GetMapping("/latest-reply")
|
List<Post> posts = postService.listPostsByLatestReply(ids, tids, page, pageSize);
|
||||||
@Operation(summary = "Latest reply posts", description = "List posts by latest replies")
|
return posts.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
|
||||||
@ApiResponse(responseCode = "200", description = "Posts sorted by latest reply",
|
}
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
|
|
||||||
public List<PostSummaryDto> latestReplyPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
|
|
||||||
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
|
|
||||||
@RequestParam(value = "tagId", required = false) Long tagId,
|
|
||||||
@RequestParam(value = "tagIds", required = false) List<Long> tagIds,
|
|
||||||
@RequestParam(value = "page", required = false) Integer page,
|
|
||||||
@RequestParam(value = "pageSize", required = false) Integer pageSize,
|
|
||||||
Authentication auth) {
|
|
||||||
List<Long> ids = categoryIds;
|
|
||||||
if (categoryId != null) {
|
|
||||||
ids = java.util.List.of(categoryId);
|
|
||||||
}
|
|
||||||
List<Long> tids = tagIds;
|
|
||||||
if (tagId != null) {
|
|
||||||
tids = java.util.List.of(tagId);
|
|
||||||
}
|
|
||||||
// 只需要在请求的一开始统计一次
|
|
||||||
// if (auth != null) {
|
|
||||||
// userVisitService.recordVisit(auth.getName());
|
|
||||||
// }
|
|
||||||
|
|
||||||
return postService.listPostsByLatestReply(ids, tids, page, pageSize)
|
@GetMapping("/featured")
|
||||||
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
|
@Operation(summary = "Featured posts", description = "List featured posts")
|
||||||
}
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
@GetMapping("/featured")
|
description = "Featured posts",
|
||||||
@Operation(summary = "Featured posts", description = "List featured posts")
|
content = @Content(
|
||||||
@ApiResponse(responseCode = "200", description = "Featured posts",
|
array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
|
)
|
||||||
public List<PostSummaryDto> featuredPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
|
)
|
||||||
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
|
public List<PostSummaryDto> featuredPosts(
|
||||||
@RequestParam(value = "tagId", required = false) Long tagId,
|
@RequestParam(value = "categoryId", required = false) Long categoryId,
|
||||||
@RequestParam(value = "tagIds", required = false) List<Long> tagIds,
|
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
|
||||||
@RequestParam(value = "page", required = false) Integer page,
|
@RequestParam(value = "tagId", required = false) Long tagId,
|
||||||
@RequestParam(value = "pageSize", required = false) Integer pageSize,
|
@RequestParam(value = "tagIds", required = false) List<Long> tagIds,
|
||||||
Authentication auth) {
|
@RequestParam(value = "page", required = false) Integer page,
|
||||||
List<Long> ids = categoryIds;
|
@RequestParam(value = "pageSize", required = false) Integer pageSize,
|
||||||
if (categoryId != null) {
|
Authentication auth
|
||||||
ids = java.util.List.of(categoryId);
|
) {
|
||||||
}
|
List<Long> ids = categoryService.getSearchCategoryIds(categoryIds, categoryId);
|
||||||
List<Long> tids = tagIds;
|
List<Long> tids = tagService.getSearchTagIds(tagIds, tagId);
|
||||||
if (tagId != null) {
|
// 只需要在请求的一开始统计一次
|
||||||
tids = java.util.List.of(tagId);
|
// if (auth != null) {
|
||||||
}
|
// userVisitService.recordVisit(auth.getName());
|
||||||
// 只需要在请求的一开始统计一次
|
// }
|
||||||
// if (auth != null) {
|
return postService
|
||||||
// userVisitService.recordVisit(auth.getName());
|
.listFeaturedPosts(ids, tids, page, pageSize)
|
||||||
// }
|
.stream()
|
||||||
return postService.listFeaturedPosts(ids, tids, page, pageSize)
|
.map(postMapper::toSummaryDto)
|
||||||
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.openisle.controller;
|
||||||
|
|
||||||
|
import com.openisle.dto.DonationRequest;
|
||||||
|
import com.openisle.dto.DonationResponse;
|
||||||
|
import com.openisle.service.PointService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/posts/{postId}/donations")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PostDonationController {
|
||||||
|
|
||||||
|
private final PointService pointService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@Operation(summary = "List donations", description = "Get recent donations for a post")
|
||||||
|
@ApiResponse(responseCode = "200", description = "Donation summary")
|
||||||
|
public DonationResponse list(@PathVariable Long postId) {
|
||||||
|
return pointService.getPostDonations(postId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
@Operation(summary = "Donate", description = "Donate points to the post author")
|
||||||
|
@ApiResponse(responseCode = "200", description = "Donation result")
|
||||||
|
public DonationResponse donate(
|
||||||
|
@PathVariable Long postId,
|
||||||
|
@RequestBody DonationRequest req,
|
||||||
|
Authentication auth
|
||||||
|
) {
|
||||||
|
return pointService.donateToPost(auth.getName(), postId, req.getAmount());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,39 +3,49 @@ package com.openisle.controller;
|
|||||||
import com.openisle.dto.PushPublicKeyDto;
|
import com.openisle.dto.PushPublicKeyDto;
|
||||||
import com.openisle.dto.PushSubscriptionRequest;
|
import com.openisle.dto.PushSubscriptionRequest;
|
||||||
import com.openisle.service.PushSubscriptionService;
|
import com.openisle.service.PushSubscriptionService;
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/push")
|
@RequestMapping("/api/push")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class PushSubscriptionController {
|
public class PushSubscriptionController {
|
||||||
private final PushSubscriptionService pushSubscriptionService;
|
|
||||||
@Value("${app.webpush.public-key}")
|
|
||||||
private String publicKey;
|
|
||||||
|
|
||||||
@GetMapping("/public-key")
|
private final PushSubscriptionService pushSubscriptionService;
|
||||||
@Operation(summary = "Get public key", description = "Retrieve web push public key")
|
|
||||||
@ApiResponse(responseCode = "200", description = "Public key",
|
|
||||||
content = @Content(schema = @Schema(implementation = PushPublicKeyDto.class)))
|
|
||||||
public PushPublicKeyDto getPublicKey() {
|
|
||||||
PushPublicKeyDto r = new PushPublicKeyDto();
|
|
||||||
r.setKey(publicKey);
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/subscribe")
|
@Value("${app.webpush.public-key}")
|
||||||
@Operation(summary = "Subscribe", description = "Subscribe to push notifications")
|
private String publicKey;
|
||||||
@ApiResponse(responseCode = "200", description = "Subscribed")
|
|
||||||
@SecurityRequirement(name = "JWT")
|
@GetMapping("/public-key")
|
||||||
public void subscribe(@RequestBody PushSubscriptionRequest req, Authentication auth) {
|
@Operation(summary = "Get public key", description = "Retrieve web push public key")
|
||||||
pushSubscriptionService.saveSubscription(auth.getName(), req.getEndpoint(), req.getP256dh(), req.getAuth());
|
@ApiResponse(
|
||||||
}
|
responseCode = "200",
|
||||||
|
description = "Public key",
|
||||||
|
content = @Content(schema = @Schema(implementation = PushPublicKeyDto.class))
|
||||||
|
)
|
||||||
|
public PushPublicKeyDto getPublicKey() {
|
||||||
|
PushPublicKeyDto r = new PushPublicKeyDto();
|
||||||
|
r.setKey(publicKey);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/subscribe")
|
||||||
|
@Operation(summary = "Subscribe", description = "Subscribe to push notifications")
|
||||||
|
@ApiResponse(responseCode = "200", description = "Subscribed")
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
public void subscribe(@RequestBody PushSubscriptionRequest req, Authentication auth) {
|
||||||
|
pushSubscriptionService.saveSubscription(
|
||||||
|
auth.getName(),
|
||||||
|
req.getEndpoint(),
|
||||||
|
req.getP256dh(),
|
||||||
|
req.getAuth()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,88 +8,107 @@ import com.openisle.model.ReactionType;
|
|||||||
import com.openisle.service.LevelService;
|
import com.openisle.service.LevelService;
|
||||||
import com.openisle.service.PointService;
|
import com.openisle.service.PointService;
|
||||||
import com.openisle.service.ReactionService;
|
import com.openisle.service.ReactionService;
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api")
|
@RequestMapping("/api")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ReactionController {
|
public class ReactionController {
|
||||||
private final ReactionService reactionService;
|
|
||||||
private final LevelService levelService;
|
|
||||||
private final ReactionMapper reactionMapper;
|
|
||||||
private final PointService pointService;
|
|
||||||
|
|
||||||
/**
|
private final ReactionService reactionService;
|
||||||
* Get all available reaction types.
|
private final LevelService levelService;
|
||||||
*/
|
private final ReactionMapper reactionMapper;
|
||||||
@GetMapping("/reaction-types")
|
private final PointService pointService;
|
||||||
@Operation(summary = "List reaction types", description = "Get all available reaction types")
|
|
||||||
@ApiResponse(responseCode = "200", description = "Reaction types",
|
|
||||||
content = @Content(schema = @Schema(implementation = ReactionType[].class)))
|
|
||||||
public ReactionType[] listReactionTypes() {
|
|
||||||
return ReactionType.values();
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/posts/{postId}/reactions")
|
/**
|
||||||
@Operation(summary = "React to post", description = "React to a post")
|
* Get all available reaction types.
|
||||||
@ApiResponse(responseCode = "200", description = "Reaction result",
|
*/
|
||||||
content = @Content(schema = @Schema(implementation = ReactionDto.class)))
|
@GetMapping("/reaction-types")
|
||||||
@SecurityRequirement(name = "JWT")
|
@Operation(summary = "List reaction types", description = "Get all available reaction types")
|
||||||
public ResponseEntity<ReactionDto> reactToPost(@PathVariable Long postId,
|
@ApiResponse(
|
||||||
@RequestBody ReactionRequest req,
|
responseCode = "200",
|
||||||
Authentication auth) {
|
description = "Reaction types",
|
||||||
Reaction reaction = reactionService.reactToPost(auth.getName(), postId, req.getType());
|
content = @Content(schema = @Schema(implementation = ReactionType[].class))
|
||||||
if (reaction == null) {
|
)
|
||||||
pointService.deductForReactionOfPost(auth.getName(), postId);
|
public ReactionType[] listReactionTypes() {
|
||||||
return ResponseEntity.noContent().build();
|
return ReactionType.values();
|
||||||
}
|
}
|
||||||
ReactionDto dto = reactionMapper.toDto(reaction);
|
|
||||||
dto.setReward(levelService.awardForReaction(auth.getName()));
|
|
||||||
pointService.awardForReactionOfPost(auth.getName(), postId);
|
|
||||||
return ResponseEntity.ok(dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/comments/{commentId}/reactions")
|
@PostMapping("/posts/{postId}/reactions")
|
||||||
@Operation(summary = "React to comment", description = "React to a comment")
|
@Operation(summary = "React to post", description = "React to a post")
|
||||||
@ApiResponse(responseCode = "200", description = "Reaction result",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = ReactionDto.class)))
|
responseCode = "200",
|
||||||
@SecurityRequirement(name = "JWT")
|
description = "Reaction result",
|
||||||
public ResponseEntity<ReactionDto> reactToComment(@PathVariable Long commentId,
|
content = @Content(schema = @Schema(implementation = ReactionDto.class))
|
||||||
@RequestBody ReactionRequest req,
|
)
|
||||||
Authentication auth) {
|
@SecurityRequirement(name = "JWT")
|
||||||
Reaction reaction = reactionService.reactToComment(auth.getName(), commentId, req.getType());
|
public ResponseEntity<ReactionDto> reactToPost(
|
||||||
if (reaction == null) {
|
@PathVariable Long postId,
|
||||||
pointService.deductForReactionOfComment(auth.getName(), commentId);
|
@RequestBody ReactionRequest req,
|
||||||
return ResponseEntity.noContent().build();
|
Authentication auth
|
||||||
}
|
) {
|
||||||
ReactionDto dto = reactionMapper.toDto(reaction);
|
Reaction reaction = reactionService.reactToPost(auth.getName(), postId, req.getType());
|
||||||
dto.setReward(levelService.awardForReaction(auth.getName()));
|
if (reaction == null) {
|
||||||
pointService.awardForReactionOfComment(auth.getName(), commentId);
|
pointService.deductForReactionOfPost(auth.getName(), postId);
|
||||||
return ResponseEntity.ok(dto);
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
|
ReactionDto dto = reactionMapper.toDto(reaction);
|
||||||
|
dto.setReward(levelService.awardForReaction(auth.getName()));
|
||||||
|
pointService.awardForReactionOfPost(auth.getName(), postId);
|
||||||
|
return ResponseEntity.ok(dto);
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/messages/{messageId}/reactions")
|
@PostMapping("/comments/{commentId}/reactions")
|
||||||
@Operation(summary = "React to message", description = "React to a message")
|
@Operation(summary = "React to comment", description = "React to a comment")
|
||||||
@ApiResponse(responseCode = "200", description = "Reaction result",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = ReactionDto.class)))
|
responseCode = "200",
|
||||||
@SecurityRequirement(name = "JWT")
|
description = "Reaction result",
|
||||||
public ResponseEntity<ReactionDto> reactToMessage(@PathVariable Long messageId,
|
content = @Content(schema = @Schema(implementation = ReactionDto.class))
|
||||||
@RequestBody ReactionRequest req,
|
)
|
||||||
Authentication auth) {
|
@SecurityRequirement(name = "JWT")
|
||||||
Reaction reaction = reactionService.reactToMessage(auth.getName(), messageId, req.getType());
|
public ResponseEntity<ReactionDto> reactToComment(
|
||||||
if (reaction == null) {
|
@PathVariable Long commentId,
|
||||||
return ResponseEntity.noContent().build();
|
@RequestBody ReactionRequest req,
|
||||||
}
|
Authentication auth
|
||||||
ReactionDto dto = reactionMapper.toDto(reaction);
|
) {
|
||||||
dto.setReward(levelService.awardForReaction(auth.getName()));
|
Reaction reaction = reactionService.reactToComment(auth.getName(), commentId, req.getType());
|
||||||
return ResponseEntity.ok(dto);
|
if (reaction == null) {
|
||||||
|
pointService.deductForReactionOfComment(auth.getName(), commentId);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
|
ReactionDto dto = reactionMapper.toDto(reaction);
|
||||||
|
dto.setReward(levelService.awardForReaction(auth.getName()));
|
||||||
|
pointService.awardForReactionOfComment(auth.getName(), commentId);
|
||||||
|
return ResponseEntity.ok(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/messages/{messageId}/reactions")
|
||||||
|
@Operation(summary = "React to message", description = "React to a message")
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Reaction result",
|
||||||
|
content = @Content(schema = @Schema(implementation = ReactionDto.class))
|
||||||
|
)
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,28 @@
|
|||||||
package com.openisle.controller;
|
package com.openisle.controller;
|
||||||
|
|
||||||
import com.openisle.model.Post;
|
|
||||||
import com.openisle.model.Comment;
|
import com.openisle.model.Comment;
|
||||||
import com.openisle.model.CommentSort;
|
import com.openisle.model.CommentSort;
|
||||||
import com.openisle.service.PostService;
|
import com.openisle.model.Post;
|
||||||
import com.openisle.service.CommentService;
|
import com.openisle.service.CommentService;
|
||||||
|
import com.openisle.service.PostService;
|
||||||
|
import com.vladsch.flexmark.ext.autolink.AutolinkExtension;
|
||||||
|
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension;
|
||||||
|
import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension;
|
||||||
|
import com.vladsch.flexmark.ext.tables.TablesExtension;
|
||||||
|
import com.vladsch.flexmark.html.HtmlRenderer;
|
||||||
|
import com.vladsch.flexmark.parser.Parser;
|
||||||
|
import com.vladsch.flexmark.util.data.MutableDataSet;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
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;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.jsoup.Jsoup;
|
import org.jsoup.Jsoup;
|
||||||
import org.jsoup.nodes.Document;
|
import org.jsoup.nodes.Document;
|
||||||
@@ -13,346 +31,376 @@ import org.jsoup.safety.Safelist;
|
|||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
|
||||||
|
|
||||||
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
|
@RestController
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class RssController {
|
public class RssController {
|
||||||
private final PostService postService;
|
|
||||||
private final CommentService commentService;
|
|
||||||
|
|
||||||
@Value("${app.website-url:https://www.open-isle.com}")
|
private final PostService postService;
|
||||||
private String websiteUrl;
|
private final CommentService commentService;
|
||||||
|
|
||||||
// 兼容 Markdown/HTML 两类图片写法(用于 enclosure)
|
@Value("${app.website-url:https://www.open-isle.com}")
|
||||||
private static final Pattern MD_IMAGE = Pattern.compile("!\\[[^\\]]*\\]\\(([^)]+)\\)");
|
private String websiteUrl;
|
||||||
private static final Pattern HTML_IMAGE = Pattern.compile("<BaseImage[^>]+src=[\"']?([^\"'>]+)[\"']?[^>]*>");
|
|
||||||
|
|
||||||
private static final DateTimeFormatter RFC1123 = DateTimeFormatter.RFC_1123_DATE_TIME;
|
// 兼容 Markdown/HTML 两类图片写法(用于 enclosure)
|
||||||
|
private static final Pattern MD_IMAGE = Pattern.compile("!\\[[^\\]]*\\]\\(([^)]+)\\)");
|
||||||
|
private static final Pattern HTML_IMAGE = Pattern.compile(
|
||||||
|
"<BaseImage[^>]+src=[\"']?([^\"'>]+)[\"']?[^>]*>"
|
||||||
|
);
|
||||||
|
|
||||||
// flexmark:Markdown -> HTML
|
private static final DateTimeFormatter RFC1123 = DateTimeFormatter.RFC_1123_DATE_TIME;
|
||||||
private static final Parser MD_PARSER;
|
|
||||||
private static final HtmlRenderer MD_RENDERER;
|
// flexmark:Markdown -> HTML
|
||||||
static {
|
private static final Parser MD_PARSER;
|
||||||
MutableDataSet opts = new MutableDataSet();
|
private static final HtmlRenderer MD_RENDERER;
|
||||||
opts.set(Parser.EXTENSIONS, Arrays.asList(
|
|
||||||
TablesExtension.create(),
|
static {
|
||||||
AutolinkExtension.create(),
|
MutableDataSet opts = new MutableDataSet();
|
||||||
StrikethroughExtension.create(),
|
opts.set(
|
||||||
TaskListExtension.create()
|
Parser.EXTENSIONS,
|
||||||
));
|
Arrays.asList(
|
||||||
// 允许内联 HTML(下游再做 sanitize)
|
TablesExtension.create(),
|
||||||
opts.set(Parser.HTML_BLOCK_PARSER, true);
|
AutolinkExtension.create(),
|
||||||
MD_PARSER = Parser.builder(opts).build();
|
StrikethroughExtension.create(),
|
||||||
MD_RENDERER = HtmlRenderer.builder(opts).escapeHtml(false).build();
|
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")
|
||||||
|
@Operation(summary = "RSS feed", description = "Generate RSS feed for latest posts")
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "RSS XML",
|
||||||
|
content = @Content(schema = @Schema(implementation = String.class))
|
||||||
|
)
|
||||||
|
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>");
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(value = "/api/rss", produces = "application/rss+xml;charset=UTF-8")
|
sb.append("</channel></rss>");
|
||||||
@Operation(summary = "RSS feed", description = "Generate RSS feed for latest posts")
|
return sb.toString();
|
||||||
@ApiResponse(responseCode = "200", description = "RSS XML", content = @Content(schema = @Schema(implementation = String.class)))
|
}
|
||||||
public String feed() {
|
|
||||||
// 建议 20;你现在是 10,这里保留你的 10
|
|
||||||
List<Post> posts = postService.listLatestRssPosts(10);
|
|
||||||
String base = trimTrailingSlash(websiteUrl);
|
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder(4096);
|
/* ===================== Markdown → HTML ===================== */
|
||||||
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) {
|
private static String renderMarkdown(String md) {
|
||||||
String link = base + "/posts/" + p.getId();
|
if (md == null || md.isEmpty()) return "";
|
||||||
|
return MD_RENDERER.render(MD_PARSER.parse(md));
|
||||||
|
}
|
||||||
|
|
||||||
// 1) Markdown -> HTML
|
/* ===================== Sanitize & 绝对化 ===================== */
|
||||||
String html = renderMarkdown(p.getContent());
|
|
||||||
|
|
||||||
// 2) Sanitize(白名单增强)
|
private static String sanitizeHtml(String html) {
|
||||||
String safeHtml = sanitizeHtml(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);
|
||||||
|
}
|
||||||
|
|
||||||
// 3) 绝对化 href/src + 强制 rel/target
|
private static String absolutifyHtml(String html, String baseUrl) {
|
||||||
String absHtml = absolutifyHtml(safeHtml, base);
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
// 4) 纯文本摘要(用于 <description>)
|
private static String absolutifyUrl(String url, String baseUrl) {
|
||||||
String plain = textSummary(absHtml, 180);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 5) enclosure(首图,已绝对化)
|
private static String absolutifySrcset(String srcset, String baseUrl) {
|
||||||
String enclosure = firstImage(p.getContent());
|
if (srcset == null || srcset.isEmpty()) return srcset;
|
||||||
if (enclosure == null) {
|
String[] parts = srcset.split(",");
|
||||||
// 如果 Markdown 没有图,尝试从渲染后的 HTML 再抓一次
|
List<String> out = new ArrayList<>(parts.length);
|
||||||
enclosure = firstImage(absHtml);
|
for (String part : parts) {
|
||||||
}
|
String p = part.trim();
|
||||||
if (enclosure != null) {
|
if (p.isEmpty()) continue;
|
||||||
enclosure = absolutifyUrl(enclosure, base);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
// 6) 构造优雅的附加区块(原文链接 + 精选评论),编入 <content:encoded>
|
/* ===================== 摘要 & enclosure ===================== */
|
||||||
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>");
|
private static String textSummary(String html, int maxLen) {
|
||||||
elem(sb, "title", cdata(nullSafe(p.getTitle())));
|
if (html == null) return "";
|
||||||
elem(sb, "link", link);
|
String text = Jsoup.parse(html).text().replaceAll("\\s+", " ").trim();
|
||||||
sb.append("<guid isPermaLink=\"true\">").append(link).append("</guid>");
|
if (text.length() <= maxLen) return text;
|
||||||
elem(sb, "pubDate", toRfc1123Gmt(p.getCreatedAt().atZone(ZoneId.systemDefault())));
|
return text.substring(0, maxLen) + "…";
|
||||||
// 摘要
|
}
|
||||||
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>");
|
private String firstImage(String content) {
|
||||||
return sb.toString();
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===================== Markdown → HTML ===================== */
|
// 渲染为 HTML,并保持和正文一致的处理流程
|
||||||
|
String html = renderMarkdown(md.toString());
|
||||||
|
String safe = sanitizeHtml(html);
|
||||||
|
return absolutifyHtml(safe, baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
private static String renderMarkdown(String md) {
|
private static String usernameOf(Comment c) {
|
||||||
if (md == null || md.isEmpty()) return "";
|
if (c == null) return "匿名";
|
||||||
return MD_RENDERER.render(MD_PARSER.parse(md));
|
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 "匿名";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ===================== Sanitize & 绝对化 ===================== */
|
/* ===================== 时间/字符串/XML ===================== */
|
||||||
|
|
||||||
private static String sanitizeHtml(String html) {
|
private static String toRfc1123Gmt(ZonedDateTime zdt) {
|
||||||
if (html == null) return "";
|
return zdt.withZoneSameInstant(ZoneId.of("GMT")).format(RFC1123);
|
||||||
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) {
|
private static String cdata(String s) {
|
||||||
if (html == null || html.isEmpty()) return "";
|
if (s == null) return "<![CDATA[]]>";
|
||||||
Document doc = Jsoup.parseBodyFragment(html, baseUrl);
|
// 防止出现 "]]>" 终止标记破坏 CDATA
|
||||||
// a[href]
|
return "<![CDATA[" + s.replace("]]>", "]]]]><![CDATA[>") + "]]>";
|
||||||
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) {
|
private static void elem(StringBuilder sb, String name, String value) {
|
||||||
if (url == null || url.isEmpty()) return url;
|
sb.append('<').append(name).append('>').append(value).append("</").append(name).append('>');
|
||||||
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) {
|
private static String escapeXml(String s) {
|
||||||
if (srcset == null || srcset.isEmpty()) return srcset;
|
if (s == null) return "";
|
||||||
String[] parts = srcset.split(",");
|
return s
|
||||||
List<String> out = new ArrayList<>(parts.length);
|
.replace("&", "&")
|
||||||
for (String part : parts) {
|
.replace("<", "<")
|
||||||
String p = part.trim();
|
.replace(">", ">")
|
||||||
if (p.isEmpty()) continue;
|
.replace("\"", """)
|
||||||
String[] seg = p.split("\\s+");
|
.replace("'", "'");
|
||||||
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 trimTrailingSlash(String s) {
|
||||||
|
if (s == null) return "";
|
||||||
|
return s.endsWith("/") ? s.substring(0, s.length() - 1) : s;
|
||||||
|
}
|
||||||
|
|
||||||
private static String textSummary(String html, int maxLen) {
|
private static String ensureTrailingSlash(String s) {
|
||||||
if (html == null) return "";
|
if (s == null || s.isEmpty()) return "/";
|
||||||
String text = Jsoup.parse(html).text().replaceAll("\\s+", " ").trim();
|
return s.endsWith("/") ? s : s + "/";
|
||||||
if (text.length() <= maxLen) return text;
|
}
|
||||||
return text.substring(0, maxLen) + "…";
|
|
||||||
}
|
|
||||||
|
|
||||||
private String firstImage(String content) {
|
private static String nullSafe(String s) {
|
||||||
if (content == null) return null;
|
return s == null ? "" : s;
|
||||||
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; }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,84 +6,120 @@ import com.openisle.dto.UserDto;
|
|||||||
import com.openisle.mapper.PostMapper;
|
import com.openisle.mapper.PostMapper;
|
||||||
import com.openisle.mapper.UserMapper;
|
import com.openisle.mapper.UserMapper;
|
||||||
import com.openisle.service.SearchService;
|
import com.openisle.service.SearchService;
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
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 io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
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;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/search")
|
@RequestMapping("/api/search")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class SearchController {
|
public class SearchController {
|
||||||
private final SearchService searchService;
|
|
||||||
private final UserMapper userMapper;
|
|
||||||
private final PostMapper postMapper;
|
|
||||||
|
|
||||||
@GetMapping("/users")
|
private final SearchService searchService;
|
||||||
@Operation(summary = "Search users", description = "Search users by keyword")
|
private final UserMapper userMapper;
|
||||||
@ApiResponse(responseCode = "200", description = "List of users",
|
private final PostMapper postMapper;
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = UserDto.class))))
|
|
||||||
public List<UserDto> searchUsers(@RequestParam String keyword) {
|
|
||||||
return searchService.searchUsers(keyword).stream()
|
|
||||||
.map(userMapper::toDto)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/posts")
|
@GetMapping("/users")
|
||||||
@Operation(summary = "Search posts", description = "Search posts by keyword")
|
@Operation(summary = "Search users", description = "Search users by keyword")
|
||||||
@ApiResponse(responseCode = "200", description = "List of posts",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
|
responseCode = "200",
|
||||||
public List<PostSummaryDto> searchPosts(@RequestParam String keyword) {
|
description = "List of users",
|
||||||
return searchService.searchPosts(keyword).stream()
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = UserDto.class)))
|
||||||
.map(postMapper::toSummaryDto)
|
)
|
||||||
.collect(Collectors.toList());
|
public List<UserDto> searchUsers(@RequestParam String keyword) {
|
||||||
}
|
return searchService
|
||||||
|
.searchUsers(keyword)
|
||||||
|
.stream()
|
||||||
|
.map(userMapper::toDto)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/posts/content")
|
@GetMapping("/posts")
|
||||||
@Operation(summary = "Search posts by content", description = "Search posts by content keyword")
|
@Operation(summary = "Search posts", description = "Search posts by keyword")
|
||||||
@ApiResponse(responseCode = "200", description = "List of posts",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
|
responseCode = "200",
|
||||||
public List<PostSummaryDto> searchPostsByContent(@RequestParam String keyword) {
|
description = "List of posts",
|
||||||
return searchService.searchPostsByContent(keyword).stream()
|
content = @Content(
|
||||||
.map(postMapper::toSummaryDto)
|
array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))
|
||||||
.collect(Collectors.toList());
|
)
|
||||||
}
|
)
|
||||||
|
public List<PostSummaryDto> searchPosts(@RequestParam String keyword) {
|
||||||
|
return searchService
|
||||||
|
.searchPosts(keyword)
|
||||||
|
.stream()
|
||||||
|
.map(postMapper::toSummaryDto)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/posts/title")
|
@GetMapping("/posts/content")
|
||||||
@Operation(summary = "Search posts by title", description = "Search posts by title keyword")
|
@Operation(summary = "Search posts by content", description = "Search posts by content keyword")
|
||||||
@ApiResponse(responseCode = "200", description = "List of posts",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
|
responseCode = "200",
|
||||||
public List<PostSummaryDto> searchPostsByTitle(@RequestParam String keyword) {
|
description = "List of posts",
|
||||||
return searchService.searchPostsByTitle(keyword).stream()
|
content = @Content(
|
||||||
.map(postMapper::toSummaryDto)
|
array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))
|
||||||
.collect(Collectors.toList());
|
)
|
||||||
}
|
)
|
||||||
|
public List<PostSummaryDto> searchPostsByContent(@RequestParam String keyword) {
|
||||||
|
return searchService
|
||||||
|
.searchPostsByContent(keyword)
|
||||||
|
.stream()
|
||||||
|
.map(postMapper::toSummaryDto)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/global")
|
@GetMapping("/posts/title")
|
||||||
@Operation(summary = "Global search", description = "Search users and posts globally")
|
@Operation(summary = "Search posts by title", description = "Search posts by title keyword")
|
||||||
@ApiResponse(responseCode = "200", description = "Search results",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = SearchResultDto.class))))
|
responseCode = "200",
|
||||||
public List<SearchResultDto> global(@RequestParam String keyword) {
|
description = "List of posts",
|
||||||
return searchService.globalSearch(keyword).stream()
|
content = @Content(
|
||||||
.map(r -> {
|
array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))
|
||||||
SearchResultDto dto = new SearchResultDto();
|
)
|
||||||
dto.setType(r.type());
|
)
|
||||||
dto.setId(r.id());
|
public List<PostSummaryDto> searchPostsByTitle(@RequestParam String keyword) {
|
||||||
dto.setText(r.text());
|
return searchService
|
||||||
dto.setSubText(r.subText());
|
.searchPostsByTitle(keyword)
|
||||||
dto.setExtra(r.extra());
|
.stream()
|
||||||
dto.setPostId(r.postId());
|
.map(postMapper::toSummaryDto)
|
||||||
return dto;
|
.collect(Collectors.toList());
|
||||||
})
|
}
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
@GetMapping("/global")
|
||||||
|
@Operation(summary = "Global search", description = "Search users and posts globally")
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Search results",
|
||||||
|
content = @Content(
|
||||||
|
array = @ArraySchema(schema = @Schema(implementation = SearchResultDto.class))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
public List<SearchResultDto> global(@RequestParam String keyword) {
|
||||||
|
return searchService
|
||||||
|
.globalSearch(keyword)
|
||||||
|
.stream()
|
||||||
|
.map(r -> {
|
||||||
|
SearchResultDto dto = new SearchResultDto();
|
||||||
|
dto.setType(r.type());
|
||||||
|
dto.setId(r.id());
|
||||||
|
dto.setText(r.text());
|
||||||
|
dto.setSubText(r.subText());
|
||||||
|
dto.setExtra(r.extra());
|
||||||
|
dto.setPostId(r.postId());
|
||||||
|
dto.setHighlightedText(r.highlightedText());
|
||||||
|
dto.setHighlightedSubText(r.highlightedSubText());
|
||||||
|
dto.setHighlightedExtra(r.highlightedExtra());
|
||||||
|
return dto;
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ package com.openisle.controller;
|
|||||||
import com.openisle.model.Post;
|
import com.openisle.model.Post;
|
||||||
import com.openisle.model.PostStatus;
|
import com.openisle.model.PostStatus;
|
||||||
import com.openisle.repository.PostRepository;
|
import com.openisle.repository.PostRepository;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import java.util.List;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
@@ -10,12 +15,6 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controller for dynamic sitemap generation.
|
* Controller for dynamic sitemap generation.
|
||||||
@@ -24,53 +23,47 @@ import java.util.List;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@RequestMapping("/api")
|
@RequestMapping("/api")
|
||||||
public class SitemapController {
|
public class SitemapController {
|
||||||
private final PostRepository postRepository;
|
|
||||||
|
|
||||||
@Value("${app.website-url}")
|
private final PostRepository postRepository;
|
||||||
private String websiteUrl;
|
|
||||||
|
|
||||||
@GetMapping(value = "/sitemap.xml", produces = MediaType.APPLICATION_XML_VALUE)
|
@Value("${app.website-url}")
|
||||||
@Operation(summary = "Sitemap", description = "Generate sitemap xml")
|
private String websiteUrl;
|
||||||
@ApiResponse(responseCode = "200", description = "Sitemap xml",
|
|
||||||
content = @Content(schema = @Schema(implementation = String.class)))
|
|
||||||
public ResponseEntity<String> sitemap() {
|
|
||||||
List<Post> posts = postRepository.findByStatus(PostStatus.PUBLISHED);
|
|
||||||
|
|
||||||
StringBuilder body = new StringBuilder();
|
@GetMapping(value = "/sitemap.xml", produces = MediaType.APPLICATION_XML_VALUE)
|
||||||
body.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
|
@Operation(summary = "Sitemap", description = "Generate sitemap xml")
|
||||||
body.append("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n");
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Sitemap xml",
|
||||||
|
content = @Content(schema = @Schema(implementation = String.class))
|
||||||
|
)
|
||||||
|
public ResponseEntity<String> sitemap() {
|
||||||
|
List<Post> posts = postRepository.findByStatus(PostStatus.PUBLISHED);
|
||||||
|
|
||||||
List<String> staticRoutes = List.of(
|
StringBuilder body = new StringBuilder();
|
||||||
"/",
|
body.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
|
||||||
"/about",
|
body.append("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n");
|
||||||
"/activities",
|
|
||||||
"/login",
|
|
||||||
"/signup"
|
|
||||||
);
|
|
||||||
|
|
||||||
for (String path : staticRoutes) {
|
List<String> staticRoutes = List.of("/", "/about", "/activities", "/login", "/signup");
|
||||||
body.append(" <url><loc>")
|
|
||||||
.append(websiteUrl)
|
|
||||||
.append(path)
|
|
||||||
.append("</loc></url>\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
for (Post p : posts) {
|
for (String path : staticRoutes) {
|
||||||
body.append(" <url>\n")
|
body.append(" <url><loc>").append(websiteUrl).append(path).append("</loc></url>\n");
|
||||||
.append(" <loc>")
|
|
||||||
.append(websiteUrl)
|
|
||||||
.append("/posts/")
|
|
||||||
.append(p.getId())
|
|
||||||
.append("</loc>\n")
|
|
||||||
.append(" <lastmod>")
|
|
||||||
.append(p.getCreatedAt().toLocalDate())
|
|
||||||
.append("</lastmod>\n")
|
|
||||||
.append(" </url>\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
body.append("</urlset>");
|
|
||||||
return ResponseEntity.ok()
|
|
||||||
.contentType(MediaType.APPLICATION_XML)
|
|
||||||
.body(body.toString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (Post p : posts) {
|
||||||
|
body
|
||||||
|
.append(" <url>\n")
|
||||||
|
.append(" <loc>")
|
||||||
|
.append(websiteUrl)
|
||||||
|
.append("/posts/")
|
||||||
|
.append(p.getId())
|
||||||
|
.append("</loc>\n")
|
||||||
|
.append(" <lastmod>")
|
||||||
|
.append(p.getCreatedAt().toLocalDate())
|
||||||
|
.append("</lastmod>\n")
|
||||||
|
.append(" </url>\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
body.append("</urlset>");
|
||||||
|
return ResponseEntity.ok().contentType(MediaType.APPLICATION_XML).body(body.toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,105 +1,127 @@
|
|||||||
package com.openisle.controller;
|
package com.openisle.controller;
|
||||||
|
|
||||||
import com.openisle.service.UserVisitService;
|
|
||||||
import com.openisle.service.StatService;
|
import com.openisle.service.StatService;
|
||||||
|
import com.openisle.service.UserVisitService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.format.annotation.DateTimeFormat;
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/stats")
|
@RequestMapping("/api/stats")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class StatController {
|
public class StatController {
|
||||||
private final UserVisitService userVisitService;
|
|
||||||
private final StatService statService;
|
|
||||||
|
|
||||||
@GetMapping("/dau")
|
private final UserVisitService userVisitService;
|
||||||
@Operation(summary = "Daily active users", description = "Get daily active user count")
|
private final StatService statService;
|
||||||
@ApiResponse(responseCode = "200", description = "DAU count",
|
|
||||||
content = @Content(schema = @Schema(implementation = java.util.Map.class)))
|
|
||||||
public Map<String, Long> dau(@RequestParam(value = "date", required = false)
|
|
||||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) {
|
|
||||||
long count = userVisitService.countDau(date);
|
|
||||||
return Map.of("dau", count);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/dau-range")
|
@GetMapping("/dau")
|
||||||
@Operation(summary = "DAU range", description = "Get daily active users over range of days")
|
@Operation(summary = "Daily active users", description = "Get daily active user count")
|
||||||
@ApiResponse(responseCode = "200", description = "DAU data",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = java.util.Map.class))))
|
responseCode = "200",
|
||||||
public List<Map<String, Object>> dauRange(@RequestParam(value = "days", defaultValue = "30") int days) {
|
description = "DAU count",
|
||||||
if (days < 1) days = 1;
|
content = @Content(schema = @Schema(implementation = java.util.Map.class))
|
||||||
LocalDate end = LocalDate.now();
|
)
|
||||||
LocalDate start = end.minusDays(days - 1L);
|
public Map<String, Long> dau(
|
||||||
var data = userVisitService.countDauRange(start, end);
|
@RequestParam(value = "date", required = false) @DateTimeFormat(
|
||||||
return data.entrySet().stream()
|
iso = DateTimeFormat.ISO.DATE
|
||||||
.map(e -> Map.<String,Object>of(
|
) LocalDate date
|
||||||
"date", e.getKey().toString(),
|
) {
|
||||||
"value", e.getValue()
|
long count = userVisitService.countDau(date);
|
||||||
))
|
return Map.of("dau", count);
|
||||||
.toList();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/new-users-range")
|
@GetMapping("/dau-range")
|
||||||
@Operation(summary = "New users range", description = "Get new users over range of days")
|
@Operation(summary = "DAU range", description = "Get daily active users over range of days")
|
||||||
@ApiResponse(responseCode = "200", description = "New user data",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = java.util.Map.class))))
|
responseCode = "200",
|
||||||
public List<Map<String, Object>> newUsersRange(@RequestParam(value = "days", defaultValue = "30") int days) {
|
description = "DAU data",
|
||||||
if (days < 1) days = 1;
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = java.util.Map.class)))
|
||||||
LocalDate end = LocalDate.now();
|
)
|
||||||
LocalDate start = end.minusDays(days - 1L);
|
public List<Map<String, Object>> dauRange(
|
||||||
var data = statService.countNewUsersRange(start, end);
|
@RequestParam(value = "days", defaultValue = "30") int days
|
||||||
return data.entrySet().stream()
|
) {
|
||||||
.map(e -> Map.<String,Object>of(
|
if (days < 1) days = 1;
|
||||||
"date", e.getKey().toString(),
|
LocalDate end = LocalDate.now();
|
||||||
"value", e.getValue()
|
LocalDate start = end.minusDays(days - 1L);
|
||||||
))
|
var data = userVisitService.countDauRange(start, end);
|
||||||
.toList();
|
return data
|
||||||
}
|
.entrySet()
|
||||||
|
.stream()
|
||||||
|
.map(e -> Map.<String, Object>of("date", e.getKey().toString(), "value", e.getValue()))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/posts-range")
|
@GetMapping("/new-users-range")
|
||||||
@Operation(summary = "Posts range", description = "Get posts count over range of days")
|
@Operation(summary = "New users range", description = "Get new users over range of days")
|
||||||
@ApiResponse(responseCode = "200", description = "Post data",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = java.util.Map.class))))
|
responseCode = "200",
|
||||||
public List<Map<String, Object>> postsRange(@RequestParam(value = "days", defaultValue = "30") int days) {
|
description = "New user data",
|
||||||
if (days < 1) days = 1;
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = java.util.Map.class)))
|
||||||
LocalDate end = LocalDate.now();
|
)
|
||||||
LocalDate start = end.minusDays(days - 1L);
|
public List<Map<String, Object>> newUsersRange(
|
||||||
var data = statService.countPostsRange(start, end);
|
@RequestParam(value = "days", defaultValue = "30") int days
|
||||||
return data.entrySet().stream()
|
) {
|
||||||
.map(e -> Map.<String,Object>of(
|
if (days < 1) days = 1;
|
||||||
"date", e.getKey().toString(),
|
LocalDate end = LocalDate.now();
|
||||||
"value", e.getValue()
|
LocalDate start = end.minusDays(days - 1L);
|
||||||
))
|
var data = statService.countNewUsersRange(start, end);
|
||||||
.toList();
|
return data
|
||||||
}
|
.entrySet()
|
||||||
|
.stream()
|
||||||
|
.map(e -> Map.<String, Object>of("date", e.getKey().toString(), "value", e.getValue()))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/comments-range")
|
@GetMapping("/posts-range")
|
||||||
@Operation(summary = "Comments range", description = "Get comments count over range of days")
|
@Operation(summary = "Posts range", description = "Get posts count over range of days")
|
||||||
@ApiResponse(responseCode = "200", description = "Comment data",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = java.util.Map.class))))
|
responseCode = "200",
|
||||||
public List<Map<String, Object>> commentsRange(@RequestParam(value = "days", defaultValue = "30") int days) {
|
description = "Post data",
|
||||||
if (days < 1) days = 1;
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = java.util.Map.class)))
|
||||||
LocalDate end = LocalDate.now();
|
)
|
||||||
LocalDate start = end.minusDays(days - 1L);
|
public List<Map<String, Object>> postsRange(
|
||||||
var data = statService.countCommentsRange(start, end);
|
@RequestParam(value = "days", defaultValue = "30") int days
|
||||||
return data.entrySet().stream()
|
) {
|
||||||
.map(e -> Map.<String,Object>of(
|
if (days < 1) days = 1;
|
||||||
"date", e.getKey().toString(),
|
LocalDate end = LocalDate.now();
|
||||||
"value", e.getValue()
|
LocalDate start = end.minusDays(days - 1L);
|
||||||
))
|
var data = statService.countPostsRange(start, end);
|
||||||
.toList();
|
return data
|
||||||
}
|
.entrySet()
|
||||||
|
.stream()
|
||||||
|
.map(e -> Map.<String, Object>of("date", e.getKey().toString(), "value", e.getValue()))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/comments-range")
|
||||||
|
@Operation(summary = "Comments range", description = "Get comments count over range of days")
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Comment data",
|
||||||
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = java.util.Map.class)))
|
||||||
|
)
|
||||||
|
public List<Map<String, Object>> commentsRange(
|
||||||
|
@RequestParam(value = "days", defaultValue = "30") int days
|
||||||
|
) {
|
||||||
|
if (days < 1) days = 1;
|
||||||
|
LocalDate end = LocalDate.now();
|
||||||
|
LocalDate start = end.minusDays(days - 1L);
|
||||||
|
var data = statService.countCommentsRange(start, end);
|
||||||
|
return data
|
||||||
|
.entrySet()
|
||||||
|
.stream()
|
||||||
|
.map(e -> Map.<String, Object>of("date", e.getKey().toString(), "value", e.getValue()))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +1,66 @@
|
|||||||
package com.openisle.controller;
|
package com.openisle.controller;
|
||||||
|
|
||||||
import com.openisle.service.SubscriptionService;
|
import com.openisle.service.SubscriptionService;
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
/** Endpoints for subscribing to posts, comments and users. */
|
/** Endpoints for subscribing to posts, comments and users. */
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/subscriptions")
|
@RequestMapping("/api/subscriptions")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class SubscriptionController {
|
public class SubscriptionController {
|
||||||
private final SubscriptionService subscriptionService;
|
|
||||||
|
|
||||||
@PostMapping("/posts/{postId}")
|
private final SubscriptionService subscriptionService;
|
||||||
@Operation(summary = "Subscribe post", description = "Subscribe to a post")
|
|
||||||
@ApiResponse(responseCode = "200", description = "Subscribed")
|
|
||||||
@SecurityRequirement(name = "JWT")
|
|
||||||
public void subscribePost(@PathVariable Long postId, Authentication auth) {
|
|
||||||
subscriptionService.subscribePost(auth.getName(), postId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@DeleteMapping("/posts/{postId}")
|
@PostMapping("/posts/{postId}")
|
||||||
@Operation(summary = "Unsubscribe post", description = "Unsubscribe from a post")
|
@Operation(summary = "Subscribe post", description = "Subscribe to a post")
|
||||||
@ApiResponse(responseCode = "200", description = "Unsubscribed")
|
@ApiResponse(responseCode = "200", description = "Subscribed")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public void unsubscribePost(@PathVariable Long postId, Authentication auth) {
|
public void subscribePost(@PathVariable Long postId, Authentication auth) {
|
||||||
subscriptionService.unsubscribePost(auth.getName(), postId);
|
subscriptionService.subscribePost(auth.getName(), postId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/comments/{commentId}")
|
@DeleteMapping("/posts/{postId}")
|
||||||
@Operation(summary = "Subscribe comment", description = "Subscribe to a comment")
|
@Operation(summary = "Unsubscribe post", description = "Unsubscribe from a post")
|
||||||
@ApiResponse(responseCode = "200", description = "Subscribed")
|
@ApiResponse(responseCode = "200", description = "Unsubscribed")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public void subscribeComment(@PathVariable Long commentId, Authentication auth) {
|
public void unsubscribePost(@PathVariable Long postId, Authentication auth) {
|
||||||
subscriptionService.subscribeComment(auth.getName(), commentId);
|
subscriptionService.unsubscribePost(auth.getName(), postId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/comments/{commentId}")
|
@PostMapping("/comments/{commentId}")
|
||||||
@Operation(summary = "Unsubscribe comment", description = "Unsubscribe from a comment")
|
@Operation(summary = "Subscribe comment", description = "Subscribe to a comment")
|
||||||
@ApiResponse(responseCode = "200", description = "Unsubscribed")
|
@ApiResponse(responseCode = "200", description = "Subscribed")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public void unsubscribeComment(@PathVariable Long commentId, Authentication auth) {
|
public void subscribeComment(@PathVariable Long commentId, Authentication auth) {
|
||||||
subscriptionService.unsubscribeComment(auth.getName(), commentId);
|
subscriptionService.subscribeComment(auth.getName(), commentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/users/{username}")
|
@DeleteMapping("/comments/{commentId}")
|
||||||
@Operation(summary = "Subscribe user", description = "Subscribe to a user")
|
@Operation(summary = "Unsubscribe comment", description = "Unsubscribe from a comment")
|
||||||
@ApiResponse(responseCode = "200", description = "Subscribed")
|
@ApiResponse(responseCode = "200", description = "Unsubscribed")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public void subscribeUser(@PathVariable String username, Authentication auth) {
|
public void unsubscribeComment(@PathVariable Long commentId, Authentication auth) {
|
||||||
subscriptionService.subscribeUser(auth.getName(), username);
|
subscriptionService.unsubscribeComment(auth.getName(), commentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/users/{username}")
|
@PostMapping("/users/{username}")
|
||||||
@Operation(summary = "Unsubscribe user", description = "Unsubscribe from a user")
|
@Operation(summary = "Subscribe user", description = "Subscribe to a user")
|
||||||
@ApiResponse(responseCode = "200", description = "Unsubscribed")
|
@ApiResponse(responseCode = "200", description = "Subscribed")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public void unsubscribeUser(@PathVariable String username, Authentication auth) {
|
public void subscribeUser(@PathVariable String username, Authentication auth) {
|
||||||
subscriptionService.unsubscribeUser(auth.getName(), username);
|
subscriptionService.subscribeUser(auth.getName(), username);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/users/{username}")
|
||||||
|
@Operation(summary = "Unsubscribe user", description = "Unsubscribe from a user")
|
||||||
|
@ApiResponse(responseCode = "200", description = "Unsubscribed")
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
public void unsubscribeUser(@PathVariable String username, Authentication auth) {
|
||||||
|
subscriptionService.unsubscribeUser(auth.getName(), username);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,109 +11,156 @@ import com.openisle.model.Tag;
|
|||||||
import com.openisle.repository.UserRepository;
|
import com.openisle.repository.UserRepository;
|
||||||
import com.openisle.service.PostService;
|
import com.openisle.service.PostService;
|
||||||
import com.openisle.service.TagService;
|
import com.openisle.service.TagService;
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/tags")
|
@RequestMapping("/api/tags")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class TagController {
|
public class TagController {
|
||||||
private final TagService tagService;
|
|
||||||
private final PostService postService;
|
|
||||||
private final UserRepository userRepository;
|
|
||||||
private final PostMapper postMapper;
|
|
||||||
private final TagMapper tagMapper;
|
|
||||||
|
|
||||||
@PostMapping
|
private final TagService tagService;
|
||||||
@Operation(summary = "Create tag", description = "Create a new tag")
|
private final PostService postService;
|
||||||
@ApiResponse(responseCode = "200", description = "Created tag",
|
private final UserRepository userRepository;
|
||||||
content = @Content(schema = @Schema(implementation = TagDto.class)))
|
private final PostMapper postMapper;
|
||||||
@SecurityRequirement(name = "JWT")
|
private final TagMapper tagMapper;
|
||||||
public TagDto create(@RequestBody TagRequest req, org.springframework.security.core.Authentication auth) {
|
|
||||||
boolean approved = true;
|
|
||||||
if (postService.getPublishMode() == PublishMode.REVIEW && auth != null) {
|
|
||||||
com.openisle.model.User user = userRepository.findByUsername(auth.getName()).orElseThrow();
|
|
||||||
if (user.getRole() != Role.ADMIN) {
|
|
||||||
approved = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Tag tag = tagService.createTag(
|
|
||||||
req.getName(),
|
|
||||||
req.getDescription(),
|
|
||||||
req.getIcon(),
|
|
||||||
req.getSmallIcon(),
|
|
||||||
approved,
|
|
||||||
auth != null ? auth.getName() : null);
|
|
||||||
long count = postService.countPostsByTag(tag.getId());
|
|
||||||
return tagMapper.toDto(tag, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PutMapping("/{id}")
|
@PostMapping
|
||||||
@Operation(summary = "Update tag", description = "Update an existing tag")
|
@Operation(summary = "Create tag", description = "Create a new tag")
|
||||||
@ApiResponse(responseCode = "200", description = "Updated tag",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = TagDto.class)))
|
responseCode = "200",
|
||||||
public TagDto update(@PathVariable Long id, @RequestBody TagRequest req) {
|
description = "Created tag",
|
||||||
Tag tag = tagService.updateTag(id, req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon());
|
content = @Content(schema = @Schema(implementation = TagDto.class))
|
||||||
long count = postService.countPostsByTag(tag.getId());
|
)
|
||||||
return tagMapper.toDto(tag, count);
|
@SecurityRequirement(name = "JWT")
|
||||||
|
public TagDto create(
|
||||||
|
@RequestBody TagRequest req,
|
||||||
|
org.springframework.security.core.Authentication auth
|
||||||
|
) {
|
||||||
|
boolean approved = true;
|
||||||
|
if (postService.getPublishMode() == PublishMode.REVIEW && auth != null) {
|
||||||
|
com.openisle.model.User user = userRepository.findByUsername(auth.getName()).orElseThrow();
|
||||||
|
if (user.getRole() != Role.ADMIN) {
|
||||||
|
approved = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Tag tag = tagService.createTag(
|
||||||
|
req.getName(),
|
||||||
|
req.getDescription(),
|
||||||
|
req.getIcon(),
|
||||||
|
req.getSmallIcon(),
|
||||||
|
approved,
|
||||||
|
auth != null ? auth.getName() : null
|
||||||
|
);
|
||||||
|
long count = postService.countPostsByTag(tag.getId());
|
||||||
|
return tagMapper.toDto(tag, count);
|
||||||
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{id}")
|
@PutMapping("/{id}")
|
||||||
@Operation(summary = "Delete tag", description = "Delete a tag by id")
|
@Operation(summary = "Update tag", description = "Update an existing tag")
|
||||||
@ApiResponse(responseCode = "200", description = "Tag deleted")
|
@ApiResponse(
|
||||||
public void delete(@PathVariable Long id) {
|
responseCode = "200",
|
||||||
tagService.deleteTag(id);
|
description = "Updated tag",
|
||||||
}
|
content = @Content(schema = @Schema(implementation = TagDto.class))
|
||||||
|
)
|
||||||
|
public TagDto update(@PathVariable Long id, @RequestBody TagRequest req) {
|
||||||
|
Tag tag = tagService.updateTag(
|
||||||
|
id,
|
||||||
|
req.getName(),
|
||||||
|
req.getDescription(),
|
||||||
|
req.getIcon(),
|
||||||
|
req.getSmallIcon()
|
||||||
|
);
|
||||||
|
long count = postService.countPostsByTag(tag.getId());
|
||||||
|
return tagMapper.toDto(tag, count);
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping
|
@DeleteMapping("/{id}")
|
||||||
@Operation(summary = "List tags", description = "List tags with optional keyword")
|
@Operation(summary = "Delete tag", description = "Delete a tag by id")
|
||||||
@ApiResponse(responseCode = "200", description = "List of tags",
|
@ApiResponse(responseCode = "200", description = "Tag deleted")
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TagDto.class))))
|
public void delete(@PathVariable Long id) {
|
||||||
public List<TagDto> list(@RequestParam(value = "keyword", required = false) String keyword,
|
tagService.deleteTag(id);
|
||||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
}
|
||||||
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) {
|
|
||||||
return dtos.subList(0, limit);
|
|
||||||
}
|
|
||||||
return dtos;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
@GetMapping
|
||||||
@Operation(summary = "Get tag", description = "Get tag by id")
|
@Operation(summary = "List tags", description = "List tags with optional keyword")
|
||||||
@ApiResponse(responseCode = "200", description = "Tag detail",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = TagDto.class)))
|
responseCode = "200",
|
||||||
public TagDto get(@PathVariable Long id) {
|
description = "List of tags",
|
||||||
Tag tag = tagService.getTag(id);
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TagDto.class)))
|
||||||
long count = postService.countPostsByTag(tag.getId());
|
)
|
||||||
return tagMapper.toDto(tag, count);
|
public List<TagDto> list(
|
||||||
|
@RequestParam(value = "keyword", required = false) String keyword,
|
||||||
|
@RequestParam(value = "page", required = false) Integer page,
|
||||||
|
@RequestParam(value = "pageSize", required = false) Integer pageSize,
|
||||||
|
@RequestParam(value = "limit", required = false) Integer limit
|
||||||
|
) {
|
||||||
|
List<Tag> tags = tagService.searchTags(keyword);
|
||||||
|
List<Long> tagIds = tags.stream().map(Tag::getId).toList();
|
||||||
|
Map<Long, Long> postCntByTagIds = postService.countPostsByTagIds(tagIds);
|
||||||
|
if (postCntByTagIds == null) {
|
||||||
|
postCntByTagIds = java.util.Collections.emptyMap();
|
||||||
}
|
}
|
||||||
|
Map<Long, Long> finalPostCntByTagIds = postCntByTagIds;
|
||||||
|
List<TagDto> dtos = tags
|
||||||
|
.stream()
|
||||||
|
.map(t -> tagMapper.toDto(t, finalPostCntByTagIds.getOrDefault(t.getId(), 0L)))
|
||||||
|
.sorted((a, b) -> Long.compare(b.getCount(), a.getCount()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (page != null && pageSize != null && page >= 0 && pageSize > 0) {
|
||||||
|
int fromIndex = page * pageSize;
|
||||||
|
if (fromIndex >= dtos.size()) {
|
||||||
|
return java.util.Collections.emptyList();
|
||||||
|
}
|
||||||
|
int toIndex = Math.min(fromIndex + pageSize, dtos.size());
|
||||||
|
return new java.util.ArrayList<>(dtos.subList(fromIndex, toIndex));
|
||||||
|
}
|
||||||
|
if (limit != null && limit > 0 && dtos.size() > limit) {
|
||||||
|
return new java.util.ArrayList<>(dtos.subList(0, limit));
|
||||||
|
}
|
||||||
|
return dtos;
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}/posts")
|
@GetMapping("/{id}")
|
||||||
@Operation(summary = "List posts by tag", description = "Get posts with specific tag")
|
@Operation(summary = "Get tag", description = "Get tag by id")
|
||||||
@ApiResponse(responseCode = "200", description = "List of posts",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
|
responseCode = "200",
|
||||||
public List<PostSummaryDto> listPostsByTag(@PathVariable Long id,
|
description = "Tag detail",
|
||||||
@RequestParam(value = "page", required = false) Integer page,
|
content = @Content(schema = @Schema(implementation = TagDto.class))
|
||||||
@RequestParam(value = "pageSize", required = false) Integer pageSize) {
|
)
|
||||||
return postService.listPostsByTags(java.util.List.of(id), page, pageSize)
|
public TagDto get(@PathVariable Long id) {
|
||||||
.stream()
|
Tag tag = tagService.getTag(id);
|
||||||
.map(postMapper::toSummaryDto)
|
long count = postService.countPostsByTag(tag.getId());
|
||||||
.collect(Collectors.toList());
|
return tagMapper.toDto(tag, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/posts")
|
||||||
|
@Operation(summary = "List posts by tag", description = "Get posts with specific tag")
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "List of posts",
|
||||||
|
content = @Content(
|
||||||
|
array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
public List<PostSummaryDto> listPostsByTag(
|
||||||
|
@PathVariable Long id,
|
||||||
|
@RequestParam(value = "page", required = false) Integer page,
|
||||||
|
@RequestParam(value = "pageSize", required = false) Integer pageSize
|
||||||
|
) {
|
||||||
|
return postService
|
||||||
|
.listPostsByTags(java.util.List.of(id), page, pageSize)
|
||||||
|
.stream()
|
||||||
|
.map(postMapper::toSummaryDto)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,95 +1,99 @@
|
|||||||
package com.openisle.controller;
|
package com.openisle.controller;
|
||||||
|
|
||||||
import com.openisle.service.ImageUploader;
|
import com.openisle.service.ImageUploader;
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.net.URLConnection;
|
import java.net.URLConnection;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/upload")
|
@RequestMapping("/api/upload")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class UploadController {
|
public class UploadController {
|
||||||
private final ImageUploader imageUploader;
|
|
||||||
|
|
||||||
@Value("${app.upload.check-type:true}")
|
private final ImageUploader imageUploader;
|
||||||
private boolean checkImageType;
|
|
||||||
|
|
||||||
@Value("${app.upload.max-size:5242880}")
|
@Value("${app.upload.check-type:true}")
|
||||||
private long maxUploadSize;
|
private boolean checkImageType;
|
||||||
|
|
||||||
@PostMapping
|
@Value("${app.upload.max-size:5242880}")
|
||||||
@Operation(summary = "Upload file", description = "Upload image file")
|
private long maxUploadSize;
|
||||||
@ApiResponse(responseCode = "200", description = "Upload result",
|
|
||||||
content = @Content(schema = @Schema(implementation = java.util.Map.class)))
|
@PostMapping
|
||||||
public ResponseEntity<?> upload(@RequestParam("file") MultipartFile file) {
|
@Operation(summary = "Upload file", description = "Upload image file")
|
||||||
if (checkImageType && (file.getContentType() == null || !file.getContentType().startsWith("image/"))) {
|
@ApiResponse(
|
||||||
return ResponseEntity.badRequest().body(Map.of("code", 1, "msg", "File is not an image"));
|
responseCode = "200",
|
||||||
}
|
description = "Upload result",
|
||||||
if (file.getSize() > maxUploadSize) {
|
content = @Content(schema = @Schema(implementation = java.util.Map.class))
|
||||||
return ResponseEntity.badRequest().body(Map.of("code", 2, "msg", "File too large"));
|
)
|
||||||
}
|
public ResponseEntity<?> upload(@RequestParam("file") MultipartFile file) {
|
||||||
String url;
|
if (
|
||||||
try {
|
checkImageType &&
|
||||||
url = imageUploader.upload(file.getBytes(), file.getOriginalFilename()).join();
|
(file.getContentType() == null || !file.getContentType().startsWith("image/"))
|
||||||
} catch (IOException e) {
|
) {
|
||||||
return ResponseEntity.internalServerError().body(Map.of("code", 3, "msg", "Upload failed"));
|
return ResponseEntity.badRequest().body(Map.of("code", 1, "msg", "File is not an image"));
|
||||||
}
|
|
||||||
return ResponseEntity.ok(Map.of(
|
|
||||||
"code", 0,
|
|
||||||
"msg", "ok",
|
|
||||||
"data", Map.of("url", url)
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
if (file.getSize() > maxUploadSize) {
|
||||||
@PostMapping("/url")
|
return ResponseEntity.badRequest().body(Map.of("code", 2, "msg", "File too large"));
|
||||||
@Operation(summary = "Upload from URL", description = "Upload image from remote URL")
|
|
||||||
@ApiResponse(responseCode = "200", description = "Upload result",
|
|
||||||
content = @Content(schema = @Schema(implementation = java.util.Map.class)))
|
|
||||||
public ResponseEntity<?> uploadUrl(@RequestBody Map<String, String> body) {
|
|
||||||
String link = body.get("url");
|
|
||||||
if (link == null || link.isBlank()) {
|
|
||||||
return ResponseEntity.badRequest().body(Map.of("code", 1, "msg", "Missing url"));
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
URL u = URI.create(link).toURL();
|
|
||||||
byte[] data = u.openStream().readAllBytes();
|
|
||||||
if (data.length > maxUploadSize) {
|
|
||||||
return ResponseEntity.badRequest().body(Map.of("code", 2, "msg", "File too large"));
|
|
||||||
}
|
|
||||||
String filename = link.substring(link.lastIndexOf('/') + 1);
|
|
||||||
String contentType = URLConnection.guessContentTypeFromStream(new ByteArrayInputStream(data));
|
|
||||||
if (checkImageType && (contentType == null || !contentType.startsWith("image/"))) {
|
|
||||||
return ResponseEntity.badRequest().body(Map.of("code", 1, "msg", "File is not an image"));
|
|
||||||
}
|
|
||||||
String url = imageUploader.upload(data, filename).join();
|
|
||||||
return ResponseEntity.ok(Map.of(
|
|
||||||
"code", 0,
|
|
||||||
"msg", "ok",
|
|
||||||
"data", Map.of("url", url)
|
|
||||||
));
|
|
||||||
} catch (Exception e) {
|
|
||||||
return ResponseEntity.internalServerError().body(Map.of("code", 3, "msg", "Upload failed"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
String url;
|
||||||
@GetMapping("/presign")
|
try {
|
||||||
@Operation(summary = "Presign upload", description = "Get presigned upload URL")
|
url = imageUploader.upload(file.getBytes(), file.getOriginalFilename()).join();
|
||||||
@ApiResponse(responseCode = "200", description = "Presigned URL",
|
} catch (IOException e) {
|
||||||
content = @Content(schema = @Schema(implementation = java.util.Map.class)))
|
return ResponseEntity.internalServerError().body(Map.of("code", 3, "msg", "Upload failed"));
|
||||||
public java.util.Map<String, String> presign(@RequestParam("filename") String filename) {
|
|
||||||
return imageUploader.presignUpload(filename);
|
|
||||||
}
|
}
|
||||||
|
return ResponseEntity.ok(Map.of("code", 0, "msg", "ok", "data", Map.of("url", url)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/url")
|
||||||
|
@Operation(summary = "Upload from URL", description = "Upload image from remote URL")
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Upload result",
|
||||||
|
content = @Content(schema = @Schema(implementation = java.util.Map.class))
|
||||||
|
)
|
||||||
|
public ResponseEntity<?> uploadUrl(@RequestBody Map<String, String> body) {
|
||||||
|
String link = body.get("url");
|
||||||
|
if (link == null || link.isBlank()) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("code", 1, "msg", "Missing url"));
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
URL u = URI.create(link).toURL();
|
||||||
|
byte[] data = u.openStream().readAllBytes();
|
||||||
|
if (data.length > maxUploadSize) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("code", 2, "msg", "File too large"));
|
||||||
|
}
|
||||||
|
String filename = link.substring(link.lastIndexOf('/') + 1);
|
||||||
|
String contentType = URLConnection.guessContentTypeFromStream(new ByteArrayInputStream(data));
|
||||||
|
if (checkImageType && (contentType == null || !contentType.startsWith("image/"))) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("code", 1, "msg", "File is not an image"));
|
||||||
|
}
|
||||||
|
String url = imageUploader.upload(data, filename).join();
|
||||||
|
return ResponseEntity.ok(Map.of("code", 0, "msg", "ok", "data", Map.of("url", url)));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.internalServerError().body(Map.of("code", 3, "msg", "Upload failed"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/presign")
|
||||||
|
@Operation(summary = "Presign upload", description = "Get presigned upload URL")
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Presigned URL",
|
||||||
|
content = @Content(schema = @Schema(implementation = java.util.Map.class))
|
||||||
|
)
|
||||||
|
public java.util.Map<String, String> presign(@RequestParam("filename") String filename) {
|
||||||
|
return imageUploader.presignUpload(filename);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import io.swagger.v3.oas.annotations.media.Content;
|
|||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
@@ -19,257 +21,359 @@ import org.springframework.security.core.Authentication;
|
|||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/users")
|
@RequestMapping("/api/users")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class UserController {
|
public class UserController {
|
||||||
private final UserService userService;
|
|
||||||
private final ImageUploader imageUploader;
|
|
||||||
private final PostService postService;
|
|
||||||
private final CommentService commentService;
|
|
||||||
private final ReactionService reactionService;
|
|
||||||
private final TagService tagService;
|
|
||||||
private final SubscriptionService subscriptionService;
|
|
||||||
private final LevelService levelService;
|
|
||||||
private final JwtService jwtService;
|
|
||||||
private final UserMapper userMapper;
|
|
||||||
private final TagMapper tagMapper;
|
|
||||||
|
|
||||||
@Value("${app.upload.check-type:true}")
|
private final UserService userService;
|
||||||
private boolean checkImageType;
|
private final ImageUploader imageUploader;
|
||||||
|
private final PostService postService;
|
||||||
|
private final CommentService commentService;
|
||||||
|
private final ReactionService reactionService;
|
||||||
|
private final TagService tagService;
|
||||||
|
private final SubscriptionService subscriptionService;
|
||||||
|
private final LevelService levelService;
|
||||||
|
private final JwtService jwtService;
|
||||||
|
private final UserMapper userMapper;
|
||||||
|
private final TagMapper tagMapper;
|
||||||
|
|
||||||
@Value("${app.upload.max-size:5242880}")
|
@Value("${app.upload.check-type:true}")
|
||||||
private long maxUploadSize;
|
private boolean checkImageType;
|
||||||
|
|
||||||
@Value("${app.user.posts-limit:10}")
|
@Value("${app.upload.max-size:5242880}")
|
||||||
private int defaultPostsLimit;
|
private long maxUploadSize;
|
||||||
|
|
||||||
@Value("${app.user.replies-limit:50}")
|
@Value("${app.user.posts-limit:10}")
|
||||||
private int defaultRepliesLimit;
|
private int defaultPostsLimit;
|
||||||
|
|
||||||
@Value("${app.user.tags-limit:50}")
|
@Value("${app.user.replies-limit:50}")
|
||||||
private int defaultTagsLimit;
|
private int defaultRepliesLimit;
|
||||||
|
|
||||||
@GetMapping("/me")
|
@Value("${app.user.tags-limit:50}")
|
||||||
@SecurityRequirement(name = "JWT")
|
private int defaultTagsLimit;
|
||||||
@Operation(summary = "Current user", description = "Get current authenticated user information")
|
|
||||||
@ApiResponse(responseCode = "200", description = "User detail",
|
@GetMapping("/me")
|
||||||
content = @Content(schema = @Schema(implementation = UserDto.class)))
|
@SecurityRequirement(name = "JWT")
|
||||||
public ResponseEntity<UserDto> me(Authentication auth) {
|
@Operation(summary = "Current user", description = "Get current authenticated user information")
|
||||||
User user = userService.findByUsername(auth.getName()).orElseThrow();
|
@ApiResponse(
|
||||||
return ResponseEntity.ok(userMapper.toDto(user, auth));
|
responseCode = "200",
|
||||||
|
description = "User detail",
|
||||||
|
content = @Content(schema = @Schema(implementation = UserDto.class))
|
||||||
|
)
|
||||||
|
public ResponseEntity<UserDto> me(Authentication auth) {
|
||||||
|
User user = userService.findByUsername(auth.getName()).orElseThrow();
|
||||||
|
return ResponseEntity.ok(userMapper.toDto(user, auth));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/me/avatar")
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
@Operation(summary = "Upload avatar", description = "Upload avatar for current user")
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Upload result",
|
||||||
|
content = @Content(schema = @Schema(implementation = Map.class))
|
||||||
|
)
|
||||||
|
public ResponseEntity<?> uploadAvatar(
|
||||||
|
@RequestParam("file") MultipartFile file,
|
||||||
|
Authentication auth
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
checkImageType &&
|
||||||
|
(file.getContentType() == null || !file.getContentType().startsWith("image/"))
|
||||||
|
) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", "File is not an image"));
|
||||||
}
|
}
|
||||||
|
if (file.getSize() > maxUploadSize) {
|
||||||
@PostMapping("/me/avatar")
|
return ResponseEntity.badRequest().body(Map.of("error", "File too large"));
|
||||||
@SecurityRequirement(name = "JWT")
|
|
||||||
@Operation(summary = "Upload avatar", description = "Upload avatar for current user")
|
|
||||||
@ApiResponse(responseCode = "200", description = "Upload result",
|
|
||||||
content = @Content(schema = @Schema(implementation = Map.class)))
|
|
||||||
public ResponseEntity<?> uploadAvatar(@RequestParam("file") MultipartFile file,
|
|
||||||
Authentication auth) {
|
|
||||||
if (checkImageType && (file.getContentType() == null || !file.getContentType().startsWith("image/"))) {
|
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", "File is not an image"));
|
|
||||||
}
|
|
||||||
if (file.getSize() > maxUploadSize) {
|
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", "File too large"));
|
|
||||||
}
|
|
||||||
String url = null;
|
|
||||||
try {
|
|
||||||
url = imageUploader.upload(file.getBytes(), file.getOriginalFilename()).join();
|
|
||||||
} catch (IOException e) {
|
|
||||||
return ResponseEntity.internalServerError().body(Map.of("url", url));
|
|
||||||
}
|
|
||||||
userService.updateAvatar(auth.getName(), url);
|
|
||||||
return ResponseEntity.ok(Map.of("url", url));
|
|
||||||
}
|
}
|
||||||
|
String url = null;
|
||||||
@PutMapping("/me")
|
try {
|
||||||
@SecurityRequirement(name = "JWT")
|
url = imageUploader.upload(file.getBytes(), file.getOriginalFilename()).join();
|
||||||
@Operation(summary = "Update profile", description = "Update current user's profile")
|
} catch (IOException e) {
|
||||||
@ApiResponse(responseCode = "200", description = "Updated profile",
|
return ResponseEntity.internalServerError().body(Map.of("url", url));
|
||||||
content = @Content(schema = @Schema(implementation = Map.class)))
|
|
||||||
public ResponseEntity<?> updateProfile(@RequestBody UpdateProfileDto dto,
|
|
||||||
Authentication auth) {
|
|
||||||
User user = userService.updateProfile(auth.getName(), dto.getUsername(), dto.getIntroduction());
|
|
||||||
return ResponseEntity.ok(Map.of(
|
|
||||||
"token", jwtService.generateToken(user.getUsername()),
|
|
||||||
"user", userMapper.toDto(user, auth)
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
userService.updateAvatar(auth.getName(), url);
|
||||||
|
return ResponseEntity.ok(Map.of("url", url));
|
||||||
|
}
|
||||||
|
|
||||||
// 这个方法似乎没有使用?
|
@PutMapping("/me")
|
||||||
@PostMapping("/me/signin")
|
@SecurityRequirement(name = "JWT")
|
||||||
@SecurityRequirement(name = "JWT")
|
@Operation(summary = "Update profile", description = "Update current user's profile")
|
||||||
@Operation(summary = "Daily sign in", description = "Sign in to receive rewards")
|
@ApiResponse(
|
||||||
@ApiResponse(responseCode = "200", description = "Sign in reward",
|
responseCode = "200",
|
||||||
content = @Content(schema = @Schema(implementation = Map.class)))
|
description = "Updated profile",
|
||||||
public Map<String, Integer> signIn(Authentication auth) {
|
content = @Content(schema = @Schema(implementation = Map.class))
|
||||||
int reward = levelService.awardForSignin(auth.getName());
|
)
|
||||||
return Map.of("reward", reward);
|
public ResponseEntity<?> updateProfile(@RequestBody UpdateProfileDto dto, Authentication auth) {
|
||||||
}
|
User user = userService.updateProfile(auth.getName(), dto.getUsername(), dto.getIntroduction());
|
||||||
|
return ResponseEntity.ok(
|
||||||
|
Map.of(
|
||||||
|
"token",
|
||||||
|
jwtService.generateToken(user.getUsername()),
|
||||||
|
"user",
|
||||||
|
userMapper.toDto(user, auth)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/{identifier}")
|
// 这个方法似乎没有使用?
|
||||||
@Operation(summary = "Get user", description = "Get user by identifier")
|
@PostMapping("/me/signin")
|
||||||
@ApiResponse(responseCode = "200", description = "User detail",
|
@SecurityRequirement(name = "JWT")
|
||||||
content = @Content(schema = @Schema(implementation = UserDto.class)))
|
@Operation(summary = "Daily sign in", description = "Sign in to receive rewards")
|
||||||
public ResponseEntity<UserDto> getUser(@PathVariable("identifier") String identifier,
|
@ApiResponse(
|
||||||
Authentication auth) {
|
responseCode = "200",
|
||||||
User user = userService.findByIdentifier(identifier).orElseThrow(() -> new NotFoundException("User not found"));
|
description = "Sign in reward",
|
||||||
return ResponseEntity.ok(userMapper.toDto(user, auth));
|
content = @Content(schema = @Schema(implementation = Map.class))
|
||||||
}
|
)
|
||||||
|
public Map<String, Integer> signIn(Authentication auth) {
|
||||||
|
int reward = levelService.awardForSignin(auth.getName());
|
||||||
|
return Map.of("reward", reward);
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/{identifier}/posts")
|
@GetMapping("/{identifier}")
|
||||||
@Operation(summary = "User posts", description = "Get recent posts by user")
|
@Operation(summary = "Get user", description = "Get user by identifier")
|
||||||
@ApiResponse(responseCode = "200", description = "User posts",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostMetaDto.class))))
|
responseCode = "200",
|
||||||
public java.util.List<PostMetaDto> userPosts(@PathVariable("identifier") String identifier,
|
description = "User detail",
|
||||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
content = @Content(schema = @Schema(implementation = UserDto.class))
|
||||||
int l = limit != null ? limit : defaultPostsLimit;
|
)
|
||||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
public ResponseEntity<UserDto> getUser(
|
||||||
return postService.getRecentPostsByUser(user.getUsername(), l).stream()
|
@PathVariable("identifier") String identifier,
|
||||||
.map(userMapper::toMetaDto)
|
Authentication auth
|
||||||
.collect(java.util.stream.Collectors.toList());
|
) {
|
||||||
}
|
User user = userService
|
||||||
|
.findByIdentifier(identifier)
|
||||||
|
.orElseThrow(() -> new NotFoundException("User not found"));
|
||||||
|
return ResponseEntity.ok(userMapper.toDto(user, auth));
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/{identifier}/subscribed-posts")
|
@GetMapping("/{identifier}/posts")
|
||||||
@Operation(summary = "Subscribed posts", description = "Get posts the user subscribed to")
|
@Operation(summary = "User posts", description = "Get recent posts by user")
|
||||||
@ApiResponse(responseCode = "200", description = "Subscribed posts",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostMetaDto.class))))
|
responseCode = "200",
|
||||||
public java.util.List<PostMetaDto> subscribedPosts(@PathVariable("identifier") String identifier,
|
description = "User posts",
|
||||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostMetaDto.class)))
|
||||||
int l = limit != null ? limit : defaultPostsLimit;
|
)
|
||||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
public java.util.List<PostMetaDto> userPosts(
|
||||||
return subscriptionService.getSubscribedPosts(user.getUsername()).stream()
|
@PathVariable("identifier") String identifier,
|
||||||
.limit(l)
|
@RequestParam(value = "limit", required = false) Integer limit
|
||||||
.map(userMapper::toMetaDto)
|
) {
|
||||||
.collect(java.util.stream.Collectors.toList());
|
int l = limit != null ? limit : defaultPostsLimit;
|
||||||
}
|
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||||
|
return postService
|
||||||
|
.getRecentPostsByUser(user.getUsername(), l)
|
||||||
|
.stream()
|
||||||
|
.map(userMapper::toMetaDto)
|
||||||
|
.collect(java.util.stream.Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/{identifier}/replies")
|
@GetMapping("/{identifier}/subscribed-posts")
|
||||||
@Operation(summary = "User replies", description = "Get recent replies by user")
|
@Operation(summary = "Subscribed posts", description = "Get posts the user subscribed to")
|
||||||
@ApiResponse(responseCode = "200", description = "User replies",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = CommentInfoDto.class))))
|
responseCode = "200",
|
||||||
public java.util.List<CommentInfoDto> userReplies(@PathVariable("identifier") String identifier,
|
description = "Subscribed posts",
|
||||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostMetaDto.class)))
|
||||||
int l = limit != null ? limit : defaultRepliesLimit;
|
)
|
||||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
public java.util.List<PostMetaDto> subscribedPosts(
|
||||||
return commentService.getRecentCommentsByUser(user.getUsername(), l).stream()
|
@PathVariable("identifier") String identifier,
|
||||||
.map(userMapper::toCommentInfoDto)
|
@RequestParam(value = "limit", required = false) Integer limit
|
||||||
.collect(java.util.stream.Collectors.toList());
|
) {
|
||||||
}
|
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}/hot-posts")
|
@GetMapping("/{identifier}/replies")
|
||||||
@Operation(summary = "User hot posts", description = "Get most reacted posts by user")
|
@Operation(summary = "User replies", description = "Get recent replies by user")
|
||||||
@ApiResponse(responseCode = "200", description = "Hot posts",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostMetaDto.class))))
|
responseCode = "200",
|
||||||
public java.util.List<PostMetaDto> hotPosts(@PathVariable("identifier") String identifier,
|
description = "User replies",
|
||||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
content = @Content(
|
||||||
int l = limit != null ? limit : 10;
|
array = @ArraySchema(schema = @Schema(implementation = CommentInfoDto.class))
|
||||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
)
|
||||||
java.util.List<Long> ids = reactionService.topPostIds(user.getUsername(), l);
|
)
|
||||||
return postService.getPostsByIds(ids).stream()
|
public java.util.List<CommentInfoDto> userReplies(
|
||||||
.map(userMapper::toMetaDto)
|
@PathVariable("identifier") String identifier,
|
||||||
.collect(java.util.stream.Collectors.toList());
|
@RequestParam(value = "limit", required = false) Integer limit
|
||||||
}
|
) {
|
||||||
|
int l = limit != null ? limit : defaultRepliesLimit;
|
||||||
|
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||||
|
return commentService
|
||||||
|
.getRecentCommentsByUser(user.getUsername(), l)
|
||||||
|
.stream()
|
||||||
|
.map(userMapper::toCommentInfoDto)
|
||||||
|
.collect(java.util.stream.Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/{identifier}/hot-replies")
|
@GetMapping("/{identifier}/hot-posts")
|
||||||
@Operation(summary = "User hot replies", description = "Get most reacted replies by user")
|
@Operation(summary = "User hot posts", description = "Get most reacted posts by user")
|
||||||
@ApiResponse(responseCode = "200", description = "Hot replies",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = CommentInfoDto.class))))
|
responseCode = "200",
|
||||||
public java.util.List<CommentInfoDto> hotReplies(@PathVariable("identifier") String identifier,
|
description = "Hot posts",
|
||||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostMetaDto.class)))
|
||||||
int l = limit != null ? limit : 10;
|
)
|
||||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
public java.util.List<PostMetaDto> hotPosts(
|
||||||
java.util.List<Long> ids = reactionService.topCommentIds(user.getUsername(), l);
|
@PathVariable("identifier") String identifier,
|
||||||
return commentService.getCommentsByIds(ids).stream()
|
@RequestParam(value = "limit", required = false) Integer limit
|
||||||
.map(userMapper::toCommentInfoDto)
|
) {
|
||||||
.collect(java.util.stream.Collectors.toList());
|
int l = limit != null ? limit : 10;
|
||||||
}
|
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||||
|
java.util.List<Long> ids = reactionService.topPostIds(user.getUsername(), l);
|
||||||
|
return postService
|
||||||
|
.getPostsByIds(ids)
|
||||||
|
.stream()
|
||||||
|
.map(userMapper::toMetaDto)
|
||||||
|
.collect(java.util.stream.Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/{identifier}/hot-tags")
|
@GetMapping("/{identifier}/hot-replies")
|
||||||
@Operation(summary = "User hot tags", description = "Get tags frequently used by user")
|
@Operation(summary = "User hot replies", description = "Get most reacted replies by user")
|
||||||
@ApiResponse(responseCode = "200", description = "Hot tags",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TagDto.class))))
|
responseCode = "200",
|
||||||
public java.util.List<TagDto> hotTags(@PathVariable("identifier") String identifier,
|
description = "Hot replies",
|
||||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
content = @Content(
|
||||||
int l = limit != null ? limit : 10;
|
array = @ArraySchema(schema = @Schema(implementation = CommentInfoDto.class))
|
||||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
)
|
||||||
return tagService.getTagsByUser(user.getUsername()).stream()
|
)
|
||||||
.map(t -> tagMapper.toDto(t, postService.countPostsByTag(t.getId())))
|
public java.util.List<CommentInfoDto> hotReplies(
|
||||||
.sorted((a, b) -> Long.compare(b.getCount(), a.getCount()))
|
@PathVariable("identifier") String identifier,
|
||||||
.limit(l)
|
@RequestParam(value = "limit", required = false) Integer limit
|
||||||
.collect(java.util.stream.Collectors.toList());
|
) {
|
||||||
}
|
int l = limit != null ? limit : 10;
|
||||||
|
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||||
|
java.util.List<Long> ids = reactionService.topCommentIds(user.getUsername(), l);
|
||||||
|
return commentService
|
||||||
|
.getCommentsByIds(ids)
|
||||||
|
.stream()
|
||||||
|
.map(userMapper::toCommentInfoDto)
|
||||||
|
.collect(java.util.stream.Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/{identifier}/tags")
|
@GetMapping("/{identifier}/hot-tags")
|
||||||
@Operation(summary = "User tags", description = "Get recent tags used by user")
|
@Operation(summary = "User hot tags", description = "Get tags frequently used by user")
|
||||||
@ApiResponse(responseCode = "200", description = "User tags",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TagDto.class))))
|
responseCode = "200",
|
||||||
public java.util.List<TagDto> userTags(@PathVariable("identifier") String identifier,
|
description = "Hot tags",
|
||||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TagDto.class)))
|
||||||
int l = limit != null ? limit : defaultTagsLimit;
|
)
|
||||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
public java.util.List<TagDto> hotTags(
|
||||||
return tagService.getRecentTagsByUser(user.getUsername(), l).stream()
|
@PathVariable("identifier") String identifier,
|
||||||
.map(t -> tagMapper.toDto(t, postService.countPostsByTag(t.getId())))
|
@RequestParam(value = "limit", required = false) Integer limit
|
||||||
.collect(java.util.stream.Collectors.toList());
|
) {
|
||||||
}
|
int l = limit != null ? limit : 10;
|
||||||
|
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||||
|
return tagService
|
||||||
|
.getTagsByUser(user.getUsername())
|
||||||
|
.stream()
|
||||||
|
.map(t -> tagMapper.toDto(t, postService.countPostsByTag(t.getId())))
|
||||||
|
.sorted((a, b) -> Long.compare(b.getCount(), a.getCount()))
|
||||||
|
.limit(l)
|
||||||
|
.collect(java.util.stream.Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/{identifier}/following")
|
@GetMapping("/{identifier}/tags")
|
||||||
@Operation(summary = "Following users", description = "Get users that this user is following")
|
@Operation(summary = "User tags", description = "Get recent tags used by user")
|
||||||
@ApiResponse(responseCode = "200", description = "Following list",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = UserDto.class))))
|
responseCode = "200",
|
||||||
public java.util.List<UserDto> following(@PathVariable("identifier") String identifier) {
|
description = "User tags",
|
||||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TagDto.class)))
|
||||||
return subscriptionService.getSubscribedUsers(user.getUsername()).stream()
|
)
|
||||||
.map(userMapper::toDto)
|
public java.util.List<TagDto> userTags(
|
||||||
.collect(java.util.stream.Collectors.toList());
|
@PathVariable("identifier") String identifier,
|
||||||
}
|
@RequestParam(value = "limit", required = false) Integer limit
|
||||||
|
) {
|
||||||
|
int l = limit != null ? limit : defaultTagsLimit;
|
||||||
|
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||||
|
return tagService
|
||||||
|
.getRecentTagsByUser(user.getUsername(), l)
|
||||||
|
.stream()
|
||||||
|
.map(t -> tagMapper.toDto(t, postService.countPostsByTag(t.getId())))
|
||||||
|
.collect(java.util.stream.Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/{identifier}/followers")
|
@GetMapping("/{identifier}/following")
|
||||||
@Operation(summary = "Followers", description = "Get followers of this user")
|
@Operation(summary = "Following users", description = "Get users that this user is following")
|
||||||
@ApiResponse(responseCode = "200", description = "Followers list",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = UserDto.class))))
|
responseCode = "200",
|
||||||
public java.util.List<UserDto> followers(@PathVariable("identifier") String identifier) {
|
description = "Following list",
|
||||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = UserDto.class)))
|
||||||
return subscriptionService.getSubscribers(user.getUsername()).stream()
|
)
|
||||||
.map(userMapper::toDto)
|
public java.util.List<UserDto> following(@PathVariable("identifier") String identifier) {
|
||||||
.collect(java.util.stream.Collectors.toList());
|
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||||
}
|
return subscriptionService
|
||||||
|
.getSubscribedUsers(user.getUsername())
|
||||||
|
.stream()
|
||||||
|
.map(userMapper::toDto)
|
||||||
|
.collect(java.util.stream.Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/admins")
|
@GetMapping("/{identifier}/followers")
|
||||||
@Operation(summary = "Admin users", description = "List administrator users")
|
@Operation(summary = "Followers", description = "Get followers of this user")
|
||||||
@ApiResponse(responseCode = "200", description = "Admin users",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = UserDto.class))))
|
responseCode = "200",
|
||||||
public java.util.List<UserDto> admins() {
|
description = "Followers list",
|
||||||
return userService.getAdmins().stream()
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = UserDto.class)))
|
||||||
.map(userMapper::toDto)
|
)
|
||||||
.collect(java.util.stream.Collectors.toList());
|
public java.util.List<UserDto> followers(@PathVariable("identifier") String identifier) {
|
||||||
}
|
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||||
|
return subscriptionService
|
||||||
|
.getSubscribers(user.getUsername())
|
||||||
|
.stream()
|
||||||
|
.map(userMapper::toDto)
|
||||||
|
.collect(java.util.stream.Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/{identifier}/all")
|
@GetMapping("/admins")
|
||||||
@Operation(summary = "User aggregate", description = "Get aggregate information for user")
|
@Operation(summary = "Admin users", description = "List administrator users")
|
||||||
@ApiResponse(responseCode = "200", description = "User aggregate",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = UserAggregateDto.class)))
|
responseCode = "200",
|
||||||
public ResponseEntity<UserAggregateDto> userAggregate(@PathVariable("identifier") String identifier,
|
description = "Admin users",
|
||||||
@RequestParam(value = "postsLimit", required = false) Integer postsLimit,
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = UserDto.class)))
|
||||||
@RequestParam(value = "repliesLimit", required = false) Integer repliesLimit,
|
)
|
||||||
Authentication auth) {
|
public java.util.List<UserDto> admins() {
|
||||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
return userService
|
||||||
int pLimit = postsLimit != null ? postsLimit : defaultPostsLimit;
|
.getAdmins()
|
||||||
int rLimit = repliesLimit != null ? repliesLimit : defaultRepliesLimit;
|
.stream()
|
||||||
java.util.List<PostMetaDto> posts = postService.getRecentPostsByUser(user.getUsername(), pLimit).stream()
|
.map(userMapper::toDto)
|
||||||
.map(userMapper::toMetaDto)
|
.collect(java.util.stream.Collectors.toList());
|
||||||
.collect(java.util.stream.Collectors.toList());
|
}
|
||||||
java.util.List<CommentInfoDto> replies = commentService.getRecentCommentsByUser(user.getUsername(), rLimit).stream()
|
|
||||||
.map(userMapper::toCommentInfoDto)
|
@GetMapping("/{identifier}/all")
|
||||||
.collect(java.util.stream.Collectors.toList());
|
@Operation(summary = "User aggregate", description = "Get aggregate information for user")
|
||||||
UserAggregateDto dto = new UserAggregateDto();
|
@ApiResponse(
|
||||||
dto.setUser(userMapper.toDto(user, auth));
|
responseCode = "200",
|
||||||
dto.setPosts(posts);
|
description = "User aggregate",
|
||||||
dto.setReplies(replies);
|
content = @Content(schema = @Schema(implementation = UserAggregateDto.class))
|
||||||
return ResponseEntity.ok(dto);
|
)
|
||||||
}
|
public ResponseEntity<UserAggregateDto> userAggregate(
|
||||||
|
@PathVariable("identifier") String identifier,
|
||||||
|
@RequestParam(value = "postsLimit", required = false) Integer postsLimit,
|
||||||
|
@RequestParam(value = "repliesLimit", required = false) Integer repliesLimit,
|
||||||
|
Authentication auth
|
||||||
|
) {
|
||||||
|
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||||
|
int pLimit = postsLimit != null ? postsLimit : defaultPostsLimit;
|
||||||
|
int rLimit = repliesLimit != null ? repliesLimit : defaultRepliesLimit;
|
||||||
|
java.util.List<PostMetaDto> posts = postService
|
||||||
|
.getRecentPostsByUser(user.getUsername(), pLimit)
|
||||||
|
.stream()
|
||||||
|
.map(userMapper::toMetaDto)
|
||||||
|
.collect(java.util.stream.Collectors.toList());
|
||||||
|
java.util.List<CommentInfoDto> replies = commentService
|
||||||
|
.getRecentCommentsByUser(user.getUsername(), rLimit)
|
||||||
|
.stream()
|
||||||
|
.map(userMapper::toCommentInfoDto)
|
||||||
|
.collect(java.util.stream.Collectors.toList());
|
||||||
|
UserAggregateDto dto = new UserAggregateDto();
|
||||||
|
dto.setUser(userMapper.toDto(user, auth));
|
||||||
|
dto.setPosts(posts);
|
||||||
|
dto.setReplies(replies);
|
||||||
|
return ResponseEntity.ok(dto);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
package com.openisle.dto;
|
package com.openisle.dto;
|
||||||
|
|
||||||
import com.openisle.model.ActivityType;
|
import com.openisle.model.ActivityType;
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DTO representing an activity without participant details.
|
* DTO representing an activity without participant details.
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
public class ActivityDto {
|
public class ActivityDto {
|
||||||
private Long id;
|
|
||||||
private String title;
|
private Long id;
|
||||||
private String icon;
|
private String title;
|
||||||
private String content;
|
private String icon;
|
||||||
private LocalDateTime startTime;
|
private String content;
|
||||||
private LocalDateTime endTime;
|
private LocalDateTime startTime;
|
||||||
private ActivityType type;
|
private LocalDateTime endTime;
|
||||||
private boolean ended;
|
private ActivityType type;
|
||||||
|
private boolean ended;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
package com.openisle.dto;
|
package com.openisle.dto;
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import com.openisle.model.MedalType;
|
import com.openisle.model.MedalType;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DTO representing a post or comment author.
|
* DTO representing a post or comment author.
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
public class AuthorDto {
|
public class AuthorDto {
|
||||||
private Long id;
|
|
||||||
private String username;
|
|
||||||
private String avatar;
|
|
||||||
private MedalType displayMedal;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
private String username;
|
||||||
|
private String avatar;
|
||||||
|
private MedalType displayMedal;
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import lombok.Data;
|
|||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
public class CategoryDto {
|
public class CategoryDto {
|
||||||
private Long id;
|
|
||||||
private String name;
|
|
||||||
private String description;
|
|
||||||
private String icon;
|
|
||||||
private String smallIcon;
|
|
||||||
private Long count;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
private String name;
|
||||||
|
private String description;
|
||||||
|
private String icon;
|
||||||
|
private String smallIcon;
|
||||||
|
private Long count;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import lombok.Data;
|
|||||||
/** Request body for creating or updating a category. */
|
/** Request body for creating or updating a category. */
|
||||||
@Data
|
@Data
|
||||||
public class CategoryRequest {
|
public class CategoryRequest {
|
||||||
private String name;
|
|
||||||
private String description;
|
private String name;
|
||||||
private String icon;
|
private String description;
|
||||||
private String smallIcon;
|
private String icon;
|
||||||
|
private String smallIcon;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ import lombok.Setter;
|
|||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
public class ChannelDto {
|
public class ChannelDto {
|
||||||
private Long id;
|
|
||||||
private String name;
|
private Long id;
|
||||||
private String description;
|
private String name;
|
||||||
private String avatar;
|
private String description;
|
||||||
private MessageDto lastMessage;
|
private String avatar;
|
||||||
private long memberCount;
|
private MessageDto lastMessage;
|
||||||
private boolean joined;
|
private long memberCount;
|
||||||
private long unreadCount;
|
private boolean joined;
|
||||||
|
private long unreadCount;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO representing the context of a comment including its post and previous comments.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class CommentContextDto {
|
||||||
|
|
||||||
|
private PostSummaryDto post;
|
||||||
|
private CommentDto targetComment;
|
||||||
|
private List<CommentDto> previousComments;
|
||||||
|
}
|
||||||
@@ -1,23 +1,22 @@
|
|||||||
package com.openisle.dto;
|
package com.openisle.dto;
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DTO representing a comment and its nested replies.
|
* DTO representing a comment and its nested replies.
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
public class CommentDto {
|
public class CommentDto {
|
||||||
private Long id;
|
|
||||||
private String content;
|
|
||||||
private LocalDateTime createdAt;
|
|
||||||
private LocalDateTime pinnedAt;
|
|
||||||
private AuthorDto author;
|
|
||||||
private List<CommentDto> replies;
|
|
||||||
private List<ReactionDto> reactions;
|
|
||||||
private int reward;
|
|
||||||
private int pointReward;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
private String content;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime pinnedAt;
|
||||||
|
private AuthorDto author;
|
||||||
|
private List<CommentDto> replies;
|
||||||
|
private List<ReactionDto> reactions;
|
||||||
|
private int reward;
|
||||||
|
private int pointReward;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
package com.openisle.dto;
|
package com.openisle.dto;
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
/** DTO for comment information in user profiles. */
|
/** DTO for comment information in user profiles. */
|
||||||
@Data
|
@Data
|
||||||
public class CommentInfoDto {
|
public class CommentInfoDto {
|
||||||
private Long id;
|
|
||||||
private String content;
|
private Long id;
|
||||||
private LocalDateTime createdAt;
|
private String content;
|
||||||
private PostMetaDto post;
|
private LocalDateTime createdAt;
|
||||||
private ParentCommentDto parentComment;
|
private PostMetaDto post;
|
||||||
|
private ParentCommentDto parentComment;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import lombok.EqualsAndHashCode;
|
|||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
public class CommentMedalDto extends MedalDto {
|
public class CommentMedalDto extends MedalDto {
|
||||||
private long currentCommentCount;
|
|
||||||
private long targetCommentCount;
|
private long currentCommentCount;
|
||||||
|
private long targetCommentCount;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import lombok.Data;
|
|||||||
/** Request body for creating or replying to a comment. */
|
/** Request body for creating or replying to a comment. */
|
||||||
@Data
|
@Data
|
||||||
public class CommentRequest {
|
public class CommentRequest {
|
||||||
private String content;
|
|
||||||
private String captcha;
|
private String content;
|
||||||
|
private String captcha;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ import lombok.Data;
|
|||||||
/** DTO for site configuration. */
|
/** DTO for site configuration. */
|
||||||
@Data
|
@Data
|
||||||
public class ConfigDto {
|
public class ConfigDto {
|
||||||
private PublishMode publishMode;
|
|
||||||
private PasswordStrength passwordStrength;
|
private PublishMode publishMode;
|
||||||
private Integer aiFormatLimit;
|
private PasswordStrength passwordStrength;
|
||||||
private RegisterMode registerMode;
|
private Integer aiFormatLimit;
|
||||||
|
private RegisterMode registerMode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import lombok.EqualsAndHashCode;
|
|||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
public class ContributorMedalDto extends MedalDto {
|
public class ContributorMedalDto extends MedalDto {
|
||||||
private long currentContributionLines;
|
|
||||||
private long targetContributionLines;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
private long currentContributionLines;
|
||||||
|
private long targetContributionLines;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
package com.openisle.dto;
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class ConversationDetailDto {
|
public class ConversationDetailDto {
|
||||||
private Long id;
|
|
||||||
private String name;
|
private Long id;
|
||||||
private boolean channel;
|
private String name;
|
||||||
private String avatar;
|
private boolean channel;
|
||||||
private List<UserSummaryDto> participants;
|
private String avatar;
|
||||||
private Page<MessageDto> messages;
|
private List<UserSummaryDto> participants;
|
||||||
}
|
private Page<MessageDto> messages;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
package com.openisle.dto;
|
package com.openisle.dto;
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
public class ConversationDto {
|
public class ConversationDto {
|
||||||
private Long id;
|
|
||||||
private String name;
|
private Long id;
|
||||||
private boolean channel;
|
private String name;
|
||||||
private String avatar;
|
private boolean channel;
|
||||||
private MessageDto lastMessage;
|
private String avatar;
|
||||||
private List<UserSummaryDto> participants;
|
private MessageDto lastMessage;
|
||||||
private LocalDateTime createdAt;
|
private List<UserSummaryDto> participants;
|
||||||
private long unreadCount;
|
private LocalDateTime createdAt;
|
||||||
}
|
private long unreadCount;
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,5 +4,6 @@ import lombok.Data;
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class CreateConversationRequest {
|
public class CreateConversationRequest {
|
||||||
private Long recipientId;
|
|
||||||
}
|
private Long recipientId;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,5 +8,6 @@ import lombok.NoArgsConstructor;
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
public class CreateConversationResponse {
|
public class CreateConversationResponse {
|
||||||
private Long conversationId;
|
|
||||||
}
|
private Long conversationId;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import lombok.Data;
|
|||||||
/** Request for Discord OAuth login. */
|
/** Request for Discord OAuth login. */
|
||||||
@Data
|
@Data
|
||||||
public class DiscordLoginRequest {
|
public class DiscordLoginRequest {
|
||||||
private String code;
|
|
||||||
private String redirectUri;
|
private String code;
|
||||||
private String inviteToken;
|
private String redirectUri;
|
||||||
|
private String inviteToken;
|
||||||
}
|
}
|
||||||
|
|||||||
16
backend/src/main/java/com/openisle/dto/DonationDto.java
Normal file
16
backend/src/main/java/com/openisle/dto/DonationDto.java
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class DonationDto {
|
||||||
|
|
||||||
|
private Long userId;
|
||||||
|
private String username;
|
||||||
|
private String avatar;
|
||||||
|
private int amount;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
11
backend/src/main/java/com/openisle/dto/DonationRequest.java
Normal file
11
backend/src/main/java/com/openisle/dto/DonationRequest.java
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class DonationRequest {
|
||||||
|
|
||||||
|
private int amount;
|
||||||
|
}
|
||||||
15
backend/src/main/java/com/openisle/dto/DonationResponse.java
Normal file
15
backend/src/main/java/com/openisle/dto/DonationResponse.java
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class DonationResponse {
|
||||||
|
|
||||||
|
private int totalAmount;
|
||||||
|
private List<DonationDto> donations = new ArrayList<>();
|
||||||
|
private Integer balance;
|
||||||
|
}
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
package com.openisle.dto;
|
package com.openisle.dto;
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
/** DTO representing a saved draft. */
|
/** DTO representing a saved draft. */
|
||||||
@Data
|
@Data
|
||||||
public class DraftDto {
|
public class DraftDto {
|
||||||
private Long id;
|
|
||||||
private String title;
|
private Long id;
|
||||||
private String content;
|
private String title;
|
||||||
private Long categoryId;
|
private String content;
|
||||||
private List<Long> tagIds;
|
private Long categoryId;
|
||||||
|
private List<Long> tagIds;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
package com.openisle.dto;
|
package com.openisle.dto;
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
/** Request body for saving a draft. */
|
/** Request body for saving a draft. */
|
||||||
@Data
|
@Data
|
||||||
public class DraftRequest {
|
public class DraftRequest {
|
||||||
private String title;
|
|
||||||
private String content;
|
private String title;
|
||||||
private Long categoryId;
|
private String content;
|
||||||
private List<Long> tagIds;
|
private Long categoryId;
|
||||||
|
private List<Long> tagIds;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import lombok.EqualsAndHashCode;
|
|||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
public class FeaturedMedalDto extends MedalDto {
|
public class FeaturedMedalDto extends MedalDto {
|
||||||
private long currentFeaturedCount;
|
|
||||||
private long targetFeaturedCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
private long currentFeaturedCount;
|
||||||
|
private long targetFeaturedCount;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ import lombok.Data;
|
|||||||
/** Request to trigger a forgot password email. */
|
/** Request to trigger a forgot password email. */
|
||||||
@Data
|
@Data
|
||||||
public class ForgotPasswordRequest {
|
public class ForgotPasswordRequest {
|
||||||
private String email;
|
|
||||||
|
private String email;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import lombok.Data;
|
|||||||
/** Request for GitHub OAuth login. */
|
/** Request for GitHub OAuth login. */
|
||||||
@Data
|
@Data
|
||||||
public class GithubLoginRequest {
|
public class GithubLoginRequest {
|
||||||
private String code;
|
|
||||||
private String redirectUri;
|
private String code;
|
||||||
private String inviteToken;
|
private String redirectUri;
|
||||||
|
private String inviteToken;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import lombok.Data;
|
|||||||
/** Request for Google OAuth login. */
|
/** Request for Google OAuth login. */
|
||||||
@Data
|
@Data
|
||||||
public class GoogleLoginRequest {
|
public class GoogleLoginRequest {
|
||||||
private String idToken;
|
|
||||||
private String inviteToken;
|
private String idToken;
|
||||||
|
private String inviteToken;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import lombok.Data;
|
|||||||
/** Request to login. */
|
/** Request to login. */
|
||||||
@Data
|
@Data
|
||||||
public class LoginRequest {
|
public class LoginRequest {
|
||||||
private String username;
|
|
||||||
private String password;
|
private String username;
|
||||||
private String captcha;
|
private String password;
|
||||||
|
private String captcha;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
package com.openisle.dto;
|
package com.openisle.dto;
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
/** Metadata for lottery posts. */
|
/** Metadata for lottery posts. */
|
||||||
@Data
|
@Data
|
||||||
public class LotteryDto {
|
public class LotteryDto {
|
||||||
private String prizeDescription;
|
|
||||||
private String prizeIcon;
|
private String prizeDescription;
|
||||||
private int prizeCount;
|
private String prizeIcon;
|
||||||
private int pointCost;
|
private int prizeCount;
|
||||||
private LocalDateTime startTime;
|
private int pointCost;
|
||||||
private LocalDateTime endTime;
|
private LocalDateTime startTime;
|
||||||
private List<AuthorDto> participants;
|
private LocalDateTime endTime;
|
||||||
private List<AuthorDto> winners;
|
private List<AuthorDto> participants;
|
||||||
|
private List<AuthorDto> winners;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import lombok.Data;
|
|||||||
/** Request to submit a reason (e.g., for moderation). */
|
/** Request to submit a reason (e.g., for moderation). */
|
||||||
@Data
|
@Data
|
||||||
public class MakeReasonRequest {
|
public class MakeReasonRequest {
|
||||||
private String token;
|
|
||||||
private String reason;
|
private String token;
|
||||||
|
private String reason;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ import lombok.Data;
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class MedalDto {
|
public class MedalDto {
|
||||||
private String icon;
|
|
||||||
private String title;
|
private String icon;
|
||||||
private String description;
|
private String title;
|
||||||
private MedalType type;
|
private String description;
|
||||||
private boolean completed;
|
private MedalType type;
|
||||||
private boolean selected;
|
private boolean completed;
|
||||||
|
private boolean selected;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ import lombok.Data;
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class MedalSelectRequest {
|
public class MedalSelectRequest {
|
||||||
private MedalType type;
|
|
||||||
|
private MedalType type;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
package com.openisle.dto;
|
package com.openisle.dto;
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class MessageDto {
|
public class MessageDto {
|
||||||
private Long id;
|
|
||||||
private String content;
|
private Long id;
|
||||||
private UserSummaryDto sender;
|
private String content;
|
||||||
private Long conversationId;
|
private UserSummaryDto sender;
|
||||||
private LocalDateTime createdAt;
|
private Long conversationId;
|
||||||
private MessageDto replyTo;
|
private LocalDateTime createdAt;
|
||||||
private List<ReactionDto> reactions;
|
private MessageDto replyTo;
|
||||||
}
|
private List<ReactionDto> reactions;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
package com.openisle.dto;
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class MessageNotificationPayload implements Serializable {
|
public class MessageNotificationPayload implements Serializable {
|
||||||
private String targetUsername;
|
|
||||||
private Object payload;
|
private String targetUsername;
|
||||||
}
|
private Object payload;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import lombok.Data;
|
|||||||
/** Info about the milk tea activity. */
|
/** Info about the milk tea activity. */
|
||||||
@Data
|
@Data
|
||||||
public class MilkTeaInfoDto {
|
public class MilkTeaInfoDto {
|
||||||
private long redeemCount;
|
|
||||||
private boolean ended;
|
private long redeemCount;
|
||||||
|
private boolean ended;
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user