mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-10 09:00:53 +08:00
Compare commits
79 Commits
codex/remo
...
codex/crea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efbb83924b | ||
|
|
26d1db79f4 | ||
|
|
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 |
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]
|
||||||
|
|
||||||
**附加上下文**
|
**附加上下文**
|
||||||
在此添加与问题相关的其他上下文信息。
|
在此添加与问题相关的其他上下文信息。
|
||||||
|
|||||||
2
.github/workflows/deploy-staging.yml
vendored
2
.github/workflows/deploy-staging.yml
vendored
@@ -12,6 +12,7 @@ jobs:
|
|||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: Deploy
|
environment: Deploy
|
||||||
|
if: ${{ !github.event.repository.fork }} # 只有非 fork 才执行
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -31,4 +32,3 @@ jobs:
|
|||||||
secrets: inherit
|
secrets: inherit
|
||||||
with:
|
with:
|
||||||
build-id: ${{ github.run_id }}
|
build-id: ${{ github.run_id }}
|
||||||
|
|
||||||
|
|||||||
@@ -15,14 +15,14 @@ If you believe someone is violating the code of conduct, we ask that you report
|
|||||||
- **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).
|
||||||
|
|||||||
131
CONTRIBUTING.md
131
CONTRIBUTING.md
@@ -4,6 +4,8 @@
|
|||||||
- [配置环境变量](#配置环境变量)
|
- [配置环境变量](#配置环境变量)
|
||||||
- [配置 IDEA 参数](#配置-idea-参数)
|
- [配置 IDEA 参数](#配置-idea-参数)
|
||||||
- [配置 MySQL](#配置-mysql)
|
- [配置 MySQL](#配置-mysql)
|
||||||
|
- [配置 Redis](#配置-redis)
|
||||||
|
- [配置 RabbitMQ](#配置-rabbitmq)
|
||||||
- [Docker 环境](#docker-环境)
|
- [Docker 环境](#docker-环境)
|
||||||
- [配置环境变量](#配置环境变量-1)
|
- [配置环境变量](#配置环境变量-1)
|
||||||
- [构建并启动镜像](#构建并启动镜像)
|
- [构建并启动镜像](#构建并启动镜像)
|
||||||
@@ -11,6 +13,13 @@
|
|||||||
- [配置环境变量](#配置环境变量-2)
|
- [配置环境变量](#配置环境变量-2)
|
||||||
- [安装依赖和运行](#安装依赖和运行)
|
- [安装依赖和运行](#安装依赖和运行)
|
||||||
- [其他配置](#其他配置)
|
- [其他配置](#其他配置)
|
||||||
|
- [配置第三方登录以GitHub为例](#配置第三方登录以GitHub为例)
|
||||||
|
- [配置Resend邮箱服务](#配置Resend邮箱服务)
|
||||||
|
- [API文档](#api文档)
|
||||||
|
- [OpenAPI文档](#openapi文档)
|
||||||
|
- [部署时间线以及文档时效性](#部署时间线以及文档时效性)
|
||||||
|
- [OpenAPI文档使用](#OpenAPI文档使用)
|
||||||
|
- [OpenAPI文档应用场景](#OpenAPI文档应用场景)
|
||||||
|
|
||||||
## 前置工作
|
## 前置工作
|
||||||
|
|
||||||
@@ -88,9 +97,8 @@ SERVER_PORT=8082
|
|||||||
> 如果不知道怎么配置数据库可以参考 [Docker 环境](#docker-环境) 章节
|
> 如果不知道怎么配置数据库可以参考 [Docker 环境](#docker-环境) 章节
|
||||||
|
|
||||||
1. 本机配置 MySQL 服务(网上很多教程,忽略)
|
1. 本机配置 MySQL 服务(网上很多教程,忽略)
|
||||||
|
- 可以用 Laragon,自带 MySQL 包括 Nodejs,版本建议 `6.x`,`7` 以后需要 Lisence
|
||||||
+ 可以用 Laragon,自带 MySQL 包括 Nodejs,版本建议 `6.x`,`7` 以后需要 Lisence
|
- [下载地址](https://github.com/leokhoa/laragon/releases)
|
||||||
+ [下载地址](https://github.com/leokhoa/laragon/releases)
|
|
||||||
|
|
||||||
2. 填写环境变量
|
2. 填写环境变量
|
||||||
|
|
||||||
@@ -111,14 +119,75 @@ SERVER_PORT=8082
|
|||||||
|
|
||||||
#### 配置 Redis
|
#### 配置 Redis
|
||||||
|
|
||||||
填写环境变量 `.env` 中的 Redis 相关配置并启动 Redis
|
后端的登录态缓存、访问频控等都依赖 Redis,请确保本地有可用的 Redis 实例。
|
||||||
|
|
||||||
```ini
|
1. **启动 Redis 服务**(已有服务可跳过)
|
||||||
REDIS_HOST=<Redis 地址>
|
|
||||||
REDIS_PORT=<Redis 端口>
|
|
||||||
```
|
|
||||||
|
|
||||||
处理完环境问题直接跑起来就能通了
|
```bash
|
||||||
|
docker run --name openisle-redis -p 6379:6379 -d redis:7-alpine
|
||||||
|
```
|
||||||
|
|
||||||
|
该命令会在本机暴露 `6379` 端口。若你已有其他端口的 Redis,可以根据实际情况调整映射关系。
|
||||||
|
|
||||||
|
2. **在 `backend/open-isle.env` 中填写连接信息**
|
||||||
|
|
||||||
|
```ini
|
||||||
|
REDIS_HOST=127.0.0.1
|
||||||
|
REDIS_PORT=6379
|
||||||
|
# 可选:若需要切换逻辑库,可新增此变量,默认使用 0 号库
|
||||||
|
REDIS_DATABASE=0
|
||||||
|
```
|
||||||
|
|
||||||
|
`application.properties` 中的默认值为 `localhost:6379`、数据库 `0`,如果你的环境恰好一致,也可以不额外填写;显式声明可以避免 IDE/运行时读取到意外配置。
|
||||||
|
|
||||||
|
3. **验证连接**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
redis-cli -h 127.0.0.1 -p 6379 ping
|
||||||
|
```
|
||||||
|
|
||||||
|
启动后端后,日志中会出现 `Redis connection established ...`(来自 `RedisConnectionLogger`),说明已成功连通。
|
||||||
|
|
||||||
|
#### 配置 RabbitMQ
|
||||||
|
|
||||||
|
消息通知和 WebSocket 推送链路依赖 RabbitMQ。后端会自动声明交换机与队列,确保本地 RabbitMQ 可用即可。
|
||||||
|
|
||||||
|
1. **启动 RabbitMQ 服务**(推荐包含管理界面)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --name openisle-rabbitmq \
|
||||||
|
-e RABBITMQ_DEFAULT_USER=openisle \
|
||||||
|
-e RABBITMQ_DEFAULT_PASS=openisle \
|
||||||
|
-p 5672:5672 -p 15672:15672 \
|
||||||
|
-d rabbitmq:3.13-management
|
||||||
|
```
|
||||||
|
|
||||||
|
管理界面位于 http://127.0.0.1:15672 ,可用于查看队列、交换机等资源。
|
||||||
|
|
||||||
|
2. **同步填写后端与 WebSocket 服务的环境变量**
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# backend/open-isle.env
|
||||||
|
RABBITMQ_HOST=127.0.0.1
|
||||||
|
RABBITMQ_PORT=5672
|
||||||
|
RABBITMQ_USERNAME=openisle
|
||||||
|
RABBITMQ_PASSWORD=openisle
|
||||||
|
|
||||||
|
# 如果需要启动 websocket_service,也需要在 websocket_service.env 中保持一致
|
||||||
|
```
|
||||||
|
|
||||||
|
如果沿用 RabbitMQ 默认的 `guest/guest`,可以不显式设置,Spring Boot 会回退到 `application.properties` 中的默认值 (`localhost:5672`、`guest/guest`、虚拟主机 `/`)。
|
||||||
|
|
||||||
|
3. **确认自动声明的资源**
|
||||||
|
|
||||||
|
- 交换机:`openisle-exchange`
|
||||||
|
- 旧版兼容队列:`notifications-queue`
|
||||||
|
- 分片队列:`notifications-queue-0` ~ `notifications-queue-f`(共 16 个,对应路由键 `notifications.shard.0` ~ `notifications.shard.f`)
|
||||||
|
- 队列持久化默认开启,来自 `rabbitmq.queue.durable=true`,如需仅在本地短暂测试,可在 `application.properties` 中调整该配置。
|
||||||
|
|
||||||
|
启动后端时可在日志中看到 `=== 开始主动声明 RabbitMQ 组件 ===` 与后续的声明结果,也可以在管理界面中查看是否创建成功。
|
||||||
|
|
||||||
|
完成 Redis 与 RabbitMQ 配置后,即可继续启动后端服务。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -210,7 +279,7 @@ npm run dev
|
|||||||
|
|
||||||
## 其他配置
|
## 其他配置
|
||||||
|
|
||||||
### 配置第三方登录,这里以 GitHub 为例:
|
### 配置第三方登录以GitHub为例
|
||||||
|
|
||||||
- 修改 `application.properties` 配置
|
- 修改 `application.properties` 配置
|
||||||
|
|
||||||
@@ -226,7 +295,7 @@ npm run dev
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
### 配置 Resend 邮箱服务
|
### 配置Resend邮箱服务
|
||||||
|
|
||||||
https://resend.com/emails 创建账号并登录
|
https://resend.com/emails 创建账号并登录
|
||||||
|
|
||||||
@@ -247,8 +316,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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import org.springframework.scheduling.annotation.EnableScheduling;
|
|||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@EnableScheduling
|
@EnableScheduling
|
||||||
public class OpenIsleApplication {
|
public class OpenIsleApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(OpenIsleApplication.class, args);
|
SpringApplication.run(OpenIsleApplication.class, args);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,16 @@ 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;
|
private final ActivityRepository activityRepository;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -21,7 +22,9 @@ public class ActivityInitializer implements CommandLineRunner {
|
|||||||
a.setTitle("🎡建站送奶茶活动");
|
a.setTitle("🎡建站送奶茶活动");
|
||||||
a.setType(ActivityType.MILK_TEA);
|
a.setType(ActivityType.MILK_TEA);
|
||||||
a.setIcon("https://icons.veryicon.com/png/o/food--drinks/delicious-food-1/coffee-36.png");
|
a.setIcon("https://icons.veryicon.com/png/o/food--drinks/delicious-food-1/coffee-36.png");
|
||||||
a.setContent("为了有利于建站推广以及激励发布内容,我们推出了建站送奶茶的活动,前50名达到level 1的用户,可以联系站长获取奶茶/咖啡一杯");
|
a.setContent(
|
||||||
|
"为了有利于建站推广以及激励发布内容,我们推出了建站送奶茶的活动,前50名达到level 1的用户,可以联系站长获取奶茶/咖啡一杯"
|
||||||
|
);
|
||||||
activityRepository.save(a);
|
activityRepository.save(a);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
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")
|
@Bean(name = "notificationExecutor")
|
||||||
public Executor notificationExecutor() {
|
public Executor notificationExecutor() {
|
||||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -35,23 +34,25 @@ import java.util.Map;
|
|||||||
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
|
||||||
@@ -64,8 +65,13 @@ public class CachingConfig {
|
|||||||
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()
|
||||||
|
.disable(Hibernate6Module.Feature.USE_TRANSIENT_ANNOTATION)
|
||||||
|
// 将 Hibernate 特有的集合类型转换为标准 Java 集合类型
|
||||||
|
// 避免序列化时出现 org.hibernate.collection.spi.PersistentSet 这样的类型信息
|
||||||
|
.configure(Hibernate6Module.Feature.REPLACE_PERSISTENT_COLLECTIONS, true)
|
||||||
|
);
|
||||||
// service的时候带上类型信息
|
// service的时候带上类型信息
|
||||||
// 启用类型信息,避免 LinkedHashMap 问题
|
// 启用类型信息,避免 LinkedHashMap 问题
|
||||||
objectMapper.activateDefaultTyping(
|
objectMapper.activateDefaultTyping(
|
||||||
@@ -81,19 +87,27 @@ public class CachingConfig {
|
|||||||
* 配置 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()
|
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
|
||||||
.entryTtl(Duration.ZERO) // 默认缓存不过期
|
.entryTtl(Duration.ZERO) // 默认缓存不过期
|
||||||
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
|
.serializeKeysWith(
|
||||||
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
|
RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())
|
||||||
|
)
|
||||||
|
.serializeValuesWith(
|
||||||
|
RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer)
|
||||||
|
)
|
||||||
.disableCachingNullValues(); // 禁止缓存 null 值
|
.disableCachingNullValues(); // 禁止缓存 null 值
|
||||||
|
|
||||||
// 个别缓存单独设置 TTL 时间
|
// 个别缓存单独设置 TTL 时间
|
||||||
Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
|
Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
|
||||||
RedisCacheConfiguration oneHourConfig = config.entryTtl(Duration.ofHours(1));
|
RedisCacheConfiguration oneHourConfig = config.entryTtl(Duration.ofHours(1));
|
||||||
|
RedisCacheConfiguration tenMinutesConfig = config.entryTtl(Duration.ofMinutes(10));
|
||||||
cacheConfigs.put(TAG_CACHE_NAME, oneHourConfig);
|
cacheConfigs.put(TAG_CACHE_NAME, oneHourConfig);
|
||||||
cacheConfigs.put(CATEGORY_CACHE_NAME, oneHourConfig);
|
cacheConfigs.put(CATEGORY_CACHE_NAME, oneHourConfig);
|
||||||
|
cacheConfigs.put(POST_CACHE_NAME, tenMinutesConfig);
|
||||||
|
|
||||||
return RedisCacheManager.builder(connectionFactory)
|
return RedisCacheManager.builder(connectionFactory)
|
||||||
.cacheDefaults(config)
|
.cacheDefaults(config)
|
||||||
@@ -105,7 +119,10 @@ public class CachingConfig {
|
|||||||
* 配置 RedisTemplate,支持直接操作 Redis
|
* 配置 RedisTemplate,支持直接操作 Redis
|
||||||
*/
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory, RedisSerializer<Object> redisSerializer) {
|
public RedisTemplate<String, Object> redisTemplate(
|
||||||
|
RedisConnectionFactory connectionFactory,
|
||||||
|
RedisSerializer<Object> redisSerializer
|
||||||
|
) {
|
||||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||||
template.setConnectionFactory(connectionFactory);
|
template.setConnectionFactory(connectionFactory);
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import org.springframework.stereotype.Component;
|
|||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ChannelInitializer implements CommandLineRunner {
|
public class ChannelInitializer implements CommandLineRunner {
|
||||||
|
|
||||||
private final MessageConversationRepository conversationRepository;
|
private final MessageConversationRepository conversationRepository;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -18,14 +19,18 @@ public class ChannelInitializer implements CommandLineRunner {
|
|||||||
chat.setChannel(true);
|
chat.setChannel(true);
|
||||||
chat.setName("吹水群");
|
chat.setName("吹水群");
|
||||||
chat.setDescription("吹水聊天");
|
chat.setDescription("吹水聊天");
|
||||||
chat.setAvatar("https://openisle-1307107697.cos.accelerate.myqcloud.com/dynamic_assert/32647273e2334d14adfd4a6ce9db0643.jpeg");
|
chat.setAvatar(
|
||||||
|
"https://openisle-1307107697.cos.accelerate.myqcloud.com/dynamic_assert/32647273e2334d14adfd4a6ce9db0643.jpeg"
|
||||||
|
);
|
||||||
conversationRepository.save(chat);
|
conversationRepository.save(chat);
|
||||||
|
|
||||||
MessageConversation tech = new MessageConversation();
|
MessageConversation tech = new MessageConversation();
|
||||||
tech.setChannel(true);
|
tech.setChannel(true);
|
||||||
tech.setName("技术讨论群");
|
tech.setName("技术讨论群");
|
||||||
tech.setDescription("讨论技术相关话题");
|
tech.setDescription("讨论技术相关话题");
|
||||||
tech.setAvatar("https://openisle-1307107697.cos.accelerate.myqcloud.com/dynamic_assert/5edde9a5864e471caa32491dbcdaa8b2.png");
|
tech.setAvatar(
|
||||||
|
"https://openisle-1307107697.cos.accelerate.myqcloud.com/dynamic_assert/5edde9a5864e471caa32491dbcdaa8b2.png"
|
||||||
|
);
|
||||||
conversationRepository.save(tech);
|
conversationRepository.save(tech);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,21 +3,23 @@ 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
|
@Override
|
||||||
public void handle(HttpServletRequest request,
|
public void handle(
|
||||||
|
HttpServletRequest request,
|
||||||
HttpServletResponse response,
|
HttpServletResponse response,
|
||||||
AccessDeniedException accessDeniedException) throws IOException, ServletException {
|
AccessDeniedException accessDeniedException
|
||||||
|
) throws IOException, ServletException {
|
||||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
response.setContentType("application/json");
|
response.setContentType("application/json");
|
||||||
response.getWriter().write("{\"error\": \"Unauthorized\"}");
|
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;
|
||||||
@@ -44,16 +43,15 @@ public class OpenApiConfig {
|
|||||||
.in(SecurityScheme.In.HEADER)
|
.in(SecurityScheme.In.HEADER)
|
||||||
.name(header);
|
.name(header);
|
||||||
|
|
||||||
List<Server> servers = springDocProperties.getServers().stream()
|
List<Server> servers = springDocProperties
|
||||||
|
.getServers()
|
||||||
|
.stream()
|
||||||
.map(s -> new Server().url(s.getUrl()).description(s.getDescription()))
|
.map(s -> new Server().url(s.getUrl()).description(s.getDescription()))
|
||||||
.collect(Collectors.toList());
|
.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)
|
|
||||||
.description(description)
|
|
||||||
.version(version))
|
|
||||||
.components(new Components().addSecuritySchemes("JWT", securityScheme))
|
.components(new Components().addSecuritySchemes("JWT", securityScheme))
|
||||||
.addSecurityItem(new SecurityRequirement().addList("JWT"));
|
.addSecurityItem(new SecurityRequirement().addList("JWT"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import org.springframework.stereotype.Component;
|
|||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class PointGoodInitializer implements CommandLineRunner {
|
public class PointGoodInitializer implements CommandLineRunner {
|
||||||
|
|
||||||
private final PointGoodRepository pointGoodRepository;
|
private final PointGoodRepository pointGoodRepository;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -18,13 +19,17 @@ public class PointGoodInitializer implements CommandLineRunner {
|
|||||||
PointGood g1 = new PointGood();
|
PointGood g1 = new PointGood();
|
||||||
g1.setName("GPT Plus 1 个月");
|
g1.setName("GPT Plus 1 个月");
|
||||||
g1.setCost(20000);
|
g1.setCost(20000);
|
||||||
g1.setImage("https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/icons/chatgpt.png");
|
g1.setImage(
|
||||||
|
"https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/icons/chatgpt.png"
|
||||||
|
);
|
||||||
pointGoodRepository.save(g1);
|
pointGoodRepository.save(g1);
|
||||||
|
|
||||||
PointGood g2 = new PointGood();
|
PointGood g2 = new PointGood();
|
||||||
g2.setName("奶茶");
|
g2.setName("奶茶");
|
||||||
g2.setCost(5000);
|
g2.setCost(5000);
|
||||||
g2.setImage("https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/icons/coffee.png");
|
g2.setImage(
|
||||||
|
"https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/icons/coffee.png"
|
||||||
|
);
|
||||||
pointGoodRepository.save(g2);
|
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,21 +11,16 @@ 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
|
||||||
@@ -71,7 +70,10 @@ public class RabbitMQConfig {
|
|||||||
* 创建所有分片绑定, 使用十六进制路由键 (notifications.shard.0 - notifications.shard.f)
|
* 创建所有分片绑定, 使用十六进制路由键 (notifications.shard.0 - notifications.shard.f)
|
||||||
*/
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
public List<Binding> shardedBindings(TopicExchange exchange, @Qualifier("shardedQueues") List<Queue> shardedQueues) {
|
public List<Binding> shardedBindings(
|
||||||
|
TopicExchange exchange,
|
||||||
|
@Qualifier("shardedQueues") List<Queue> shardedQueues
|
||||||
|
) {
|
||||||
log.info("开始创建分片绑定 Bean...");
|
log.info("开始创建分片绑定 Bean...");
|
||||||
List<Binding> bindings = new ArrayList<>();
|
List<Binding> bindings = new ArrayList<>();
|
||||||
if (shardedQueues != null) {
|
if (shardedQueues != null) {
|
||||||
@@ -108,7 +110,9 @@ public class RabbitMQConfig {
|
|||||||
public Jackson2JsonMessageConverter messageConverter() {
|
public Jackson2JsonMessageConverter messageConverter() {
|
||||||
ObjectMapper objectMapper = new ObjectMapper();
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
objectMapper.registerModule(new com.fasterxml.jackson.datatype.jsr310.JavaTimeModule());
|
objectMapper.registerModule(new com.fasterxml.jackson.datatype.jsr310.JavaTimeModule());
|
||||||
objectMapper.disable(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
objectMapper.disable(
|
||||||
|
com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS
|
||||||
|
);
|
||||||
return new Jackson2JsonMessageConverter(objectMapper);
|
return new Jackson2JsonMessageConverter(objectMapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,13 +133,15 @@ public class RabbitMQConfig {
|
|||||||
* 这样可以确保 RabbitAdmin 和所有 Bean 都已正确初始化
|
* 这样可以确保 RabbitAdmin 和所有 Bean 都已正确初始化
|
||||||
*/
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
@DependsOn({"rabbitAdmin", "shardedQueues", "exchange"})
|
@DependsOn({ "rabbitAdmin", "shardedQueues", "exchange" })
|
||||||
public CommandLineRunner queueDeclarationRunner(RabbitAdmin rabbitAdmin,
|
public CommandLineRunner queueDeclarationRunner(
|
||||||
|
RabbitAdmin rabbitAdmin,
|
||||||
@Qualifier("shardedQueues") List<Queue> shardedQueues,
|
@Qualifier("shardedQueues") List<Queue> shardedQueues,
|
||||||
TopicExchange exchange,
|
TopicExchange exchange,
|
||||||
Queue legacyQueue,
|
Queue legacyQueue,
|
||||||
@Qualifier("shardedBindings") List<Binding> shardedBindings,
|
@Qualifier("shardedBindings") List<Binding> shardedBindings,
|
||||||
Binding legacyBinding) {
|
Binding legacyBinding
|
||||||
|
) {
|
||||||
return args -> {
|
return args -> {
|
||||||
log.info("=== 开始主动声明 RabbitMQ 组件 ===");
|
log.info("=== 开始主动声明 RabbitMQ 组件 ===");
|
||||||
|
|
||||||
@@ -157,14 +163,21 @@ public class RabbitMQConfig {
|
|||||||
rabbitAdmin.declareQueue(queue);
|
rabbitAdmin.declareQueue(queue);
|
||||||
successCount++;
|
successCount++;
|
||||||
} catch (org.springframework.amqp.AmqpIOException e) {
|
} catch (org.springframework.amqp.AmqpIOException e) {
|
||||||
if (e.getMessage().contains("PRECONDITION_FAILED") && e.getMessage().contains("durable")) {
|
if (
|
||||||
|
e.getMessage().contains("PRECONDITION_FAILED") && e.getMessage().contains("durable")
|
||||||
|
) {
|
||||||
skippedCount++;
|
skippedCount++;
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("队列声明失败: {}, 错误: {}", queueName, e.getMessage());
|
log.error("队列声明失败: {}, 错误: {}", queueName, e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.info("分片队列处理完成: 成功 {}, 跳过 {}, 总数 {}", successCount, skippedCount, shardedQueues.size());
|
log.info(
|
||||||
|
"分片队列处理完成: 成功 {}, 跳过 {}, 总数 {}",
|
||||||
|
successCount,
|
||||||
|
skippedCount,
|
||||||
|
shardedQueues.size()
|
||||||
|
);
|
||||||
|
|
||||||
// 声明分片绑定
|
// 声明分片绑定
|
||||||
log.info("开始声明 {} 个分片绑定...", shardedBindings.size());
|
log.info("开始声明 {} 个分片绑定...", shardedBindings.size());
|
||||||
@@ -185,7 +198,9 @@ public class RabbitMQConfig {
|
|||||||
rabbitAdmin.declareBinding(legacyBinding);
|
rabbitAdmin.declareBinding(legacyBinding);
|
||||||
log.info("遗留队列和绑定就绪: {} (已存在或新创建)", QUEUE_NAME);
|
log.info("遗留队列和绑定就绪: {} (已存在或新创建)", QUEUE_NAME);
|
||||||
} catch (org.springframework.amqp.AmqpIOException e) {
|
} catch (org.springframework.amqp.AmqpIOException e) {
|
||||||
if (e.getMessage().contains("PRECONDITION_FAILED") && e.getMessage().contains("durable")) {
|
if (
|
||||||
|
e.getMessage().contains("PRECONDITION_FAILED") && e.getMessage().contains("durable")
|
||||||
|
) {
|
||||||
log.warn("遗留队列已存在但 durable 设置不匹配: {}, 保持现有队列", QUEUE_NAME);
|
log.warn("遗留队列已存在但 durable 设置不匹配: {}, 保持现有队列", QUEUE_NAME);
|
||||||
} else {
|
} else {
|
||||||
log.error("遗留队列声明失败: {}, 错误: {}", QUEUE_NAME, e.getMessage());
|
log.error("遗留队列声明失败: {}, 错误: {}", QUEUE_NAME, e.getMessage());
|
||||||
@@ -196,7 +211,6 @@ public class RabbitMQConfig {
|
|||||||
|
|
||||||
log.info("=== RabbitMQ 组件声明完成 ===");
|
log.info("=== RabbitMQ 组件声明完成 ===");
|
||||||
log.info("请检查 RabbitMQ 管理界面确认队列已正确创建");
|
log.info("请检查 RabbitMQ 管理界面确认队列已正确创建");
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("RabbitMQ 组件声明过程中发生严重错误", e);
|
log.error("RabbitMQ 组件声明过程中发生严重错误", e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ 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
|
@Bean
|
||||||
public TaskScheduler taskScheduler() {
|
public TaskScheduler taskScheduler() {
|
||||||
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
||||||
|
|||||||
@@ -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,28 +30,20 @@ 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 JwtService jwtService;
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
private final AccessDeniedHandler customAccessDeniedHandler;
|
private final AccessDeniedHandler customAccessDeniedHandler;
|
||||||
private final UserVisitService userVisitService;
|
private final UserVisitService userVisitService;
|
||||||
|
|
||||||
@Value("${app.website-url}")
|
@Value("${app.website-url}")
|
||||||
private String websiteUrl;
|
private String websiteUrl;
|
||||||
|
|
||||||
@@ -56,18 +56,26 @@ public class SecurityConfig {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public UserDetailsService userDetailsService() {
|
public UserDetailsService userDetailsService() {
|
||||||
return username -> userRepository.findByUsername(username)
|
return username ->
|
||||||
.<UserDetails>map(user -> org.springframework.security.core.userdetails.User
|
userRepository
|
||||||
.withUsername(user.getUsername())
|
.findByUsername(username)
|
||||||
|
.<UserDetails>map(user ->
|
||||||
|
org.springframework.security.core.userdetails.User.withUsername(user.getUsername())
|
||||||
.password(user.getPassword())
|
.password(user.getPassword())
|
||||||
.authorities(user.getRole().name())
|
.authorities(user.getRole().name())
|
||||||
.build())
|
.build()
|
||||||
|
)
|
||||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public AuthenticationManager authenticationManager(HttpSecurity http, PasswordEncoder passwordEncoder, UserDetailsService userDetailsService) throws Exception {
|
public AuthenticationManager authenticationManager(
|
||||||
return http.getSharedObject(AuthenticationManagerBuilder.class)
|
HttpSecurity http,
|
||||||
|
PasswordEncoder passwordEncoder,
|
||||||
|
UserDetailsService userDetailsService
|
||||||
|
) throws Exception {
|
||||||
|
return http
|
||||||
|
.getSharedObject(AuthenticationManagerBuilder.class)
|
||||||
.userDetailsService(userDetailsService)
|
.userDetailsService(userDetailsService)
|
||||||
.passwordEncoder(passwordEncoder)
|
.passwordEncoder(passwordEncoder)
|
||||||
.and()
|
.and()
|
||||||
@@ -77,7 +85,8 @@ public class SecurityConfig {
|
|||||||
@Bean
|
@Bean
|
||||||
public CorsConfigurationSource corsConfigurationSource() {
|
public CorsConfigurationSource corsConfigurationSource() {
|
||||||
CorsConfiguration cfg = new CorsConfiguration();
|
CorsConfiguration cfg = new CorsConfiguration();
|
||||||
cfg.setAllowedOrigins(List.of(
|
cfg.setAllowedOrigins(
|
||||||
|
List.of(
|
||||||
"http://127.0.0.1:8080",
|
"http://127.0.0.1:8080",
|
||||||
"http://127.0.0.1:8081",
|
"http://127.0.0.1:8081",
|
||||||
"http://127.0.0.1:8082",
|
"http://127.0.0.1:8082",
|
||||||
@@ -92,16 +101,17 @@ public class SecurityConfig {
|
|||||||
"http://localhost",
|
"http://localhost",
|
||||||
"http://30.211.97.238:3000",
|
"http://30.211.97.238:3000",
|
||||||
"http://30.211.97.238",
|
"http://30.211.97.238",
|
||||||
"http://192.168.7.98",
|
"http://192.168.7.90",
|
||||||
"http://192.168.7.98:3000",
|
"http://192.168.7.90:3000",
|
||||||
"https://petstore.swagger.io",
|
"https://petstore.swagger.io",
|
||||||
// 允许自建OpenAPI地址
|
// 允许自建OpenAPI地址
|
||||||
"https://docs.open-isle.com",
|
"https://docs.open-isle.com",
|
||||||
"https://www.docs.open-isle.com",
|
"https://www.docs.open-isle.com",
|
||||||
websiteUrl,
|
websiteUrl,
|
||||||
websiteUrl.replace("://www.", "://")
|
websiteUrl.replace("://www.", "://")
|
||||||
));
|
)
|
||||||
cfg.setAllowedMethods(List.of("GET","POST","PUT","DELETE","OPTIONS"));
|
);
|
||||||
|
cfg.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||||
cfg.setAllowedHeaders(List.of("*"));
|
cfg.setAllowedHeaders(List.of("*"));
|
||||||
cfg.setAllowCredentials(true);
|
cfg.setAllowCredentials(true);
|
||||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||||
@@ -111,43 +121,76 @@ public class SecurityConfig {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
http.csrf(csrf -> csrf.disable())
|
http
|
||||||
|
.csrf(csrf -> csrf.disable())
|
||||||
.cors(Customizer.withDefaults())
|
.cors(Customizer.withDefaults())
|
||||||
.headers(h -> h.frameOptions(f -> f.sameOrigin()))
|
.headers(h -> h.frameOptions(f -> f.sameOrigin()))
|
||||||
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
.exceptionHandling(eh -> eh.accessDeniedHandler(customAccessDeniedHandler))
|
.exceptionHandling(eh -> eh.accessDeniedHandler(customAccessDeniedHandler))
|
||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth ->
|
||||||
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
|
auth
|
||||||
.requestMatchers("/api/ws/**", "/api/sockjs/**").permitAll()
|
.requestMatchers(HttpMethod.OPTIONS, "/**")
|
||||||
.requestMatchers("/api/v3/api-docs/**").permitAll()
|
.permitAll()
|
||||||
.requestMatchers(HttpMethod.POST, "/api/auth/**").permitAll()
|
.requestMatchers("/api/ws/**", "/api/sockjs/**")
|
||||||
.requestMatchers(HttpMethod.GET, "/api/posts/**").permitAll()
|
.permitAll()
|
||||||
.requestMatchers(HttpMethod.GET, "/api/comments/**").permitAll()
|
.requestMatchers("/api/v3/api-docs/**")
|
||||||
.requestMatchers(HttpMethod.GET, "/api/categories/**").permitAll()
|
.permitAll()
|
||||||
.requestMatchers(HttpMethod.GET, "/api/tags/**").permitAll()
|
.requestMatchers(HttpMethod.POST, "/api/auth/**")
|
||||||
.requestMatchers(HttpMethod.GET, "/api/config/**").permitAll()
|
.permitAll()
|
||||||
.requestMatchers(HttpMethod.POST,"/api/auth/google").permitAll()
|
.requestMatchers(HttpMethod.GET, "/api/posts/**")
|
||||||
.requestMatchers(HttpMethod.POST,"/api/auth/reason").permitAll()
|
.permitAll()
|
||||||
.requestMatchers(HttpMethod.GET, "/api/search/**").permitAll()
|
.requestMatchers(HttpMethod.GET, "/api/comments/**")
|
||||||
.requestMatchers(HttpMethod.GET, "/api/users/**").permitAll()
|
.permitAll()
|
||||||
.requestMatchers(HttpMethod.GET, "/api/medals/**").permitAll()
|
.requestMatchers(HttpMethod.GET, "/api/categories/**")
|
||||||
.requestMatchers(HttpMethod.GET, "/api/push/public-key").permitAll()
|
.permitAll()
|
||||||
.requestMatchers(HttpMethod.GET, "/api/reaction-types").permitAll()
|
.requestMatchers(HttpMethod.GET, "/api/tags/**")
|
||||||
.requestMatchers(HttpMethod.GET, "/api/activities/**").permitAll()
|
.permitAll()
|
||||||
.requestMatchers(HttpMethod.GET, "/api/sitemap.xml").permitAll()
|
.requestMatchers(HttpMethod.GET, "/api/config/**")
|
||||||
.requestMatchers(HttpMethod.GET, "/api/channels").permitAll()
|
.permitAll()
|
||||||
.requestMatchers(HttpMethod.GET, "/api/rss").permitAll()
|
.requestMatchers(HttpMethod.POST, "/api/auth/google")
|
||||||
.requestMatchers(HttpMethod.GET, "/api/online/**").permitAll()
|
.permitAll()
|
||||||
.requestMatchers(HttpMethod.POST, "/api/online/**").permitAll()
|
.requestMatchers(HttpMethod.POST, "/api/auth/reason")
|
||||||
.requestMatchers(HttpMethod.GET, "/api/point-goods").permitAll()
|
.permitAll()
|
||||||
.requestMatchers(HttpMethod.POST, "/api/point-goods").permitAll()
|
.requestMatchers(HttpMethod.GET, "/api/search/**")
|
||||||
.requestMatchers(HttpMethod.POST, "/api/categories/**").hasAuthority("ADMIN")
|
.permitAll()
|
||||||
.requestMatchers(HttpMethod.POST, "/api/tags/**").authenticated()
|
.requestMatchers(HttpMethod.GET, "/api/users/**")
|
||||||
.requestMatchers(HttpMethod.DELETE, "/api/categories/**").hasAuthority("ADMIN")
|
.permitAll()
|
||||||
.requestMatchers(HttpMethod.DELETE, "/api/tags/**").hasAuthority("ADMIN")
|
.requestMatchers(HttpMethod.GET, "/api/medals/**")
|
||||||
.requestMatchers(HttpMethod.GET, "/api/stats/**").hasAuthority("ADMIN")
|
.permitAll()
|
||||||
.requestMatchers("/api/admin/**").hasAuthority("ADMIN")
|
.requestMatchers(HttpMethod.GET, "/api/push/public-key")
|
||||||
.anyRequest().authenticated()
|
.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)
|
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
|
||||||
.addFilterAfter(userVisitFilter(), UsernamePasswordAuthenticationFilter.class);
|
.addFilterAfter(userVisitFilter(), UsernamePasswordAuthenticationFilter.class);
|
||||||
@@ -158,7 +201,11 @@ public class SecurityConfig {
|
|||||||
public OncePerRequestFilter jwtAuthenticationFilter() {
|
public OncePerRequestFilter jwtAuthenticationFilter() {
|
||||||
return new OncePerRequestFilter() {
|
return new OncePerRequestFilter() {
|
||||||
@Override
|
@Override
|
||||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
protected void doFilterInternal(
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
FilterChain filterChain
|
||||||
|
) throws ServletException, IOException {
|
||||||
// 让预检请求直接通过
|
// 让预检请求直接通过
|
||||||
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
|
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
@@ -167,14 +214,22 @@ public class SecurityConfig {
|
|||||||
String authHeader = request.getHeader("Authorization");
|
String authHeader = request.getHeader("Authorization");
|
||||||
String uri = request.getRequestURI();
|
String uri = request.getRequestURI();
|
||||||
|
|
||||||
boolean publicGet = "GET".equalsIgnoreCase(request.getMethod()) &&
|
boolean publicGet =
|
||||||
(uri.startsWith("/api/posts") || uri.startsWith("/api/comments") ||
|
"GET".equalsIgnoreCase(request.getMethod()) &&
|
||||||
uri.startsWith("/api/categories") || uri.startsWith("/api/tags") ||
|
(uri.startsWith("/api/posts") ||
|
||||||
uri.startsWith("/api/search") || uri.startsWith("/api/users") ||
|
uri.startsWith("/api/comments") ||
|
||||||
uri.startsWith("/api/reaction-types") || uri.startsWith("/api/config") ||
|
uri.startsWith("/api/categories") ||
|
||||||
uri.startsWith("/api/activities") || uri.startsWith("/api/push/public-key") ||
|
uri.startsWith("/api/tags") ||
|
||||||
uri.startsWith("/api/point-goods") || uri.startsWith("/api/channels") ||
|
uri.startsWith("/api/search") ||
|
||||||
uri.startsWith("/api/sitemap.xml") || uri.startsWith("/api/medals") ||
|
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("/api/rss"));
|
uri.startsWith("/api/rss"));
|
||||||
|
|
||||||
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
||||||
@@ -183,18 +238,27 @@ public class SecurityConfig {
|
|||||||
String username = jwtService.validateAndGetSubject(token);
|
String username = jwtService.validateAndGetSubject(token);
|
||||||
UserDetails userDetails = userDetailsService().loadUserByUsername(username);
|
UserDetails userDetails = userDetailsService().loadUserByUsername(username);
|
||||||
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
|
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
|
||||||
userDetails, null, userDetails.getAuthorities());
|
userDetails,
|
||||||
org.springframework.security.core.context.SecurityContextHolder.getContext().setAuthentication(authToken);
|
null,
|
||||||
|
userDetails.getAuthorities()
|
||||||
|
);
|
||||||
|
org.springframework.security.core.context.SecurityContextHolder.getContext().setAuthentication(
|
||||||
|
authToken
|
||||||
|
);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
response.setContentType("application/json");
|
response.setContentType("application/json");
|
||||||
response.getWriter().write("{\"error\": \"Invalid or expired token\"}");
|
response.getWriter().write("{\"error\": \"Invalid or expired token\"}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (!uri.startsWith("/api/auth") && !publicGet
|
} else if (
|
||||||
&& !uri.startsWith("/api/ws") && !uri.startsWith("/api/sockjs")
|
!uri.startsWith("/api/auth") &&
|
||||||
&& !uri.startsWith("/api/v3/api-docs")
|
!publicGet &&
|
||||||
&& !uri.startsWith("/api/online")) {
|
!uri.startsWith("/api/ws") &&
|
||||||
|
!uri.startsWith("/api/sockjs") &&
|
||||||
|
!uri.startsWith("/api/v3/api-docs") &&
|
||||||
|
!uri.startsWith("/api/online")
|
||||||
|
) {
|
||||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
response.setContentType("application/json");
|
response.setContentType("application/json");
|
||||||
response.getWriter().write("{\"error\": \"Missing token\"}");
|
response.getWriter().write("{\"error\": \"Missing token\"}");
|
||||||
@@ -210,10 +274,20 @@ public class SecurityConfig {
|
|||||||
public OncePerRequestFilter userVisitFilter() {
|
public OncePerRequestFilter userVisitFilter() {
|
||||||
return new OncePerRequestFilter() {
|
return new OncePerRequestFilter() {
|
||||||
@Override
|
@Override
|
||||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
protected void doFilterInternal(
|
||||||
var auth = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication();
|
HttpServletRequest request,
|
||||||
if (auth != null && auth.isAuthenticated() && !(auth instanceof org.springframework.security.authentication.AnonymousAuthenticationToken)) {
|
HttpServletResponse response,
|
||||||
String key = CachingConfig.VISIT_CACHE_NAME+":"+ LocalDate.now();
|
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());
|
redisTemplate.opsForSet().add(key, auth.getName());
|
||||||
}
|
}
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import lombok.NoArgsConstructor;
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
public class ShardInfo {
|
public class ShardInfo {
|
||||||
|
|
||||||
private int shardIndex;
|
private int shardIndex;
|
||||||
private String queueName;
|
private String queueName;
|
||||||
private String routingKey;
|
private String routingKey;
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
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 {
|
||||||
@@ -38,8 +37,13 @@ public class ShardingStrategy {
|
|||||||
int shard = getShardFromHexChar(firstChar);
|
int shard = getShardFromHexChar(firstChar);
|
||||||
recordShardUsage(shard);
|
recordShardUsage(shard);
|
||||||
|
|
||||||
log.debug("Username '{}' -> hash '{}' -> firstChar '{}' -> shard {}",
|
log.debug(
|
||||||
username, hash, firstChar, shard);
|
"Username '{}' -> hash '{}' -> firstChar '{}' -> shard {}",
|
||||||
|
username,
|
||||||
|
hash,
|
||||||
|
firstChar,
|
||||||
|
shard
|
||||||
|
);
|
||||||
|
|
||||||
return getShardInfoByIndex(shard);
|
return getShardInfoByIndex(shard);
|
||||||
}
|
}
|
||||||
@@ -80,5 +84,4 @@ public class ShardingStrategy {
|
|||||||
private void recordShardUsage(int shard) {
|
private void recordShardUsage(int shard) {
|
||||||
shardCounts.computeIfAbsent(shard, k -> new AtomicLong(0)).incrementAndGet();
|
shardCounts.computeIfAbsent(shard, k -> new AtomicLong(0)).incrementAndGet();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -10,10 +10,12 @@ 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<>();
|
private List<ServerConfig> servers = new ArrayList<>();
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class ServerConfig {
|
public static class ServerConfig {
|
||||||
|
|
||||||
private String url;
|
private String url;
|
||||||
private String description;
|
private String description;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,12 +14,15 @@ 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 UserRepository userRepository;
|
||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run(String... args) {
|
public void run(String... args) {
|
||||||
userRepository.findByUsername("system").orElseGet(() -> {
|
userRepository
|
||||||
|
.findByUsername("system")
|
||||||
|
.orElseGet(() -> {
|
||||||
User system = new User();
|
User system = new User();
|
||||||
system.setUsername("system");
|
system.setUsername("system");
|
||||||
system.setEmail("system@openisle.local");
|
system.setEmail("system@openisle.local");
|
||||||
@@ -28,9 +31,10 @@ public class SystemUserInitializer implements CommandLineRunner {
|
|||||||
system.setRole(Role.USER);
|
system.setRole(Role.USER);
|
||||||
system.setVerified(true);
|
system.setVerified(true);
|
||||||
system.setApproved(true);
|
system.setApproved(true);
|
||||||
system.setAvatar("https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/image.png");
|
system.setAvatar(
|
||||||
|
"https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/image.png"
|
||||||
|
);
|
||||||
return userRepository.save(system);
|
return userRepository.save(system);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,41 +9,45 @@ 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 ActivityService activityService;
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
private final ActivityMapper activityMapper;
|
private final ActivityMapper activityMapper;
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@Operation(summary = "List activities", description = "Retrieve all activities")
|
@Operation(summary = "List activities", description = "Retrieve all activities")
|
||||||
@ApiResponse(responseCode = "200", description = "List of activities",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ActivityDto.class))))
|
responseCode = "200",
|
||||||
|
description = "List of activities",
|
||||||
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ActivityDto.class)))
|
||||||
|
)
|
||||||
public List<ActivityDto> list() {
|
public List<ActivityDto> list() {
|
||||||
return activityService.list().stream()
|
return activityService.list().stream().map(activityMapper::toDto).collect(Collectors.toList());
|
||||||
.map(activityMapper::toDto)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/milk-tea")
|
@GetMapping("/milk-tea")
|
||||||
@Operation(summary = "Milk tea info", description = "Get milk tea activity information")
|
@Operation(summary = "Milk tea info", description = "Get milk tea activity information")
|
||||||
@ApiResponse(responseCode = "200", description = "Milk tea info",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = MilkTeaInfoDto.class)))
|
responseCode = "200",
|
||||||
|
description = "Milk tea info",
|
||||||
|
content = @Content(schema = @Schema(implementation = MilkTeaInfoDto.class))
|
||||||
|
)
|
||||||
public MilkTeaInfoDto milkTea() {
|
public MilkTeaInfoDto milkTea() {
|
||||||
Activity a = activityService.getByType(ActivityType.MILK_TEA);
|
Activity a = activityService.getByType(ActivityType.MILK_TEA);
|
||||||
long count = activityService.countParticipants(a);
|
long count = activityService.countParticipants(a);
|
||||||
@@ -58,10 +62,16 @@ public class ActivityController {
|
|||||||
|
|
||||||
@PostMapping("/milk-tea/redeem")
|
@PostMapping("/milk-tea/redeem")
|
||||||
@Operation(summary = "Redeem milk tea", description = "Redeem milk tea activity reward")
|
@Operation(summary = "Redeem milk tea", description = "Redeem milk tea activity reward")
|
||||||
@ApiResponse(responseCode = "200", description = "Redeem result",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = java.util.Map.class)))
|
responseCode = "200",
|
||||||
|
description = "Redeem result",
|
||||||
|
content = @Content(schema = @Schema(implementation = java.util.Map.class))
|
||||||
|
)
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public java.util.Map<String, String> redeemMilkTea(@RequestBody MilkTeaRedeemRequest req, Authentication auth) {
|
public java.util.Map<String, String> redeemMilkTea(
|
||||||
|
@RequestBody MilkTeaRedeemRequest req,
|
||||||
|
Authentication auth
|
||||||
|
) {
|
||||||
User user = userService.findByIdentifier(auth.getName()).orElseThrow();
|
User user = userService.findByIdentifier(auth.getName()).orElseThrow();
|
||||||
Activity a = activityService.getByType(ActivityType.MILK_TEA);
|
Activity a = activityService.getByType(ActivityType.MILK_TEA);
|
||||||
boolean first = activityService.redeem(a, user, req.getContact());
|
boolean first = activityService.redeem(a, user, req.getContact());
|
||||||
|
|||||||
@@ -19,14 +19,18 @@ 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 CommentService commentService;
|
||||||
private final CommentMapper commentMapper;
|
private final CommentMapper commentMapper;
|
||||||
|
|
||||||
@PostMapping("/{id}/pin")
|
@PostMapping("/{id}/pin")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "Pin comment", description = "Pin a comment by its id")
|
@Operation(summary = "Pin comment", description = "Pin a comment by its id")
|
||||||
@ApiResponse(responseCode = "200", description = "Pinned comment",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = CommentDto.class)))
|
responseCode = "200",
|
||||||
|
description = "Pinned comment",
|
||||||
|
content = @Content(schema = @Schema(implementation = CommentDto.class))
|
||||||
|
)
|
||||||
public CommentDto pin(@PathVariable Long id, Authentication auth) {
|
public CommentDto pin(@PathVariable Long id, Authentication auth) {
|
||||||
return commentMapper.toDto(commentService.pinComment(auth.getName(), id));
|
return commentMapper.toDto(commentService.pinComment(auth.getName(), id));
|
||||||
}
|
}
|
||||||
@@ -34,8 +38,11 @@ public class AdminCommentController {
|
|||||||
@PostMapping("/{id}/unpin")
|
@PostMapping("/{id}/unpin")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "Unpin comment", description = "Remove pin from a comment")
|
@Operation(summary = "Unpin comment", description = "Remove pin from a comment")
|
||||||
@ApiResponse(responseCode = "200", description = "Unpinned comment",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = CommentDto.class)))
|
responseCode = "200",
|
||||||
|
description = "Unpinned comment",
|
||||||
|
content = @Content(schema = @Schema(implementation = CommentDto.class))
|
||||||
|
)
|
||||||
public CommentDto unpin(@PathVariable Long id, Authentication auth) {
|
public CommentDto unpin(@PathVariable Long id, Authentication auth) {
|
||||||
return commentMapper.toDto(commentService.unpinComment(auth.getName(), id));
|
return commentMapper.toDto(commentService.unpinComment(auth.getName(), id));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ 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 PostService postService;
|
||||||
private final PasswordValidator passwordValidator;
|
private final PasswordValidator passwordValidator;
|
||||||
private final AiUsageService aiUsageService;
|
private final AiUsageService aiUsageService;
|
||||||
@@ -24,9 +25,15 @@ public class AdminConfigController {
|
|||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "Get configuration", description = "Retrieve application configuration settings")
|
@Operation(
|
||||||
@ApiResponse(responseCode = "200", description = "Current configuration",
|
summary = "Get configuration",
|
||||||
content = @Content(schema = @Schema(implementation = ConfigDto.class)))
|
description = "Retrieve application configuration settings"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Current configuration",
|
||||||
|
content = @Content(schema = @Schema(implementation = ConfigDto.class))
|
||||||
|
)
|
||||||
public ConfigDto getConfig() {
|
public ConfigDto getConfig() {
|
||||||
ConfigDto dto = new ConfigDto();
|
ConfigDto dto = new ConfigDto();
|
||||||
dto.setPublishMode(postService.getPublishMode());
|
dto.setPublishMode(postService.getPublishMode());
|
||||||
@@ -38,9 +45,15 @@ public class AdminConfigController {
|
|||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "Update configuration", description = "Update application configuration settings")
|
@Operation(
|
||||||
@ApiResponse(responseCode = "200", description = "Updated configuration",
|
summary = "Update configuration",
|
||||||
content = @Content(schema = @Schema(implementation = ConfigDto.class)))
|
description = "Update application configuration settings"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Updated configuration",
|
||||||
|
content = @Content(schema = @Schema(implementation = ConfigDto.class))
|
||||||
|
)
|
||||||
public ConfigDto updateConfig(@RequestBody ConfigDto dto) {
|
public ConfigDto updateConfig(@RequestBody ConfigDto dto) {
|
||||||
if (dto.getPublishMode() != null) {
|
if (dto.getPublishMode() != null) {
|
||||||
postService.setPublishMode(dto.getPublishMode());
|
postService.setPublishMode(dto.getPublishMode());
|
||||||
@@ -56,5 +69,4 @@ public class AdminConfigController {
|
|||||||
}
|
}
|
||||||
return getConfig();
|
return getConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,20 +5,24 @@ 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")
|
@GetMapping("/api/admin/hello")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "Admin greeting", description = "Returns a greeting for admin users")
|
@Operation(summary = "Admin greeting", description = "Returns a greeting for admin users")
|
||||||
@ApiResponse(responseCode = "200", description = "Greeting payload",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = Map.class)))
|
responseCode = "200",
|
||||||
|
description = "Greeting payload",
|
||||||
|
content = @Content(schema = @Schema(implementation = Map.class))
|
||||||
|
)
|
||||||
public Map<String, String> adminHello() {
|
public Map<String, String> adminHello() {
|
||||||
return Map.of("message", "Hello, Admin User");
|
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,16 +21,24 @@ 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 PostService postService;
|
||||||
private final PostMapper postMapper;
|
private final PostMapper postMapper;
|
||||||
|
|
||||||
@GetMapping("/pending")
|
@GetMapping("/pending")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "List pending posts", description = "Retrieve posts awaiting approval")
|
@Operation(summary = "List pending posts", description = "Retrieve posts awaiting approval")
|
||||||
@ApiResponse(responseCode = "200", description = "Pending posts",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
|
responseCode = "200",
|
||||||
|
description = "Pending posts",
|
||||||
|
content = @Content(
|
||||||
|
array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))
|
||||||
|
)
|
||||||
|
)
|
||||||
public List<PostSummaryDto> pendingPosts() {
|
public List<PostSummaryDto> pendingPosts() {
|
||||||
return postService.listPendingPosts().stream()
|
return postService
|
||||||
|
.listPendingPosts()
|
||||||
|
.stream()
|
||||||
.map(postMapper::toSummaryDto)
|
.map(postMapper::toSummaryDto)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
@@ -39,8 +46,11 @@ public class AdminPostController {
|
|||||||
@PostMapping("/{id}/approve")
|
@PostMapping("/{id}/approve")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "Approve post", description = "Approve a pending post")
|
@Operation(summary = "Approve post", description = "Approve a pending post")
|
||||||
@ApiResponse(responseCode = "200", description = "Approved post",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = PostSummaryDto.class)))
|
responseCode = "200",
|
||||||
|
description = "Approved post",
|
||||||
|
content = @Content(schema = @Schema(implementation = PostSummaryDto.class))
|
||||||
|
)
|
||||||
public PostSummaryDto approve(@PathVariable Long id) {
|
public PostSummaryDto approve(@PathVariable Long id) {
|
||||||
return postMapper.toSummaryDto(postService.approvePost(id));
|
return postMapper.toSummaryDto(postService.approvePost(id));
|
||||||
}
|
}
|
||||||
@@ -48,8 +58,11 @@ public class AdminPostController {
|
|||||||
@PostMapping("/{id}/reject")
|
@PostMapping("/{id}/reject")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "Reject post", description = "Reject a pending post")
|
@Operation(summary = "Reject post", description = "Reject a pending post")
|
||||||
@ApiResponse(responseCode = "200", description = "Rejected post",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = PostSummaryDto.class)))
|
responseCode = "200",
|
||||||
|
description = "Rejected post",
|
||||||
|
content = @Content(schema = @Schema(implementation = PostSummaryDto.class))
|
||||||
|
)
|
||||||
public PostSummaryDto reject(@PathVariable Long id) {
|
public PostSummaryDto reject(@PathVariable Long id) {
|
||||||
return postMapper.toSummaryDto(postService.rejectPost(id));
|
return postMapper.toSummaryDto(postService.rejectPost(id));
|
||||||
}
|
}
|
||||||
@@ -57,36 +70,60 @@ public class AdminPostController {
|
|||||||
@PostMapping("/{id}/pin")
|
@PostMapping("/{id}/pin")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "Pin post", description = "Pin a post to the top")
|
@Operation(summary = "Pin post", description = "Pin a post to the top")
|
||||||
@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 = "Pinned post",
|
||||||
|
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()));
|
return postMapper.toSummaryDto(postService.pinPost(id, auth.getName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/{id}/unpin")
|
@PostMapping("/{id}/unpin")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "Unpin post", description = "Remove a post from the top")
|
@Operation(summary = "Unpin post", description = "Remove a post from 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 = "Unpinned post",
|
||||||
|
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()));
|
return postMapper.toSummaryDto(postService.unpinPost(id, auth.getName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/{id}/rss-exclude")
|
@PostMapping("/{id}/rss-exclude")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "Exclude from RSS", description = "Exclude a post from 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 excludeFromRss(@PathVariable Long id, org.springframework.security.core.Authentication auth) {
|
description = "Updated post",
|
||||||
|
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()));
|
return postMapper.toSummaryDto(postService.excludeFromRss(id, auth.getName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/{id}/rss-include")
|
@PostMapping("/{id}/rss-include")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "Include in RSS", description = "Include a post in the RSS feed")
|
@Operation(summary = "Include in RSS", description = "Include a post in the 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",
|
||||||
|
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()));
|
return postMapper.toSummaryDto(postService.includeInRss(id, auth.getName()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,16 +11,16 @@ 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 TagService tagService;
|
||||||
private final PostService postService;
|
private final PostService postService;
|
||||||
private final TagMapper tagMapper;
|
private final TagMapper tagMapper;
|
||||||
@@ -28,10 +28,15 @@ public class AdminTagController {
|
|||||||
@GetMapping("/pending")
|
@GetMapping("/pending")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "List pending tags", description = "Retrieve tags awaiting approval")
|
@Operation(summary = "List pending tags", description = "Retrieve tags awaiting approval")
|
||||||
@ApiResponse(responseCode = "200", description = "Pending tags",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TagDto.class))))
|
responseCode = "200",
|
||||||
|
description = "Pending tags",
|
||||||
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TagDto.class)))
|
||||||
|
)
|
||||||
public List<TagDto> pendingTags() {
|
public List<TagDto> pendingTags() {
|
||||||
return tagService.listPendingTags().stream()
|
return tagService
|
||||||
|
.listPendingTags()
|
||||||
|
.stream()
|
||||||
.map(t -> tagMapper.toDto(t, postService.countPostsByTag(t.getId())))
|
.map(t -> tagMapper.toDto(t, postService.countPostsByTag(t.getId())))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
@@ -39,8 +44,11 @@ public class AdminTagController {
|
|||||||
@PostMapping("/{id}/approve")
|
@PostMapping("/{id}/approve")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "Approve tag", description = "Approve a pending tag")
|
@Operation(summary = "Approve tag", description = "Approve a pending tag")
|
||||||
@ApiResponse(responseCode = "200", description = "Approved tag",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = TagDto.class)))
|
responseCode = "200",
|
||||||
|
description = "Approved tag",
|
||||||
|
content = @Content(schema = @Schema(implementation = TagDto.class))
|
||||||
|
)
|
||||||
public TagDto approve(@PathVariable Long id) {
|
public TagDto approve(@PathVariable Long id) {
|
||||||
Tag tag = tagService.approveTag(id);
|
Tag tag = tagService.approveTag(id);
|
||||||
long count = postService.countPostsByTag(tag.getId());
|
long count = postService.countPostsByTag(tag.getId());
|
||||||
|
|||||||
@@ -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,9 +18,11 @@ 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 UserRepository userRepository;
|
||||||
private final NotificationRepository notificationRepository;
|
private final NotificationRepository notificationRepository;
|
||||||
private final EmailSender emailSender;
|
private final EmailSender emailSender;
|
||||||
|
|
||||||
@Value("${app.website-url}")
|
@Value("${app.website-url}")
|
||||||
private String websiteUrl;
|
private String websiteUrl;
|
||||||
|
|
||||||
@@ -33,8 +35,11 @@ public class AdminUserController {
|
|||||||
user.setApproved(true);
|
user.setApproved(true);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
markRegisterRequestNotificationsRead(user);
|
markRegisterRequestNotificationsRead(user);
|
||||||
emailSender.sendEmail(user.getEmail(), "您的注册已审核通过",
|
emailSender.sendEmail(
|
||||||
"🎉您的注册已经审核通过, 点击以访问网站: " + websiteUrl);
|
user.getEmail(),
|
||||||
|
"您的注册已审核通过",
|
||||||
|
"🎉您的注册已经审核通过, 点击以访问网站: " + websiteUrl
|
||||||
|
);
|
||||||
return ResponseEntity.ok().build();
|
return ResponseEntity.ok().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,14 +52,19 @@ public class AdminUserController {
|
|||||||
user.setApproved(false);
|
user.setApproved(false);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
markRegisterRequestNotificationsRead(user);
|
markRegisterRequestNotificationsRead(user);
|
||||||
emailSender.sendEmail(user.getEmail(), "您的注册已被管理员拒绝",
|
emailSender.sendEmail(
|
||||||
"您的注册被管理员拒绝, 点击链接可以重新填写理由申请: " + websiteUrl);
|
user.getEmail(),
|
||||||
|
"您的注册已被管理员拒绝",
|
||||||
|
"您的注册被管理员拒绝, 点击链接可以重新填写理由申请: " + websiteUrl
|
||||||
|
);
|
||||||
return ResponseEntity.ok().build();
|
return ResponseEntity.ok().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void markRegisterRequestNotificationsRead(User applicant) {
|
private void markRegisterRequestNotificationsRead(User applicant) {
|
||||||
java.util.List<Notification> notifs =
|
java.util.List<Notification> notifs = notificationRepository.findByTypeAndFromUser(
|
||||||
notificationRepository.findByTypeAndFromUser(NotificationType.REGISTER_REQUEST, applicant);
|
NotificationType.REGISTER_REQUEST,
|
||||||
|
applicant
|
||||||
|
);
|
||||||
for (Notification n : notifs) {
|
for (Notification n : notifs) {
|
||||||
n.setRead(true);
|
n.setRead(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,13 +15,6 @@ 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")
|
||||||
@@ -27,11 +26,16 @@ public class AiController {
|
|||||||
|
|
||||||
@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",
|
||||||
|
description = "Formatted content",
|
||||||
|
content = @Content(schema = @Schema(implementation = Map.class))
|
||||||
|
)
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public ResponseEntity<Map<String, String>> format(@RequestBody Map<String, String> req,
|
public ResponseEntity<Map<String, String>> format(
|
||||||
Authentication auth) {
|
@RequestBody Map<String, String> req,
|
||||||
|
Authentication auth
|
||||||
|
) {
|
||||||
String text = req.get("text");
|
String text = req.get("text");
|
||||||
if (text == null) {
|
if (text == null) {
|
||||||
return ResponseEntity.badRequest().build();
|
return ResponseEntity.badRequest().build();
|
||||||
@@ -42,7 +46,8 @@ public class AiController {
|
|||||||
return ResponseEntity.status(429).build();
|
return ResponseEntity.status(429).build();
|
||||||
}
|
}
|
||||||
aiUsageService.incrementAndGetCount(auth.getName());
|
aiUsageService.incrementAndGetCount(auth.getName());
|
||||||
return openAiService.formatMarkdown(text)
|
return openAiService
|
||||||
|
.formatMarkdown(text)
|
||||||
.map(t -> ResponseEntity.ok(Map.of("content", t)))
|
.map(t -> ResponseEntity.ok(Map.of("content", t)))
|
||||||
.orElse(ResponseEntity.status(500).build());
|
.orElse(ResponseEntity.status(500).build());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,20 +13,20 @@ 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.Optional;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/auth")
|
@RequestMapping("/api/auth")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AuthController {
|
public class AuthController {
|
||||||
|
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
private final JwtService jwtService;
|
private final JwtService jwtService;
|
||||||
private final EmailSender emailService;
|
private final EmailSender emailService;
|
||||||
@@ -41,7 +41,6 @@ public class AuthController {
|
|||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
private final InviteService inviteService;
|
private final InviteService inviteService;
|
||||||
|
|
||||||
|
|
||||||
@Value("${app.captcha.enabled:false}")
|
@Value("${app.captcha.enabled:false}")
|
||||||
private boolean captchaEnabled;
|
private boolean captchaEnabled;
|
||||||
|
|
||||||
@@ -53,8 +52,11 @@ public class AuthController {
|
|||||||
|
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
@Operation(summary = "Register user", description = "Register a new user account")
|
@Operation(summary = "Register user", description = "Register a new user account")
|
||||||
@ApiResponse(responseCode = "200", description = "Registration result",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = Map.class)))
|
responseCode = "200",
|
||||||
|
description = "Registration result",
|
||||||
|
content = @Content(schema = @Schema(implementation = Map.class))
|
||||||
|
)
|
||||||
public ResponseEntity<?> register(@RequestBody RegisterRequest req) {
|
public ResponseEntity<?> register(@RequestBody RegisterRequest req) {
|
||||||
if (captchaEnabled && registerCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
|
if (captchaEnabled && registerCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", "Invalid captcha"));
|
return ResponseEntity.badRequest().body(Map.of("error", "Invalid captcha"));
|
||||||
@@ -66,23 +68,34 @@ public class AuthController {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
User user = userService.registerWithInvite(
|
User user = userService.registerWithInvite(
|
||||||
req.getUsername(), req.getEmail(), req.getPassword());
|
req.getUsername(),
|
||||||
|
req.getEmail(),
|
||||||
|
req.getPassword()
|
||||||
|
);
|
||||||
inviteService.consume(req.getInviteToken(), user.getUsername());
|
inviteService.consume(req.getInviteToken(), user.getUsername());
|
||||||
// 发送确认邮件
|
// 发送确认邮件
|
||||||
userService.sendVerifyMail(user, VerifyType.REGISTER);
|
userService.sendVerifyMail(user, VerifyType.REGISTER);
|
||||||
return ResponseEntity.ok(Map.of(
|
return ResponseEntity.ok(
|
||||||
"token", jwtService.generateToken(user.getUsername()),
|
Map.of(
|
||||||
"reason_code", "INVITE_APPROVED"
|
"token",
|
||||||
));
|
jwtService.generateToken(user.getUsername()),
|
||||||
|
"reason_code",
|
||||||
|
"INVITE_APPROVED"
|
||||||
|
)
|
||||||
|
);
|
||||||
} catch (FieldException e) {
|
} catch (FieldException e) {
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(
|
||||||
"field", e.getField(),
|
Map.of("field", e.getField(), "error", e.getMessage())
|
||||||
"error", e.getMessage()
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
User user = userService.register(
|
User user = userService.register(
|
||||||
req.getUsername(), req.getEmail(), req.getPassword(), "", registerModeService.getRegisterMode());
|
req.getUsername(),
|
||||||
|
req.getEmail(),
|
||||||
|
req.getPassword(),
|
||||||
|
"",
|
||||||
|
registerModeService.getRegisterMode()
|
||||||
|
);
|
||||||
// 发送确认邮件
|
// 发送确认邮件
|
||||||
userService.sendVerifyMail(user, VerifyType.REGISTER);
|
userService.sendVerifyMail(user, VerifyType.REGISTER);
|
||||||
if (!user.isApproved()) {
|
if (!user.isApproved()) {
|
||||||
@@ -93,8 +106,11 @@ public class AuthController {
|
|||||||
|
|
||||||
@PostMapping("/verify")
|
@PostMapping("/verify")
|
||||||
@Operation(summary = "Verify account", description = "Verify registration code")
|
@Operation(summary = "Verify account", description = "Verify registration code")
|
||||||
@ApiResponse(responseCode = "200", description = "Verification result",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = Map.class)))
|
responseCode = "200",
|
||||||
|
description = "Verification result",
|
||||||
|
content = @Content(schema = @Schema(implementation = Map.class))
|
||||||
|
)
|
||||||
public ResponseEntity<?> verify(@RequestBody VerifyRequest req) {
|
public ResponseEntity<?> verify(@RequestBody VerifyRequest req) {
|
||||||
Optional<User> userOpt = userService.findByUsername(req.getUsername());
|
Optional<User> userOpt = userService.findByUsername(req.getUsername());
|
||||||
if (userOpt.isEmpty()) {
|
if (userOpt.isEmpty()) {
|
||||||
@@ -105,17 +121,27 @@ public class AuthController {
|
|||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
|
|
||||||
if (user.isApproved()) {
|
if (user.isApproved()) {
|
||||||
return ResponseEntity.ok(Map.of(
|
return ResponseEntity.ok(
|
||||||
"message", "Verified and isApproved",
|
Map.of(
|
||||||
"reason_code", "VERIFIED_AND_APPROVED",
|
"message",
|
||||||
"token", jwtService.generateToken(req.getUsername())
|
"Verified and isApproved",
|
||||||
));
|
"reason_code",
|
||||||
|
"VERIFIED_AND_APPROVED",
|
||||||
|
"token",
|
||||||
|
jwtService.generateToken(req.getUsername())
|
||||||
|
)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return ResponseEntity.ok(Map.of(
|
return ResponseEntity.ok(
|
||||||
"message", "Verified",
|
Map.of(
|
||||||
"reason_code", "VERIFIED",
|
"message",
|
||||||
"token", jwtService.generateReasonToken(req.getUsername())
|
"Verified",
|
||||||
));
|
"reason_code",
|
||||||
|
"VERIFIED",
|
||||||
|
"token",
|
||||||
|
jwtService.generateReasonToken(req.getUsername())
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", "Invalid verification code"));
|
return ResponseEntity.badRequest().body(Map.of("error", "Invalid verification code"));
|
||||||
@@ -123,8 +149,11 @@ public class AuthController {
|
|||||||
|
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
@Operation(summary = "Login", description = "Authenticate with username/email and password")
|
@Operation(summary = "Login", description = "Authenticate with username/email and password")
|
||||||
@ApiResponse(responseCode = "200", description = "Authentication result",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = Map.class)))
|
responseCode = "200",
|
||||||
|
description = "Authentication result",
|
||||||
|
content = @Content(schema = @Schema(implementation = Map.class))
|
||||||
|
)
|
||||||
public ResponseEntity<?> login(@RequestBody LoginRequest req) {
|
public ResponseEntity<?> login(@RequestBody LoginRequest req) {
|
||||||
if (captchaEnabled && loginCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
|
if (captchaEnabled && loginCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", "Invalid captcha"));
|
return ResponseEntity.badRequest().body(Map.of("error", "Invalid captcha"));
|
||||||
@@ -134,103 +163,154 @@ public class AuthController {
|
|||||||
userOpt = userService.findByEmail(req.getUsername());
|
userOpt = userService.findByEmail(req.getUsername());
|
||||||
}
|
}
|
||||||
if (userOpt.isEmpty() || !userService.matchesPassword(userOpt.get(), req.getPassword())) {
|
if (userOpt.isEmpty() || !userService.matchesPassword(userOpt.get(), req.getPassword())) {
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(
|
||||||
"error", "Invalid credentials",
|
Map.of("error", "Invalid credentials", "reason_code", "INVALID_CREDENTIALS")
|
||||||
"reason_code", "INVALID_CREDENTIALS"));
|
);
|
||||||
}
|
}
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
if (!user.isVerified()) {
|
if (!user.isVerified()) {
|
||||||
user = userService.register(user.getUsername(), user.getEmail(), user.getPassword(), user.getRegisterReason(), registerModeService.getRegisterMode());
|
user = userService.register(
|
||||||
|
user.getUsername(),
|
||||||
|
user.getEmail(),
|
||||||
|
user.getPassword(),
|
||||||
|
user.getRegisterReason(),
|
||||||
|
registerModeService.getRegisterMode()
|
||||||
|
);
|
||||||
userService.sendVerifyMail(user, VerifyType.REGISTER);
|
userService.sendVerifyMail(user, VerifyType.REGISTER);
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(
|
||||||
"error", "User not verified",
|
Map.of(
|
||||||
"reason_code", "NOT_VERIFIED",
|
"error",
|
||||||
"user_name", user.getUsername()));
|
"User not verified",
|
||||||
|
"reason_code",
|
||||||
|
"NOT_VERIFIED",
|
||||||
|
"user_name",
|
||||||
|
user.getUsername()
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (RegisterMode.WHITELIST.equals(registerModeService.getRegisterMode()) && !user.isApproved()) {
|
if (
|
||||||
|
RegisterMode.WHITELIST.equals(registerModeService.getRegisterMode()) && !user.isApproved()
|
||||||
|
) {
|
||||||
if (user.getRegisterReason() != null && !user.getRegisterReason().isEmpty()) {
|
if (user.getRegisterReason() != null && !user.getRegisterReason().isEmpty()) {
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(
|
||||||
"error", "Account awaiting approval",
|
Map.of("error", "Account awaiting approval", "reason_code", "IS_APPROVING")
|
||||||
"reason_code", "IS_APPROVING"
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(
|
||||||
"error", "Register reason not approved",
|
Map.of(
|
||||||
"reason_code", "NOT_APPROVED",
|
"error",
|
||||||
"token", jwtService.generateReasonToken(user.getUsername())));
|
"Register reason not approved",
|
||||||
|
"reason_code",
|
||||||
|
"NOT_APPROVED",
|
||||||
|
"token",
|
||||||
|
jwtService.generateReasonToken(user.getUsername())
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.getUsername())));
|
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.getUsername())));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/google")
|
@PostMapping("/google")
|
||||||
@Operation(summary = "Login with Google", description = "Authenticate using Google account")
|
@Operation(summary = "Login with Google", description = "Authenticate using Google account")
|
||||||
@ApiResponse(responseCode = "200", description = "Authentication result",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = Map.class)))
|
responseCode = "200",
|
||||||
|
description = "Authentication result",
|
||||||
|
content = @Content(schema = @Schema(implementation = Map.class))
|
||||||
|
)
|
||||||
public ResponseEntity<?> loginWithGoogle(@RequestBody GoogleLoginRequest req) {
|
public ResponseEntity<?> loginWithGoogle(@RequestBody GoogleLoginRequest req) {
|
||||||
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
|
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
|
||||||
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken());
|
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(
|
||||||
|
req.getInviteToken()
|
||||||
|
);
|
||||||
if (viaInvite && !inviteValidateResult.isValidate()) {
|
if (viaInvite && !inviteValidateResult.isValidate()) {
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", "Invalid invite token"));
|
return ResponseEntity.badRequest().body(Map.of("error", "Invalid invite token"));
|
||||||
}
|
}
|
||||||
Optional<AuthResult> resultOpt = googleAuthService.authenticate(
|
Optional<AuthResult> resultOpt = googleAuthService.authenticate(
|
||||||
req.getIdToken(),
|
req.getIdToken(),
|
||||||
registerModeService.getRegisterMode(),
|
registerModeService.getRegisterMode(),
|
||||||
viaInvite);
|
viaInvite
|
||||||
|
);
|
||||||
if (resultOpt.isPresent()) {
|
if (resultOpt.isPresent()) {
|
||||||
AuthResult result = resultOpt.get();
|
AuthResult result = resultOpt.get();
|
||||||
if (viaInvite && result.isNewUser()) {
|
if (viaInvite && result.isNewUser()) {
|
||||||
inviteService.consume(req.getInviteToken(), inviteValidateResult.getInviteToken().getInviter().getUsername());
|
inviteService.consume(
|
||||||
return ResponseEntity.ok(Map.of(
|
req.getInviteToken(),
|
||||||
"token", jwtService.generateToken(result.getUser().getUsername()),
|
inviteValidateResult.getInviteToken().getInviter().getUsername()
|
||||||
"reason_code", "INVITE_APPROVED"
|
);
|
||||||
));
|
return ResponseEntity.ok(
|
||||||
|
Map.of(
|
||||||
|
"token",
|
||||||
|
jwtService.generateToken(result.getUser().getUsername()),
|
||||||
|
"reason_code",
|
||||||
|
"INVITE_APPROVED"
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
||||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
|
return ResponseEntity.ok(
|
||||||
|
Map.of("token", jwtService.generateToken(result.getUser().getUsername()))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (!result.getUser().isApproved()) {
|
if (!result.getUser().isApproved()) {
|
||||||
if (result.getUser().getRegisterReason() != null && !result.getUser().getRegisterReason().isEmpty()) {
|
if (
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
result.getUser().getRegisterReason() != null &&
|
||||||
"error", "Account awaiting approval",
|
!result.getUser().getRegisterReason().isEmpty()
|
||||||
"reason_code", "IS_APPROVING",
|
) {
|
||||||
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
return ResponseEntity.badRequest().body(
|
||||||
));
|
Map.of(
|
||||||
|
"error",
|
||||||
|
"Account awaiting approval",
|
||||||
|
"reason_code",
|
||||||
|
"IS_APPROVING",
|
||||||
|
"token",
|
||||||
|
jwtService.generateReasonToken(result.getUser().getUsername())
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(
|
||||||
"error", "Account awaiting approval",
|
Map.of(
|
||||||
"reason_code", "NOT_APPROVED",
|
"error",
|
||||||
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
"Account awaiting approval",
|
||||||
));
|
"reason_code",
|
||||||
|
"NOT_APPROVED",
|
||||||
|
"token",
|
||||||
|
jwtService.generateReasonToken(result.getUser().getUsername())
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
|
return ResponseEntity.ok(
|
||||||
|
Map.of("token", jwtService.generateToken(result.getUser().getUsername()))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(
|
||||||
"error", "Invalid google token",
|
Map.of("error", "Invalid google token", "reason_code", "INVALID_CREDENTIALS")
|
||||||
"reason_code", "INVALID_CREDENTIALS"
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@PostMapping("/reason")
|
@PostMapping("/reason")
|
||||||
@Operation(summary = "Submit register reason", description = "Submit registration reason for approval")
|
@Operation(
|
||||||
@ApiResponse(responseCode = "200", description = "Submission result",
|
summary = "Submit register reason",
|
||||||
content = @Content(schema = @Schema(implementation = Map.class)))
|
description = "Submit registration reason for approval"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Submission result",
|
||||||
|
content = @Content(schema = @Schema(implementation = Map.class))
|
||||||
|
)
|
||||||
public ResponseEntity<?> reason(@RequestBody MakeReasonRequest req) {
|
public ResponseEntity<?> reason(@RequestBody MakeReasonRequest req) {
|
||||||
String username = jwtService.validateAndGetSubjectForReason(req.getToken());
|
String username = jwtService.validateAndGetSubjectForReason(req.getToken());
|
||||||
Optional<User> userOpt = userService.findByUsername(username);
|
Optional<User> userOpt = userService.findByUsername(username);
|
||||||
if (userOpt.isEmpty()) {
|
if (userOpt.isEmpty()) {
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(
|
||||||
"error", "Invalid token, Please re-login",
|
Map.of("error", "Invalid token, Please re-login", "reason_code", "INVALID_CREDENTIALS")
|
||||||
"reason_code", "INVALID_CREDENTIALS"
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.getReason() == null || req.getReason().trim().length() <= 20) {
|
if (req.getReason() == null || req.getReason().trim().length() <= 20) {
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(
|
||||||
"error", "Reason's length must longer than 20",
|
Map.of("error", "Reason's length must longer than 20", "reason_code", "INVALID_CREDENTIALS")
|
||||||
"reason_code", "INVALID_CREDENTIALS"
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
@@ -245,11 +325,16 @@ public class AuthController {
|
|||||||
|
|
||||||
@PostMapping("/github")
|
@PostMapping("/github")
|
||||||
@Operation(summary = "Login with GitHub", description = "Authenticate using GitHub account")
|
@Operation(summary = "Login with GitHub", description = "Authenticate using GitHub account")
|
||||||
@ApiResponse(responseCode = "200", description = "Authentication result",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = Map.class)))
|
responseCode = "200",
|
||||||
|
description = "Authentication result",
|
||||||
|
content = @Content(schema = @Schema(implementation = Map.class))
|
||||||
|
)
|
||||||
public ResponseEntity<?> loginWithGithub(@RequestBody GithubLoginRequest req) {
|
public ResponseEntity<?> loginWithGithub(@RequestBody GithubLoginRequest req) {
|
||||||
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
|
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
|
||||||
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken());
|
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(
|
||||||
|
req.getInviteToken()
|
||||||
|
);
|
||||||
if (viaInvite && !inviteValidateResult.isValidate()) {
|
if (viaInvite && !inviteValidateResult.isValidate()) {
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", "Invalid invite token"));
|
return ResponseEntity.badRequest().body(Map.of("error", "Invalid invite token"));
|
||||||
}
|
}
|
||||||
@@ -257,50 +342,79 @@ public class AuthController {
|
|||||||
req.getCode(),
|
req.getCode(),
|
||||||
registerModeService.getRegisterMode(),
|
registerModeService.getRegisterMode(),
|
||||||
req.getRedirectUri(),
|
req.getRedirectUri(),
|
||||||
viaInvite);
|
viaInvite
|
||||||
|
);
|
||||||
if (resultOpt.isPresent()) {
|
if (resultOpt.isPresent()) {
|
||||||
AuthResult result = resultOpt.get();
|
AuthResult result = resultOpt.get();
|
||||||
if (viaInvite && result.isNewUser()) {
|
if (viaInvite && result.isNewUser()) {
|
||||||
inviteService.consume(req.getInviteToken(), inviteValidateResult.getInviteToken().getInviter().getUsername());
|
inviteService.consume(
|
||||||
return ResponseEntity.ok(Map.of(
|
req.getInviteToken(),
|
||||||
"token", jwtService.generateToken(result.getUser().getUsername()),
|
inviteValidateResult.getInviteToken().getInviter().getUsername()
|
||||||
"reason_code", "INVITE_APPROVED"
|
);
|
||||||
));
|
return ResponseEntity.ok(
|
||||||
|
Map.of(
|
||||||
|
"token",
|
||||||
|
jwtService.generateToken(result.getUser().getUsername()),
|
||||||
|
"reason_code",
|
||||||
|
"INVITE_APPROVED"
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
||||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
|
return ResponseEntity.ok(
|
||||||
|
Map.of("token", jwtService.generateToken(result.getUser().getUsername()))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (!result.getUser().isApproved()) {
|
if (!result.getUser().isApproved()) {
|
||||||
if (result.getUser().getRegisterReason() != null && !result.getUser().getRegisterReason().isEmpty()) {
|
if (
|
||||||
|
result.getUser().getRegisterReason() != null &&
|
||||||
|
!result.getUser().getRegisterReason().isEmpty()
|
||||||
|
) {
|
||||||
// 已填写注册理由
|
// 已填写注册理由
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(
|
||||||
"error", "Account awaiting approval",
|
Map.of(
|
||||||
"reason_code", "IS_APPROVING",
|
"error",
|
||||||
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
"Account awaiting approval",
|
||||||
));
|
"reason_code",
|
||||||
|
"IS_APPROVING",
|
||||||
|
"token",
|
||||||
|
jwtService.generateReasonToken(result.getUser().getUsername())
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(
|
||||||
"error", "Account awaiting approval",
|
Map.of(
|
||||||
"reason_code", "NOT_APPROVED",
|
"error",
|
||||||
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
"Account awaiting approval",
|
||||||
));
|
"reason_code",
|
||||||
|
"NOT_APPROVED",
|
||||||
|
"token",
|
||||||
|
jwtService.generateReasonToken(result.getUser().getUsername())
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
|
return ResponseEntity.ok(
|
||||||
|
Map.of("token", jwtService.generateToken(result.getUser().getUsername()))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(
|
||||||
"error", "Invalid github code",
|
Map.of("error", "Invalid github code", "reason_code", "INVALID_CREDENTIALS")
|
||||||
"reason_code", "INVALID_CREDENTIALS"
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/discord")
|
@PostMapping("/discord")
|
||||||
@Operation(summary = "Login with Discord", description = "Authenticate using Discord account")
|
@Operation(summary = "Login with Discord", description = "Authenticate using Discord account")
|
||||||
@ApiResponse(responseCode = "200", description = "Authentication result",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = Map.class)))
|
responseCode = "200",
|
||||||
|
description = "Authentication result",
|
||||||
|
content = @Content(schema = @Schema(implementation = Map.class))
|
||||||
|
)
|
||||||
public ResponseEntity<?> loginWithDiscord(@RequestBody DiscordLoginRequest req) {
|
public ResponseEntity<?> loginWithDiscord(@RequestBody DiscordLoginRequest req) {
|
||||||
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
|
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
|
||||||
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken());
|
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(
|
||||||
|
req.getInviteToken()
|
||||||
|
);
|
||||||
if (viaInvite && !inviteValidateResult.isValidate()) {
|
if (viaInvite && !inviteValidateResult.isValidate()) {
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", "Invalid invite token"));
|
return ResponseEntity.badRequest().body(Map.of("error", "Invalid invite token"));
|
||||||
}
|
}
|
||||||
@@ -308,49 +422,78 @@ public class AuthController {
|
|||||||
req.getCode(),
|
req.getCode(),
|
||||||
registerModeService.getRegisterMode(),
|
registerModeService.getRegisterMode(),
|
||||||
req.getRedirectUri(),
|
req.getRedirectUri(),
|
||||||
viaInvite);
|
viaInvite
|
||||||
|
);
|
||||||
if (resultOpt.isPresent()) {
|
if (resultOpt.isPresent()) {
|
||||||
AuthResult result = resultOpt.get();
|
AuthResult result = resultOpt.get();
|
||||||
if (viaInvite && result.isNewUser()) {
|
if (viaInvite && result.isNewUser()) {
|
||||||
inviteService.consume(req.getInviteToken(), inviteValidateResult.getInviteToken().getInviter().getUsername());
|
inviteService.consume(
|
||||||
return ResponseEntity.ok(Map.of(
|
req.getInviteToken(),
|
||||||
"token", jwtService.generateToken(result.getUser().getUsername()),
|
inviteValidateResult.getInviteToken().getInviter().getUsername()
|
||||||
"reason_code", "INVITE_APPROVED"
|
);
|
||||||
));
|
return ResponseEntity.ok(
|
||||||
|
Map.of(
|
||||||
|
"token",
|
||||||
|
jwtService.generateToken(result.getUser().getUsername()),
|
||||||
|
"reason_code",
|
||||||
|
"INVITE_APPROVED"
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
||||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
|
return ResponseEntity.ok(
|
||||||
|
Map.of("token", jwtService.generateToken(result.getUser().getUsername()))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (!result.getUser().isApproved()) {
|
if (!result.getUser().isApproved()) {
|
||||||
if (result.getUser().getRegisterReason() != null && !result.getUser().getRegisterReason().isEmpty()) {
|
if (
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
result.getUser().getRegisterReason() != null &&
|
||||||
"error", "Account awaiting approval",
|
!result.getUser().getRegisterReason().isEmpty()
|
||||||
"reason_code", "IS_APPROVING",
|
) {
|
||||||
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
return ResponseEntity.badRequest().body(
|
||||||
));
|
Map.of(
|
||||||
|
"error",
|
||||||
|
"Account awaiting approval",
|
||||||
|
"reason_code",
|
||||||
|
"IS_APPROVING",
|
||||||
|
"token",
|
||||||
|
jwtService.generateReasonToken(result.getUser().getUsername())
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(
|
||||||
"error", "Account awaiting approval",
|
Map.of(
|
||||||
"reason_code", "NOT_APPROVED",
|
"error",
|
||||||
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
"Account awaiting approval",
|
||||||
));
|
"reason_code",
|
||||||
|
"NOT_APPROVED",
|
||||||
|
"token",
|
||||||
|
jwtService.generateReasonToken(result.getUser().getUsername())
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
|
return ResponseEntity.ok(
|
||||||
|
Map.of("token", jwtService.generateToken(result.getUser().getUsername()))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(
|
||||||
"error", "Invalid discord code",
|
Map.of("error", "Invalid discord code", "reason_code", "INVALID_CREDENTIALS")
|
||||||
"reason_code", "INVALID_CREDENTIALS"
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/twitter")
|
@PostMapping("/twitter")
|
||||||
@Operation(summary = "Login with Twitter", description = "Authenticate using Twitter account")
|
@Operation(summary = "Login with Twitter", description = "Authenticate using Twitter account")
|
||||||
@ApiResponse(responseCode = "200", description = "Authentication result",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = Map.class)))
|
responseCode = "200",
|
||||||
|
description = "Authentication result",
|
||||||
|
content = @Content(schema = @Schema(implementation = Map.class))
|
||||||
|
)
|
||||||
public ResponseEntity<?> loginWithTwitter(@RequestBody TwitterLoginRequest req) {
|
public ResponseEntity<?> loginWithTwitter(@RequestBody TwitterLoginRequest req) {
|
||||||
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
|
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
|
||||||
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken());
|
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(
|
||||||
|
req.getInviteToken()
|
||||||
|
);
|
||||||
if (viaInvite && !inviteValidateResult.isValidate()) {
|
if (viaInvite && !inviteValidateResult.isValidate()) {
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", "Invalid invite token"));
|
return ResponseEntity.badRequest().body(Map.of("error", "Invalid invite token"));
|
||||||
}
|
}
|
||||||
@@ -359,103 +502,162 @@ public class AuthController {
|
|||||||
req.getCodeVerifier(),
|
req.getCodeVerifier(),
|
||||||
registerModeService.getRegisterMode(),
|
registerModeService.getRegisterMode(),
|
||||||
req.getRedirectUri(),
|
req.getRedirectUri(),
|
||||||
viaInvite);
|
viaInvite
|
||||||
|
);
|
||||||
if (resultOpt.isPresent()) {
|
if (resultOpt.isPresent()) {
|
||||||
AuthResult result = resultOpt.get();
|
AuthResult result = resultOpt.get();
|
||||||
if (viaInvite && result.isNewUser()) {
|
if (viaInvite && result.isNewUser()) {
|
||||||
inviteService.consume(req.getInviteToken(), inviteValidateResult.getInviteToken().getInviter().getUsername());
|
inviteService.consume(
|
||||||
return ResponseEntity.ok(Map.of(
|
req.getInviteToken(),
|
||||||
"token", jwtService.generateToken(result.getUser().getUsername()),
|
inviteValidateResult.getInviteToken().getInviter().getUsername()
|
||||||
"reason_code", "INVITE_APPROVED"
|
);
|
||||||
));
|
return ResponseEntity.ok(
|
||||||
|
Map.of(
|
||||||
|
"token",
|
||||||
|
jwtService.generateToken(result.getUser().getUsername()),
|
||||||
|
"reason_code",
|
||||||
|
"INVITE_APPROVED"
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
||||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
|
return ResponseEntity.ok(
|
||||||
|
Map.of("token", jwtService.generateToken(result.getUser().getUsername()))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (!result.getUser().isApproved()) {
|
if (!result.getUser().isApproved()) {
|
||||||
if (result.getUser().getRegisterReason() != null && !result.getUser().getRegisterReason().isEmpty()) {
|
if (
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
result.getUser().getRegisterReason() != null &&
|
||||||
"error", "Account awaiting approval",
|
!result.getUser().getRegisterReason().isEmpty()
|
||||||
"reason_code", "IS_APPROVING",
|
) {
|
||||||
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
return ResponseEntity.badRequest().body(
|
||||||
));
|
Map.of(
|
||||||
|
"error",
|
||||||
|
"Account awaiting approval",
|
||||||
|
"reason_code",
|
||||||
|
"IS_APPROVING",
|
||||||
|
"token",
|
||||||
|
jwtService.generateReasonToken(result.getUser().getUsername())
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(
|
||||||
"error", "Account awaiting approval",
|
Map.of(
|
||||||
"reason_code", "NOT_APPROVED",
|
"error",
|
||||||
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
"Account awaiting approval",
|
||||||
));
|
"reason_code",
|
||||||
|
"NOT_APPROVED",
|
||||||
|
"token",
|
||||||
|
jwtService.generateReasonToken(result.getUser().getUsername())
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
|
return ResponseEntity.ok(
|
||||||
|
Map.of("token", jwtService.generateToken(result.getUser().getUsername()))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(
|
||||||
"error", "Invalid twitter code",
|
Map.of("error", "Invalid twitter code", "reason_code", "INVALID_CREDENTIALS")
|
||||||
"reason_code", "INVALID_CREDENTIALS"
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/telegram")
|
@PostMapping("/telegram")
|
||||||
@Operation(summary = "Login with Telegram", description = "Authenticate using Telegram data")
|
@Operation(summary = "Login with Telegram", description = "Authenticate using Telegram data")
|
||||||
@ApiResponse(responseCode = "200", description = "Authentication result",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = Map.class)))
|
responseCode = "200",
|
||||||
|
description = "Authentication result",
|
||||||
|
content = @Content(schema = @Schema(implementation = Map.class))
|
||||||
|
)
|
||||||
public ResponseEntity<?> loginWithTelegram(@RequestBody TelegramLoginRequest req) {
|
public ResponseEntity<?> loginWithTelegram(@RequestBody TelegramLoginRequest req) {
|
||||||
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
|
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
|
||||||
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken());
|
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(
|
||||||
|
req.getInviteToken()
|
||||||
|
);
|
||||||
if (viaInvite && !inviteValidateResult.isValidate()) {
|
if (viaInvite && !inviteValidateResult.isValidate()) {
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", "Invalid invite token"));
|
return ResponseEntity.badRequest().body(Map.of("error", "Invalid invite token"));
|
||||||
}
|
}
|
||||||
Optional<AuthResult> resultOpt = telegramAuthService.authenticate(
|
Optional<AuthResult> resultOpt = telegramAuthService.authenticate(
|
||||||
req,
|
req,
|
||||||
registerModeService.getRegisterMode(),
|
registerModeService.getRegisterMode(),
|
||||||
viaInvite);
|
viaInvite
|
||||||
|
);
|
||||||
if (resultOpt.isPresent()) {
|
if (resultOpt.isPresent()) {
|
||||||
AuthResult result = resultOpt.get();
|
AuthResult result = resultOpt.get();
|
||||||
if (viaInvite && result.isNewUser()) {
|
if (viaInvite && result.isNewUser()) {
|
||||||
inviteService.consume(req.getInviteToken(), inviteValidateResult.getInviteToken().getInviter().getUsername());
|
inviteService.consume(
|
||||||
return ResponseEntity.ok(Map.of(
|
req.getInviteToken(),
|
||||||
"token", jwtService.generateToken(result.getUser().getUsername()),
|
inviteValidateResult.getInviteToken().getInviter().getUsername()
|
||||||
"reason_code", "INVITE_APPROVED"
|
);
|
||||||
));
|
return ResponseEntity.ok(
|
||||||
|
Map.of(
|
||||||
|
"token",
|
||||||
|
jwtService.generateToken(result.getUser().getUsername()),
|
||||||
|
"reason_code",
|
||||||
|
"INVITE_APPROVED"
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
||||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
|
return ResponseEntity.ok(
|
||||||
|
Map.of("token", jwtService.generateToken(result.getUser().getUsername()))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (!result.getUser().isApproved()) {
|
if (!result.getUser().isApproved()) {
|
||||||
if (result.getUser().getRegisterReason() != null && !result.getUser().getRegisterReason().isEmpty()) {
|
if (
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
result.getUser().getRegisterReason() != null &&
|
||||||
"error", "Account awaiting approval",
|
!result.getUser().getRegisterReason().isEmpty()
|
||||||
"reason_code", "IS_APPROVING",
|
) {
|
||||||
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
return ResponseEntity.badRequest().body(
|
||||||
));
|
Map.of(
|
||||||
|
"error",
|
||||||
|
"Account awaiting approval",
|
||||||
|
"reason_code",
|
||||||
|
"IS_APPROVING",
|
||||||
|
"token",
|
||||||
|
jwtService.generateReasonToken(result.getUser().getUsername())
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(
|
||||||
"error", "Account awaiting approval",
|
Map.of(
|
||||||
"reason_code", "NOT_APPROVED",
|
"error",
|
||||||
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
"Account awaiting approval",
|
||||||
));
|
"reason_code",
|
||||||
|
"NOT_APPROVED",
|
||||||
|
"token",
|
||||||
|
jwtService.generateReasonToken(result.getUser().getUsername())
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
|
return ResponseEntity.ok(
|
||||||
|
Map.of("token", jwtService.generateToken(result.getUser().getUsername()))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(
|
||||||
"error", "Invalid telegram data",
|
Map.of("error", "Invalid telegram data", "reason_code", "INVALID_CREDENTIALS")
|
||||||
"reason_code", "INVALID_CREDENTIALS"
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/check")
|
@GetMapping("/check")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "Check token", description = "Validate JWT token")
|
@Operation(summary = "Check token", description = "Validate JWT token")
|
||||||
@ApiResponse(responseCode = "200", description = "Token valid",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = Map.class)))
|
responseCode = "200",
|
||||||
|
description = "Token valid",
|
||||||
|
content = @Content(schema = @Schema(implementation = Map.class))
|
||||||
|
)
|
||||||
public ResponseEntity<?> checkToken() {
|
public ResponseEntity<?> checkToken() {
|
||||||
return ResponseEntity.ok(Map.of("valid", true));
|
return ResponseEntity.ok(Map.of("valid", true));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/forgot/send")
|
@PostMapping("/forgot/send")
|
||||||
@Operation(summary = "Send reset code", description = "Send verification code for password reset")
|
@Operation(summary = "Send reset code", description = "Send verification code for password reset")
|
||||||
@ApiResponse(responseCode = "200", description = "Sending result",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = Map.class)))
|
responseCode = "200",
|
||||||
|
description = "Sending result",
|
||||||
|
content = @Content(schema = @Schema(implementation = Map.class))
|
||||||
|
)
|
||||||
public ResponseEntity<?> sendReset(@RequestBody ForgotPasswordRequest req) {
|
public ResponseEntity<?> sendReset(@RequestBody ForgotPasswordRequest req) {
|
||||||
Optional<User> userOpt = userService.findByEmail(req.getEmail());
|
Optional<User> userOpt = userService.findByEmail(req.getEmail());
|
||||||
if (userOpt.isEmpty()) {
|
if (userOpt.isEmpty()) {
|
||||||
@@ -467,8 +669,11 @@ public class AuthController {
|
|||||||
|
|
||||||
@PostMapping("/forgot/verify")
|
@PostMapping("/forgot/verify")
|
||||||
@Operation(summary = "Verify reset code", description = "Verify password reset code")
|
@Operation(summary = "Verify reset code", description = "Verify password reset code")
|
||||||
@ApiResponse(responseCode = "200", description = "Verification result",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = Map.class)))
|
responseCode = "200",
|
||||||
|
description = "Verification result",
|
||||||
|
content = @Content(schema = @Schema(implementation = Map.class))
|
||||||
|
)
|
||||||
public ResponseEntity<?> verifyReset(@RequestBody VerifyForgotRequest req) {
|
public ResponseEntity<?> verifyReset(@RequestBody VerifyForgotRequest req) {
|
||||||
Optional<User> userOpt = userService.findByEmail(req.getEmail());
|
Optional<User> userOpt = userService.findByEmail(req.getEmail());
|
||||||
if (userOpt.isEmpty()) {
|
if (userOpt.isEmpty()) {
|
||||||
@@ -484,18 +689,20 @@ public class AuthController {
|
|||||||
|
|
||||||
@PostMapping("/forgot/reset")
|
@PostMapping("/forgot/reset")
|
||||||
@Operation(summary = "Reset password", description = "Reset user password after verification")
|
@Operation(summary = "Reset password", description = "Reset user password after verification")
|
||||||
@ApiResponse(responseCode = "200", description = "Reset result",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = Map.class)))
|
responseCode = "200",
|
||||||
|
description = "Reset result",
|
||||||
|
content = @Content(schema = @Schema(implementation = Map.class))
|
||||||
|
)
|
||||||
public ResponseEntity<?> resetPassword(@RequestBody ResetPasswordRequest req) {
|
public ResponseEntity<?> resetPassword(@RequestBody ResetPasswordRequest req) {
|
||||||
String username = jwtService.validateAndGetSubjectForReset(req.getToken());
|
String username = jwtService.validateAndGetSubjectForReset(req.getToken());
|
||||||
try {
|
try {
|
||||||
userService.updatePassword(username, req.getPassword());
|
userService.updatePassword(username, req.getPassword());
|
||||||
return ResponseEntity.ok(Map.of("message", "Password updated"));
|
return ResponseEntity.ok(Map.of("message", "Password updated"));
|
||||||
} catch (FieldException e) {
|
} catch (FieldException e) {
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(
|
||||||
"field", e.getField(),
|
Map.of("field", e.getField(), "error", e.getMessage())
|
||||||
"error", e.getMessage()
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,22 +8,22 @@ 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 CategoryService categoryService;
|
||||||
private final PostService postService;
|
private final PostService postService;
|
||||||
private final PostMapper postMapper;
|
private final PostMapper postMapper;
|
||||||
@@ -31,20 +31,37 @@ public class CategoryController {
|
|||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@Operation(summary = "Create category", description = "Create a new category")
|
@Operation(summary = "Create category", description = "Create a new category")
|
||||||
@ApiResponse(responseCode = "200", description = "Created category",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = CategoryDto.class)))
|
responseCode = "200",
|
||||||
|
description = "Created category",
|
||||||
|
content = @Content(schema = @Schema(implementation = CategoryDto.class))
|
||||||
|
)
|
||||||
public CategoryDto create(@RequestBody CategoryRequest req) {
|
public CategoryDto create(@RequestBody CategoryRequest req) {
|
||||||
Category c = categoryService.createCategory(req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon());
|
Category c = categoryService.createCategory(
|
||||||
|
req.getName(),
|
||||||
|
req.getDescription(),
|
||||||
|
req.getIcon(),
|
||||||
|
req.getSmallIcon()
|
||||||
|
);
|
||||||
long count = postService.countPostsByCategory(c.getId());
|
long count = postService.countPostsByCategory(c.getId());
|
||||||
return categoryMapper.toDto(c, count);
|
return categoryMapper.toDto(c, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/{id}")
|
@PutMapping("/{id}")
|
||||||
@Operation(summary = "Update category", description = "Update an existing category")
|
@Operation(summary = "Update category", description = "Update an existing category")
|
||||||
@ApiResponse(responseCode = "200", description = "Updated category",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = CategoryDto.class)))
|
responseCode = "200",
|
||||||
|
description = "Updated category",
|
||||||
|
content = @Content(schema = @Schema(implementation = CategoryDto.class))
|
||||||
|
)
|
||||||
public CategoryDto update(@PathVariable Long id, @RequestBody CategoryRequest req) {
|
public CategoryDto update(@PathVariable Long id, @RequestBody CategoryRequest req) {
|
||||||
Category c = categoryService.updateCategory(id, req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon());
|
Category c = categoryService.updateCategory(
|
||||||
|
id,
|
||||||
|
req.getName(),
|
||||||
|
req.getDescription(),
|
||||||
|
req.getIcon(),
|
||||||
|
req.getSmallIcon()
|
||||||
|
);
|
||||||
long count = postService.countPostsByCategory(c.getId());
|
long count = postService.countPostsByCategory(c.getId());
|
||||||
return categoryMapper.toDto(c, count);
|
return categoryMapper.toDto(c, count);
|
||||||
}
|
}
|
||||||
@@ -58,13 +75,17 @@ public class CategoryController {
|
|||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@Operation(summary = "List categories", description = "Get all categories")
|
@Operation(summary = "List categories", description = "Get all categories")
|
||||||
@ApiResponse(responseCode = "200", description = "List of categories",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = CategoryDto.class))))
|
responseCode = "200",
|
||||||
|
description = "List of categories",
|
||||||
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = CategoryDto.class)))
|
||||||
|
)
|
||||||
public List<CategoryDto> list() {
|
public List<CategoryDto> list() {
|
||||||
List<Category> all = categoryService.listCategories();
|
List<Category> all = categoryService.listCategories();
|
||||||
List<Long> ids = all.stream().map(Category::getId).toList();
|
List<Long> ids = all.stream().map(Category::getId).toList();
|
||||||
Map<Long, Long> postsCntByCategoryIds = postService.countPostsByCategoryIds(ids);
|
Map<Long, Long> postsCntByCategoryIds = postService.countPostsByCategoryIds(ids);
|
||||||
return all.stream()
|
return all
|
||||||
|
.stream()
|
||||||
.map(c -> categoryMapper.toDto(c, postsCntByCategoryIds.getOrDefault(c.getId(), 0L)))
|
.map(c -> categoryMapper.toDto(c, postsCntByCategoryIds.getOrDefault(c.getId(), 0L)))
|
||||||
.sorted((a, b) -> Long.compare(b.getCount(), a.getCount()))
|
.sorted((a, b) -> Long.compare(b.getCount(), a.getCount()))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
@@ -72,8 +93,11 @@ public class CategoryController {
|
|||||||
|
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
@Operation(summary = "Get category", description = "Get category by id")
|
@Operation(summary = "Get category", description = "Get category by id")
|
||||||
@ApiResponse(responseCode = "200", description = "Category detail",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = CategoryDto.class)))
|
responseCode = "200",
|
||||||
|
description = "Category detail",
|
||||||
|
content = @Content(schema = @Schema(implementation = CategoryDto.class))
|
||||||
|
)
|
||||||
public CategoryDto get(@PathVariable Long id) {
|
public CategoryDto get(@PathVariable Long id) {
|
||||||
Category c = categoryService.getCategory(id);
|
Category c = categoryService.getCategory(id);
|
||||||
long count = postService.countPostsByCategory(c.getId());
|
long count = postService.countPostsByCategory(c.getId());
|
||||||
@@ -82,12 +106,20 @@ public class CategoryController {
|
|||||||
|
|
||||||
@GetMapping("/{id}/posts")
|
@GetMapping("/{id}/posts")
|
||||||
@Operation(summary = "List posts by category", description = "Get posts under a category")
|
@Operation(summary = "List posts by category", description = "Get posts under a category")
|
||||||
@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 = "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 = "page", required = false) Integer page,
|
||||||
@RequestParam(value = "pageSize", required = false) Integer pageSize) {
|
@RequestParam(value = "pageSize", required = false) Integer pageSize
|
||||||
return postService.listPostsByCategories(java.util.List.of(id), page, pageSize)
|
) {
|
||||||
|
return postService
|
||||||
|
.listPostsByCategories(java.util.List.of(id), page, pageSize)
|
||||||
.stream()
|
.stream()
|
||||||
.map(postMapper::toSummaryDto)
|
.map(postMapper::toSummaryDto)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|||||||
@@ -5,36 +5,40 @@ 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 ChannelService channelService;
|
||||||
private final MessageService messageService;
|
private final MessageService messageService;
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
private Long getCurrentUserId(Authentication auth) {
|
private Long getCurrentUserId(Authentication auth) {
|
||||||
User user = userRepository.findByUsername(auth.getName())
|
User user = userRepository
|
||||||
|
.findByUsername(auth.getName())
|
||||||
.orElseThrow(() -> new IllegalArgumentException("User not found"));
|
.orElseThrow(() -> new IllegalArgumentException("User not found"));
|
||||||
return user.getId();
|
return user.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@Operation(summary = "List channels", description = "List channels for the current user")
|
@Operation(summary = "List channels", description = "List channels for the current user")
|
||||||
@ApiResponse(responseCode = "200", description = "Channels",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ChannelDto.class))))
|
responseCode = "200",
|
||||||
|
description = "Channels",
|
||||||
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ChannelDto.class)))
|
||||||
|
)
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public List<ChannelDto> listChannels(Authentication auth) {
|
public List<ChannelDto> listChannels(Authentication auth) {
|
||||||
return channelService.listChannels(getCurrentUserId(auth));
|
return channelService.listChannels(getCurrentUserId(auth));
|
||||||
@@ -42,8 +46,11 @@ public class ChannelController {
|
|||||||
|
|
||||||
@PostMapping("/{channelId}/join")
|
@PostMapping("/{channelId}/join")
|
||||||
@Operation(summary = "Join channel", description = "Join a channel")
|
@Operation(summary = "Join channel", description = "Join a channel")
|
||||||
@ApiResponse(responseCode = "200", description = "Joined channel",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = ChannelDto.class)))
|
responseCode = "200",
|
||||||
|
description = "Joined channel",
|
||||||
|
content = @Content(schema = @Schema(implementation = ChannelDto.class))
|
||||||
|
)
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public ChannelDto joinChannel(@PathVariable Long channelId, Authentication auth) {
|
public ChannelDto joinChannel(@PathVariable Long channelId, Authentication auth) {
|
||||||
return channelService.joinChannel(channelId, getCurrentUserId(auth));
|
return channelService.joinChannel(channelId, getCurrentUserId(auth));
|
||||||
@@ -51,8 +58,11 @@ public class ChannelController {
|
|||||||
|
|
||||||
@GetMapping("/unread-count")
|
@GetMapping("/unread-count")
|
||||||
@Operation(summary = "Unread count", description = "Get unread channel count")
|
@Operation(summary = "Unread count", description = "Get unread channel count")
|
||||||
@ApiResponse(responseCode = "200", description = "Unread count",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = Long.class)))
|
responseCode = "200",
|
||||||
|
description = "Unread count",
|
||||||
|
content = @Content(schema = @Schema(implementation = Long.class))
|
||||||
|
)
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public long unreadCount(Authentication auth) {
|
public long unreadCount(Authentication auth) {
|
||||||
return messageService.getUnreadChannelCount(getCurrentUserId(auth));
|
return messageService.getUnreadChannelCount(getCurrentUserId(auth));
|
||||||
|
|||||||
@@ -1,39 +1,44 @@
|
|||||||
package com.openisle.controller;
|
package com.openisle.controller;
|
||||||
|
|
||||||
import com.openisle.model.Comment;
|
|
||||||
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.model.Comment;
|
||||||
import com.openisle.service.LevelService;
|
import com.openisle.model.CommentSort;
|
||||||
import com.openisle.service.PointService;
|
import com.openisle.service.*;
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import 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.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 CommentService commentService;
|
||||||
private final LevelService levelService;
|
private final LevelService levelService;
|
||||||
private final CaptchaService captchaService;
|
private final CaptchaService captchaService;
|
||||||
private final CommentMapper commentMapper;
|
private final CommentMapper commentMapper;
|
||||||
private final PointService pointService;
|
private final PointService pointService;
|
||||||
|
private final PostChangeLogService changeLogService;
|
||||||
|
private final PostChangeLogMapper postChangeLogMapper;
|
||||||
|
|
||||||
@Value("${app.captcha.enabled:false}")
|
@Value("${app.captcha.enabled:false}")
|
||||||
private boolean captchaEnabled;
|
private boolean captchaEnabled;
|
||||||
@@ -43,12 +48,17 @@ public class CommentController {
|
|||||||
|
|
||||||
@PostMapping("/posts/{postId}/comments")
|
@PostMapping("/posts/{postId}/comments")
|
||||||
@Operation(summary = "Create comment", description = "Add a comment to a post")
|
@Operation(summary = "Create comment", description = "Add a comment to a post")
|
||||||
@ApiResponse(responseCode = "200", description = "Created comment",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = CommentDto.class)))
|
responseCode = "200",
|
||||||
|
description = "Created comment",
|
||||||
|
content = @Content(schema = @Schema(implementation = CommentDto.class))
|
||||||
|
)
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public ResponseEntity<CommentDto> createComment(@PathVariable Long postId,
|
public ResponseEntity<CommentDto> createComment(
|
||||||
|
@PathVariable Long postId,
|
||||||
@RequestBody CommentRequest req,
|
@RequestBody CommentRequest req,
|
||||||
Authentication auth) {
|
Authentication auth
|
||||||
|
) {
|
||||||
log.debug("createComment called by user {} for post {}", auth.getName(), postId);
|
log.debug("createComment called by user {} for post {}", auth.getName(), postId);
|
||||||
if (captchaEnabled && commentCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
|
if (captchaEnabled && commentCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
|
||||||
log.debug("Captcha verification failed for user {} on post {}", auth.getName(), postId);
|
log.debug("Captcha verification failed for user {} on post {}", auth.getName(), postId);
|
||||||
@@ -64,12 +74,17 @@ public class CommentController {
|
|||||||
|
|
||||||
@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",
|
||||||
|
description = "Reply created",
|
||||||
|
content = @Content(schema = @Schema(implementation = CommentDto.class))
|
||||||
|
)
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public ResponseEntity<CommentDto> replyComment(@PathVariable Long commentId,
|
public ResponseEntity<CommentDto> replyComment(
|
||||||
|
@PathVariable Long commentId,
|
||||||
@RequestBody CommentRequest req,
|
@RequestBody CommentRequest req,
|
||||||
Authentication auth) {
|
Authentication auth
|
||||||
|
) {
|
||||||
log.debug("replyComment called by user {} for comment {}", auth.getName(), commentId);
|
log.debug("replyComment called by user {} for comment {}", auth.getName(), commentId);
|
||||||
if (captchaEnabled && commentCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
|
if (captchaEnabled && commentCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
|
||||||
log.debug("Captcha verification failed for user {} on comment {}", auth.getName(), commentId);
|
log.debug("Captcha verification failed for user {} on comment {}", auth.getName(), commentId);
|
||||||
@@ -84,16 +99,65 @@ public class CommentController {
|
|||||||
|
|
||||||
@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(
|
||||||
|
array = @ArraySchema(schema = @Schema(implementation = TimelineItemDto.class))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
public List<TimelineItemDto<?>> listComments(
|
||||||
|
@PathVariable Long postId,
|
||||||
|
@RequestParam(value = "sort", required = false, defaultValue = "OLDEST") CommentSort sort
|
||||||
|
) {
|
||||||
log.debug("listComments called for post {} with sort {}", postId, sort);
|
log.debug("listComments called for post {} with sort {}", postId, sort);
|
||||||
List<CommentDto> list = commentService.getCommentsForPost(postId, sort).stream()
|
List<CommentDto> commentDtoList = commentService
|
||||||
|
.getCommentsForPost(postId, sort)
|
||||||
|
.stream()
|
||||||
.map(commentMapper::toDtoWithReplies)
|
.map(commentMapper::toDtoWithReplies)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
log.debug("listComments returning {} comments", list.size());
|
List<PostChangeLogDto> postChangeLogDtoList = changeLogService
|
||||||
return list;
|
.listLogs(postId)
|
||||||
|
.stream()
|
||||||
|
.map(postChangeLogMapper::toDto)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
List<TimelineItemDto<?>> itemDtoList = new ArrayList<>();
|
||||||
|
|
||||||
|
itemDtoList.addAll(
|
||||||
|
commentDtoList
|
||||||
|
.stream()
|
||||||
|
.map(c ->
|
||||||
|
new TimelineItemDto<>(
|
||||||
|
c.getId(),
|
||||||
|
"comment",
|
||||||
|
c.getCreatedAt(),
|
||||||
|
c // payload 是 CommentDto
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.toList()
|
||||||
|
);
|
||||||
|
|
||||||
|
itemDtoList.addAll(
|
||||||
|
postChangeLogDtoList
|
||||||
|
.stream()
|
||||||
|
.map(l ->
|
||||||
|
new TimelineItemDto<>(
|
||||||
|
l.getId(),
|
||||||
|
"log",
|
||||||
|
l.getTime(), // 注意字段名不一样
|
||||||
|
l // payload 是 PostChangeLogDto
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.toList()
|
||||||
|
);
|
||||||
|
// 排序
|
||||||
|
Comparator<TimelineItemDto<?>> comparator = Comparator.comparing(TimelineItemDto::getCreatedAt);
|
||||||
|
if (CommentSort.NEWEST.equals(sort)) {
|
||||||
|
comparator = comparator.reversed();
|
||||||
|
}
|
||||||
|
itemDtoList.sort(comparator);
|
||||||
|
log.debug("listComments returning {} comments", itemDtoList.size());
|
||||||
|
return itemDtoList;
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/comments/{id}")
|
@DeleteMapping("/comments/{id}")
|
||||||
@@ -108,8 +172,11 @@ public class CommentController {
|
|||||||
|
|
||||||
@PostMapping("/comments/{id}/pin")
|
@PostMapping("/comments/{id}/pin")
|
||||||
@Operation(summary = "Pin comment", description = "Pin a comment")
|
@Operation(summary = "Pin comment", description = "Pin a comment")
|
||||||
@ApiResponse(responseCode = "200", description = "Pinned comment",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = CommentDto.class)))
|
responseCode = "200",
|
||||||
|
description = "Pinned comment",
|
||||||
|
content = @Content(schema = @Schema(implementation = CommentDto.class))
|
||||||
|
)
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public CommentDto pinComment(@PathVariable Long id, Authentication auth) {
|
public CommentDto pinComment(@PathVariable Long id, Authentication auth) {
|
||||||
log.debug("pinComment called by user {} for comment {}", auth.getName(), id);
|
log.debug("pinComment called by user {} for comment {}", auth.getName(), id);
|
||||||
@@ -118,8 +185,11 @@ public class CommentController {
|
|||||||
|
|
||||||
@PostMapping("/comments/{id}/unpin")
|
@PostMapping("/comments/{id}/unpin")
|
||||||
@Operation(summary = "Unpin comment", description = "Unpin a comment")
|
@Operation(summary = "Unpin comment", description = "Unpin a comment")
|
||||||
@ApiResponse(responseCode = "200", description = "Unpinned comment",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = CommentDto.class)))
|
responseCode = "200",
|
||||||
|
description = "Unpinned comment",
|
||||||
|
content = @Content(schema = @Schema(implementation = CommentDto.class))
|
||||||
|
)
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public CommentDto unpinComment(@PathVariable Long id, Authentication auth) {
|
public CommentDto unpinComment(@PathVariable Long id, Authentication auth) {
|
||||||
log.debug("unpinComment called by user {} for comment {}", auth.getName(), id);
|
log.debug("unpinComment called by user {} for comment {}", auth.getName(), id);
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ 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")
|
||||||
@@ -38,8 +38,11 @@ public class ConfigController {
|
|||||||
|
|
||||||
@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",
|
||||||
|
description = "Site configuration",
|
||||||
|
content = @Content(schema = @Schema(implementation = SiteConfigDto.class))
|
||||||
|
)
|
||||||
public SiteConfigDto getConfig() {
|
public SiteConfigDto getConfig() {
|
||||||
SiteConfigDto resp = new SiteConfigDto();
|
SiteConfigDto resp = new SiteConfigDto();
|
||||||
resp.setCaptchaEnabled(captchaEnabled);
|
resp.setCaptchaEnabled(captchaEnabled);
|
||||||
|
|||||||
@@ -5,40 +5,54 @@ 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 DraftService draftService;
|
||||||
private final DraftMapper draftMapper;
|
private final DraftMapper draftMapper;
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@Operation(summary = "Save draft", description = "Save a draft for current user")
|
@Operation(summary = "Save draft", description = "Save a draft for current user")
|
||||||
@ApiResponse(responseCode = "200", description = "Draft saved",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = DraftDto.class)))
|
responseCode = "200",
|
||||||
|
description = "Draft saved",
|
||||||
|
content = @Content(schema = @Schema(implementation = DraftDto.class))
|
||||||
|
)
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public ResponseEntity<DraftDto> saveDraft(@RequestBody DraftRequest req, Authentication auth) {
|
public ResponseEntity<DraftDto> saveDraft(@RequestBody DraftRequest req, Authentication auth) {
|
||||||
Draft draft = draftService.saveDraft(auth.getName(), req.getCategoryId(), req.getTitle(), req.getContent(), req.getTagIds());
|
Draft draft = draftService.saveDraft(
|
||||||
|
auth.getName(),
|
||||||
|
req.getCategoryId(),
|
||||||
|
req.getTitle(),
|
||||||
|
req.getContent(),
|
||||||
|
req.getTagIds()
|
||||||
|
);
|
||||||
return ResponseEntity.ok(draftMapper.toDto(draft));
|
return ResponseEntity.ok(draftMapper.toDto(draft));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/me")
|
@GetMapping("/me")
|
||||||
@Operation(summary = "Get my draft", description = "Get current user's draft")
|
@Operation(summary = "Get my draft", description = "Get current user's draft")
|
||||||
@ApiResponse(responseCode = "200", description = "Draft details",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = DraftDto.class)))
|
responseCode = "200",
|
||||||
|
description = "Draft details",
|
||||||
|
content = @Content(schema = @Schema(implementation = DraftDto.class))
|
||||||
|
)
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public ResponseEntity<DraftDto> getMyDraft(Authentication auth) {
|
public ResponseEntity<DraftDto> getMyDraft(Authentication auth) {
|
||||||
return draftService.getDraft(auth.getName())
|
return draftService
|
||||||
|
.getDraft(auth.getName())
|
||||||
.map(d -> ResponseEntity.ok(draftMapper.toDto(d)))
|
.map(d -> ResponseEntity.ok(draftMapper.toDto(d)))
|
||||||
.orElseGet(() -> ResponseEntity.noContent().build());
|
.orElseGet(() -> ResponseEntity.noContent().build());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
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)
|
||||||
@@ -37,4 +37,3 @@ public class GlobalExceptionHandler {
|
|||||||
return ResponseEntity.badRequest().body(Map.of("error", message));
|
return ResponseEntity.badRequest().body(Map.of("error", message));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,17 +5,21 @@ 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")
|
@GetMapping("/api/hello")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "Hello endpoint", description = "Returns a greeting for authenticated users")
|
@Operation(summary = "Hello endpoint", description = "Returns a greeting for authenticated users")
|
||||||
@ApiResponse(responseCode = "200", description = "Greeting payload",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = Map.class)))
|
responseCode = "200",
|
||||||
|
description = "Greeting payload",
|
||||||
|
content = @Content(schema = @Schema(implementation = Map.class))
|
||||||
|
)
|
||||||
public Map<String, String> hello() {
|
public Map<String, String> hello() {
|
||||||
return Map.of("message", "Hello, Authenticated User");
|
return Map.of("message", "Hello, Authenticated User");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,32 @@
|
|||||||
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;
|
private final InviteService inviteService;
|
||||||
|
|
||||||
@PostMapping("/generate")
|
@PostMapping("/generate")
|
||||||
@Operation(summary = "Generate invite", description = "Generate an invite token")
|
@Operation(summary = "Generate invite", description = "Generate an invite token")
|
||||||
@ApiResponse(responseCode = "200", description = "Invite token",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = Map.class)))
|
responseCode = "200",
|
||||||
|
description = "Invite token",
|
||||||
|
content = @Content(schema = @Schema(implementation = Map.class))
|
||||||
|
)
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public Map<String, String> generate(Authentication auth) {
|
public Map<String, String> generate(Authentication auth) {
|
||||||
String token = inviteService.generate(auth.getName());
|
String token = inviteService.generate(auth.getName());
|
||||||
|
|||||||
@@ -3,29 +3,32 @@ 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;
|
private final MedalService medalService;
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@Operation(summary = "List medals", description = "List medals for user or globally")
|
@Operation(summary = "List medals", description = "List medals for user or globally")
|
||||||
@ApiResponse(responseCode = "200", description = "List of medals",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = MedalDto.class))))
|
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) {
|
public List<MedalDto> getMedals(@RequestParam(value = "userId", required = false) Long userId) {
|
||||||
return medalService.getMedals(userId);
|
return medalService.getMedals(userId);
|
||||||
}
|
}
|
||||||
@@ -34,7 +37,10 @@ public class MedalController {
|
|||||||
@Operation(summary = "Select medal", description = "Select a medal for current user")
|
@Operation(summary = "Select medal", description = "Select a medal for current user")
|
||||||
@ApiResponse(responseCode = "200", description = "Medal selected")
|
@ApiResponse(responseCode = "200", description = "Medal selected")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public ResponseEntity<Void> selectMedal(@RequestBody MedalSelectRequest req, Authentication auth) {
|
public ResponseEntity<Void> selectMedal(
|
||||||
|
@RequestBody MedalSelectRequest req,
|
||||||
|
Authentication auth
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
medalService.selectMedal(auth.getName(), req.getType());
|
medalService.selectMedal(auth.getName(), req.getType());
|
||||||
return ResponseEntity.ok().build();
|
return ResponseEntity.ok().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,14 +25,6 @@ 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")
|
||||||
@@ -37,15 +36,22 @@ public class MessageController {
|
|||||||
|
|
||||||
// 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
|
||||||
|
.findByUsername(auth.getName())
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Sender not found"));
|
||||||
// In a real application, you would get this from the Authentication object
|
// In a real application, you would get this from the Authentication object
|
||||||
return user.getId();
|
return user.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/conversations")
|
@GetMapping("/conversations")
|
||||||
@Operation(summary = "List conversations", description = "Get all conversations of current user")
|
@Operation(summary = "List conversations", description = "Get all conversations of current user")
|
||||||
@ApiResponse(responseCode = "200", description = "List of conversations",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ConversationDto.class))))
|
responseCode = "200",
|
||||||
|
description = "List of conversations",
|
||||||
|
content = @Content(
|
||||||
|
array = @ArraySchema(schema = @Schema(implementation = ConversationDto.class))
|
||||||
|
)
|
||||||
|
)
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public ResponseEntity<List<ConversationDto>> getConversations(Authentication auth) {
|
public ResponseEntity<List<ConversationDto>> getConversations(Authentication auth) {
|
||||||
List<ConversationDto> conversations = messageService.getConversations(getCurrentUserId(auth));
|
List<ConversationDto> conversations = messageService.getConversations(getCurrentUserId(auth));
|
||||||
@@ -54,42 +60,75 @@ public class MessageController {
|
|||||||
|
|
||||||
@GetMapping("/conversations/{conversationId}")
|
@GetMapping("/conversations/{conversationId}")
|
||||||
@Operation(summary = "Get conversation", description = "Get messages of a conversation")
|
@Operation(summary = "Get conversation", description = "Get messages of a conversation")
|
||||||
@ApiResponse(responseCode = "200", description = "Conversation detail",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = ConversationDetailDto.class)))
|
responseCode = "200",
|
||||||
|
description = "Conversation detail",
|
||||||
|
content = @Content(schema = @Schema(implementation = ConversationDetailDto.class))
|
||||||
|
)
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public ResponseEntity<ConversationDetailDto> getMessages(@PathVariable Long conversationId,
|
public ResponseEntity<ConversationDetailDto> getMessages(
|
||||||
|
@PathVariable Long conversationId,
|
||||||
@RequestParam(defaultValue = "0") int page,
|
@RequestParam(defaultValue = "0") int page,
|
||||||
@RequestParam(defaultValue = "20") int size,
|
@RequestParam(defaultValue = "20") int size,
|
||||||
Authentication auth) {
|
Authentication auth
|
||||||
|
) {
|
||||||
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
|
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
|
||||||
ConversationDetailDto conversationDetails = messageService.getConversationDetails(conversationId, getCurrentUserId(auth), pageable);
|
ConversationDetailDto conversationDetails = messageService.getConversationDetails(
|
||||||
|
conversationId,
|
||||||
|
getCurrentUserId(auth),
|
||||||
|
pageable
|
||||||
|
);
|
||||||
return ResponseEntity.ok(conversationDetails);
|
return ResponseEntity.ok(conversationDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@Operation(summary = "Send message", description = "Send a direct message to a user")
|
@Operation(summary = "Send message", description = "Send a direct message to a user")
|
||||||
@ApiResponse(responseCode = "200", description = "Message sent",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = MessageDto.class)))
|
responseCode = "200",
|
||||||
|
description = "Message sent",
|
||||||
|
content = @Content(schema = @Schema(implementation = MessageDto.class))
|
||||||
|
)
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public ResponseEntity<MessageDto> sendMessage(@RequestBody MessageRequest req, Authentication auth) {
|
public ResponseEntity<MessageDto> sendMessage(
|
||||||
Message message = messageService.sendMessage(getCurrentUserId(auth), req.getRecipientId(), req.getContent(), req.getReplyToId());
|
@RequestBody MessageRequest req,
|
||||||
|
Authentication auth
|
||||||
|
) {
|
||||||
|
Message message = messageService.sendMessage(
|
||||||
|
getCurrentUserId(auth),
|
||||||
|
req.getRecipientId(),
|
||||||
|
req.getContent(),
|
||||||
|
req.getReplyToId()
|
||||||
|
);
|
||||||
return ResponseEntity.ok(messageService.toDto(message));
|
return ResponseEntity.ok(messageService.toDto(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/conversations/{conversationId}/messages")
|
@PostMapping("/conversations/{conversationId}/messages")
|
||||||
@Operation(summary = "Send message to conversation", description = "Reply within a conversation")
|
@Operation(summary = "Send message to conversation", description = "Reply within a conversation")
|
||||||
@ApiResponse(responseCode = "200", description = "Message sent",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = MessageDto.class)))
|
responseCode = "200",
|
||||||
|
description = "Message sent",
|
||||||
|
content = @Content(schema = @Schema(implementation = MessageDto.class))
|
||||||
|
)
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public ResponseEntity<MessageDto> sendMessageToConversation(@PathVariable Long conversationId,
|
public ResponseEntity<MessageDto> sendMessageToConversation(
|
||||||
|
@PathVariable Long conversationId,
|
||||||
@RequestBody ChannelMessageRequest req,
|
@RequestBody ChannelMessageRequest req,
|
||||||
Authentication auth) {
|
Authentication auth
|
||||||
Message message = messageService.sendMessageToConversation(getCurrentUserId(auth), conversationId, req.getContent(), req.getReplyToId());
|
) {
|
||||||
|
Message message = messageService.sendMessageToConversation(
|
||||||
|
getCurrentUserId(auth),
|
||||||
|
conversationId,
|
||||||
|
req.getContent(),
|
||||||
|
req.getReplyToId()
|
||||||
|
);
|
||||||
return ResponseEntity.ok(messageService.toDto(message));
|
return ResponseEntity.ok(messageService.toDto(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/conversations/{conversationId}/read")
|
@PostMapping("/conversations/{conversationId}/read")
|
||||||
@Operation(summary = "Mark conversation read", description = "Mark messages in conversation as read")
|
@Operation(
|
||||||
|
summary = "Mark conversation read",
|
||||||
|
description = "Mark messages in conversation as read"
|
||||||
|
)
|
||||||
@ApiResponse(responseCode = "200", description = "Marked as read")
|
@ApiResponse(responseCode = "200", description = "Marked as read")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public ResponseEntity<Void> markAsRead(@PathVariable Long conversationId, Authentication auth) {
|
public ResponseEntity<Void> markAsRead(@PathVariable Long conversationId, Authentication auth) {
|
||||||
@@ -98,19 +137,37 @@ public class MessageController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/conversations")
|
@PostMapping("/conversations")
|
||||||
@Operation(summary = "Find or create conversation", description = "Find existing or create new conversation with recipient")
|
@Operation(
|
||||||
@ApiResponse(responseCode = "200", description = "Conversation id",
|
summary = "Find or create conversation",
|
||||||
content = @Content(schema = @Schema(implementation = CreateConversationResponse.class)))
|
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")
|
@SecurityRequirement(name = "JWT")
|
||||||
public ResponseEntity<CreateConversationResponse> findOrCreateConversation(@RequestBody CreateConversationRequest req, Authentication auth) {
|
public ResponseEntity<CreateConversationResponse> findOrCreateConversation(
|
||||||
MessageConversation conversation = messageService.findOrCreateConversation(getCurrentUserId(auth), req.getRecipientId());
|
@RequestBody CreateConversationRequest req,
|
||||||
|
Authentication auth
|
||||||
|
) {
|
||||||
|
MessageConversation conversation = messageService.findOrCreateConversation(
|
||||||
|
getCurrentUserId(auth),
|
||||||
|
req.getRecipientId()
|
||||||
|
);
|
||||||
return ResponseEntity.ok(new CreateConversationResponse(conversation.getId()));
|
return ResponseEntity.ok(new CreateConversationResponse(conversation.getId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/unread-count")
|
@GetMapping("/unread-count")
|
||||||
@Operation(summary = "Unread message count", description = "Get unread message count for current user")
|
@Operation(
|
||||||
@ApiResponse(responseCode = "200", description = "Unread count",
|
summary = "Unread message count",
|
||||||
content = @Content(schema = @Schema(implementation = Long.class)))
|
description = "Get unread message count for current user"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Unread count",
|
||||||
|
content = @Content(schema = @Schema(implementation = Long.class))
|
||||||
|
)
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public ResponseEntity<Long> getUnreadCount(Authentication auth) {
|
public ResponseEntity<Long> getUnreadCount(Authentication auth) {
|
||||||
return ResponseEntity.ok(messageService.getUnreadMessageCount(getCurrentUserId(auth)));
|
return ResponseEntity.ok(messageService.getUnreadMessageCount(getCurrentUserId(auth)));
|
||||||
@@ -118,6 +175,7 @@ public class MessageController {
|
|||||||
|
|
||||||
// A simple request DTO
|
// A simple request DTO
|
||||||
static class MessageRequest {
|
static class MessageRequest {
|
||||||
|
|
||||||
private Long recipientId;
|
private Long recipientId;
|
||||||
private String content;
|
private String content;
|
||||||
private Long replyToId;
|
private Long replyToId;
|
||||||
@@ -148,6 +206,7 @@ public class MessageController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static class ChannelMessageRequest {
|
static class ChannelMessageRequest {
|
||||||
|
|
||||||
private String content;
|
private String content;
|
||||||
private Long replyToId;
|
private Long replyToId;
|
||||||
|
|
||||||
|
|||||||
@@ -2,62 +2,89 @@ 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 NotificationService notificationService;
|
||||||
private final NotificationMapper notificationMapper;
|
private final NotificationMapper notificationMapper;
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@Operation(summary = "List notifications", description = "Retrieve notifications for the current user")
|
@Operation(
|
||||||
@ApiResponse(responseCode = "200", description = "Notifications",
|
summary = "List notifications",
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = NotificationDto.class))))
|
description = "Retrieve notifications for the current user"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Notifications",
|
||||||
|
content = @Content(
|
||||||
|
array = @ArraySchema(schema = @Schema(implementation = NotificationDto.class))
|
||||||
|
)
|
||||||
|
)
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public List<NotificationDto> list(@RequestParam(value = "page", defaultValue = "0") int page,
|
public List<NotificationDto> list(
|
||||||
|
@RequestParam(value = "page", defaultValue = "0") int page,
|
||||||
@RequestParam(value = "size", defaultValue = "30") int size,
|
@RequestParam(value = "size", defaultValue = "30") int size,
|
||||||
Authentication auth) {
|
Authentication auth
|
||||||
return notificationService.listNotifications(auth.getName(), null, page, size).stream()
|
) {
|
||||||
|
return notificationService
|
||||||
|
.listNotifications(auth.getName(), null, page, size)
|
||||||
|
.stream()
|
||||||
.map(notificationMapper::toDto)
|
.map(notificationMapper::toDto)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/unread")
|
@GetMapping("/unread")
|
||||||
@Operation(summary = "List unread notifications", description = "Retrieve unread notifications for the current user")
|
@Operation(
|
||||||
@ApiResponse(responseCode = "200", description = "Unread notifications",
|
summary = "List unread notifications",
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = NotificationDto.class))))
|
description = "Retrieve unread notifications for the current user"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Unread notifications",
|
||||||
|
content = @Content(
|
||||||
|
array = @ArraySchema(schema = @Schema(implementation = NotificationDto.class))
|
||||||
|
)
|
||||||
|
)
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public List<NotificationDto> listUnread(@RequestParam(value = "page", defaultValue = "0") int page,
|
public List<NotificationDto> listUnread(
|
||||||
|
@RequestParam(value = "page", defaultValue = "0") int page,
|
||||||
@RequestParam(value = "size", defaultValue = "30") int size,
|
@RequestParam(value = "size", defaultValue = "30") int size,
|
||||||
Authentication auth) {
|
Authentication auth
|
||||||
return notificationService.listNotifications(auth.getName(), false, page, size).stream()
|
) {
|
||||||
|
return notificationService
|
||||||
|
.listNotifications(auth.getName(), false, page, size)
|
||||||
|
.stream()
|
||||||
.map(notificationMapper::toDto)
|
.map(notificationMapper::toDto)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/unread-count")
|
@GetMapping("/unread-count")
|
||||||
@Operation(summary = "Unread count", description = "Get count of unread notifications")
|
@Operation(summary = "Unread count", description = "Get count of unread notifications")
|
||||||
@ApiResponse(responseCode = "200", description = "Unread count",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = NotificationUnreadCountDto.class)))
|
responseCode = "200",
|
||||||
|
description = "Unread count",
|
||||||
|
content = @Content(schema = @Schema(implementation = NotificationUnreadCountDto.class))
|
||||||
|
)
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public NotificationUnreadCountDto unreadCount(Authentication auth) {
|
public NotificationUnreadCountDto unreadCount(Authentication auth) {
|
||||||
long count = notificationService.countUnread(auth.getName());
|
long count = notificationService.countUnread(auth.getName());
|
||||||
@@ -76,8 +103,13 @@ public class NotificationController {
|
|||||||
|
|
||||||
@GetMapping("/prefs")
|
@GetMapping("/prefs")
|
||||||
@Operation(summary = "List preferences", description = "List notification preferences")
|
@Operation(summary = "List preferences", description = "List notification preferences")
|
||||||
@ApiResponse(responseCode = "200", description = "Preferences",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = NotificationPreferenceDto.class))))
|
responseCode = "200",
|
||||||
|
description = "Preferences",
|
||||||
|
content = @Content(
|
||||||
|
array = @ArraySchema(schema = @Schema(implementation = NotificationPreferenceDto.class))
|
||||||
|
)
|
||||||
|
)
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public List<NotificationPreferenceDto> prefs(Authentication auth) {
|
public List<NotificationPreferenceDto> prefs(Authentication auth) {
|
||||||
return notificationService.listPreferences(auth.getName());
|
return notificationService.listPreferences(auth.getName());
|
||||||
@@ -87,24 +119,41 @@ public class NotificationController {
|
|||||||
@Operation(summary = "Update preference", description = "Update notification preference")
|
@Operation(summary = "Update preference", description = "Update notification preference")
|
||||||
@ApiResponse(responseCode = "200", description = "Preference updated")
|
@ApiResponse(responseCode = "200", description = "Preference updated")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public void updatePref(@RequestBody NotificationPreferenceUpdateRequest req, Authentication auth) {
|
public void updatePref(
|
||||||
|
@RequestBody NotificationPreferenceUpdateRequest req,
|
||||||
|
Authentication auth
|
||||||
|
) {
|
||||||
notificationService.updatePreference(auth.getName(), req.getType(), req.isEnabled());
|
notificationService.updatePreference(auth.getName(), req.getType(), req.isEnabled());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/email-prefs")
|
@GetMapping("/email-prefs")
|
||||||
@Operation(summary = "List email preferences", description = "List email notification preferences")
|
@Operation(
|
||||||
@ApiResponse(responseCode = "200", description = "Email preferences",
|
summary = "List email preferences",
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = NotificationPreferenceDto.class))))
|
description = "List email notification preferences"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Email preferences",
|
||||||
|
content = @Content(
|
||||||
|
array = @ArraySchema(schema = @Schema(implementation = NotificationPreferenceDto.class))
|
||||||
|
)
|
||||||
|
)
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public List<NotificationPreferenceDto> emailPrefs(Authentication auth) {
|
public List<NotificationPreferenceDto> emailPrefs(Authentication auth) {
|
||||||
return notificationService.listEmailPreferences(auth.getName());
|
return notificationService.listEmailPreferences(auth.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/email-prefs")
|
@PostMapping("/email-prefs")
|
||||||
@Operation(summary = "Update email preference", description = "Update email notification preference")
|
@Operation(
|
||||||
|
summary = "Update email preference",
|
||||||
|
description = "Update email notification preference"
|
||||||
|
)
|
||||||
@ApiResponse(responseCode = "200", description = "Email preference updated")
|
@ApiResponse(responseCode = "200", description = "Email preference updated")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public void updateEmailPref(@RequestBody NotificationPreferenceUpdateRequest req, Authentication auth) {
|
public void updateEmailPref(
|
||||||
|
@RequestBody NotificationPreferenceUpdateRequest req,
|
||||||
|
Authentication auth
|
||||||
|
) {
|
||||||
notificationService.updateEmailPreference(auth.getName(), req.getType(), req.isEnabled());
|
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
|
||||||
@@ -23,20 +22,23 @@ import java.time.Duration;
|
|||||||
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 PointService pointService;
|
||||||
private final PointHistoryMapper pointHistoryMapper;
|
private final PointHistoryMapper pointHistoryMapper;
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@Operation(summary = "Point history", description = "List point history for current user")
|
@Operation(summary = "Point history", description = "List point history for current user")
|
||||||
@ApiResponse(responseCode = "200", description = "List of point histories",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PointHistoryDto.class))))
|
responseCode = "200",
|
||||||
|
description = "List of point histories",
|
||||||
|
content = @Content(
|
||||||
|
array = @ArraySchema(schema = @Schema(implementation = PointHistoryDto.class))
|
||||||
|
)
|
||||||
|
)
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public List<PointHistoryDto> list(Authentication auth) {
|
public List<PointHistoryDto> list(Authentication auth) {
|
||||||
return pointService.listHistory(auth.getName()).stream()
|
return pointService
|
||||||
|
.listHistory(auth.getName())
|
||||||
|
.stream()
|
||||||
.map(pointHistoryMapper::toDto)
|
.map(pointHistoryMapper::toDto)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/trend")
|
@GetMapping("/trend")
|
||||||
@Operation(summary = "Point trend", description = "Get point trend data for current user")
|
@Operation(summary = "Point trend", description = "Get point trend data for current user")
|
||||||
@ApiResponse(responseCode = "200", description = "Trend data",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = java.util.Map.class))))
|
responseCode = "200",
|
||||||
|
description = "Trend data",
|
||||||
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = java.util.Map.class)))
|
||||||
|
)
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public List<Map<String, Object>> trend(Authentication auth,
|
public List<Map<String, Object>> trend(
|
||||||
@RequestParam(value = "days", defaultValue = "30") int days) {
|
Authentication auth,
|
||||||
|
@RequestParam(value = "days", defaultValue = "30") int days
|
||||||
|
) {
|
||||||
return pointService.trend(auth.getName(), days);
|
return pointService.trend(auth.getName(), days);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,43 +6,51 @@ 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 PointMallService pointMallService;
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
private final PointGoodMapper pointGoodMapper;
|
private final PointGoodMapper pointGoodMapper;
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@Operation(summary = "List goods", description = "List all point goods")
|
@Operation(summary = "List goods", description = "List all point goods")
|
||||||
@ApiResponse(responseCode = "200", description = "List of goods",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PointGoodDto.class))))
|
responseCode = "200",
|
||||||
|
description = "List of goods",
|
||||||
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PointGoodDto.class)))
|
||||||
|
)
|
||||||
public List<PointGoodDto> list() {
|
public List<PointGoodDto> list() {
|
||||||
return pointMallService.listGoods().stream()
|
return pointMallService
|
||||||
|
.listGoods()
|
||||||
|
.stream()
|
||||||
.map(pointGoodMapper::toDto)
|
.map(pointGoodMapper::toDto)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/redeem")
|
@PostMapping("/redeem")
|
||||||
@Operation(summary = "Redeem good", description = "Redeem a point good")
|
@Operation(summary = "Redeem good", description = "Redeem a point good")
|
||||||
@ApiResponse(responseCode = "200", description = "Remaining points",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = java.util.Map.class)))
|
responseCode = "200",
|
||||||
|
description = "Remaining points",
|
||||||
|
content = @Content(schema = @Schema(implementation = java.util.Map.class))
|
||||||
|
)
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public Map<String, Integer> redeem(@RequestBody PointRedeemRequest req, Authentication auth) {
|
public Map<String, Integer> redeem(@RequestBody PointRedeemRequest req, Authentication auth) {
|
||||||
User user = userService.findByIdentifier(auth.getName()).orElseThrow();
|
User user = userService.findByIdentifier(auth.getName()).orElseThrow();
|
||||||
|
|||||||
@@ -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 PostChangeLogService changeLogService;
|
||||||
private final PostChangeLogMapper mapper;
|
private final PostChangeLogMapper mapper;
|
||||||
|
|
||||||
@GetMapping("/{id}/change-logs")
|
@GetMapping("/{id}/change-logs")
|
||||||
@Operation(summary = "Post change logs", description = "List change logs for a post")
|
@Operation(summary = "Post change logs", description = "List change logs for a post")
|
||||||
@ApiResponse(responseCode = "200", description = "Change logs",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostChangeLogDto.class))))
|
responseCode = "200",
|
||||||
|
description = "Change logs",
|
||||||
|
content = @Content(
|
||||||
|
array = @ArraySchema(schema = @Schema(implementation = PostChangeLogDto.class))
|
||||||
|
)
|
||||||
|
)
|
||||||
public List<PostChangeLogDto> listLogs(@PathVariable Long id) {
|
public List<PostChangeLogDto> listLogs(@PathVariable Long id) {
|
||||||
return changeLogService.listLogs(id).stream()
|
return changeLogService.listLogs(id).stream().map(mapper::toDto).collect(Collectors.toList());
|
||||||
.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,20 +14,23 @@ 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 PostService postService;
|
||||||
|
private final CategoryService categoryService;
|
||||||
|
private final TagService tagService;
|
||||||
private final LevelService levelService;
|
private final LevelService levelService;
|
||||||
private final CaptchaService captchaService;
|
private final CaptchaService captchaService;
|
||||||
private final DraftService draftService;
|
private final DraftService draftService;
|
||||||
@@ -43,18 +47,34 @@ public class PostController {
|
|||||||
@PostMapping
|
@PostMapping
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "Create post", description = "Create a new post")
|
@Operation(summary = "Create post", description = "Create a new post")
|
||||||
@ApiResponse(responseCode = "200", description = "Created post",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = PostDetailDto.class)))
|
responseCode = "200",
|
||||||
public ResponseEntity<PostDetailDto> createPost(@RequestBody PostRequest req, Authentication auth) {
|
description = "Created post",
|
||||||
|
content = @Content(schema = @Schema(implementation = PostDetailDto.class))
|
||||||
|
)
|
||||||
|
public ResponseEntity<PostDetailDto> createPost(
|
||||||
|
@RequestBody PostRequest req,
|
||||||
|
Authentication auth
|
||||||
|
) {
|
||||||
if (captchaEnabled && postCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
|
if (captchaEnabled && postCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
|
||||||
return ResponseEntity.badRequest().build();
|
return ResponseEntity.badRequest().build();
|
||||||
}
|
}
|
||||||
Post post = postService.createPost(auth.getName(), req.getCategoryId(),
|
Post post = postService.createPost(
|
||||||
req.getTitle(), req.getContent(), req.getTagIds(),
|
auth.getName(),
|
||||||
req.getType(), req.getPrizeDescription(), req.getPrizeIcon(),
|
req.getCategoryId(),
|
||||||
req.getPrizeCount(), req.getPointCost(),
|
req.getTitle(),
|
||||||
req.getStartTime(), req.getEndTime(),
|
req.getContent(),
|
||||||
req.getOptions(), req.getMultiple());
|
req.getTagIds(),
|
||||||
|
req.getType(),
|
||||||
|
req.getPrizeDescription(),
|
||||||
|
req.getPrizeIcon(),
|
||||||
|
req.getPrizeCount(),
|
||||||
|
req.getPointCost(),
|
||||||
|
req.getStartTime(),
|
||||||
|
req.getEndTime(),
|
||||||
|
req.getOptions(),
|
||||||
|
req.getMultiple()
|
||||||
|
);
|
||||||
draftService.deleteDraft(auth.getName());
|
draftService.deleteDraft(auth.getName());
|
||||||
PostDetailDto dto = postMapper.toDetailDto(post, auth.getName());
|
PostDetailDto dto = postMapper.toDetailDto(post, auth.getName());
|
||||||
dto.setReward(levelService.awardForPost(auth.getName()));
|
dto.setReward(levelService.awardForPost(auth.getName()));
|
||||||
@@ -65,12 +85,24 @@ public class PostController {
|
|||||||
@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(
|
||||||
|
@PathVariable Long id,
|
||||||
|
@RequestBody PostRequest req,
|
||||||
|
Authentication auth
|
||||||
|
) {
|
||||||
|
Post post = postService.updatePost(
|
||||||
|
id,
|
||||||
|
auth.getName(),
|
||||||
|
req.getCategoryId(),
|
||||||
|
req.getTitle(),
|
||||||
|
req.getContent(),
|
||||||
|
req.getTagIds()
|
||||||
|
);
|
||||||
return ResponseEntity.ok(postMapper.toDetailDto(post, auth.getName()));
|
return ResponseEntity.ok(postMapper.toDetailDto(post, auth.getName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,8 +117,11 @@ public class PostController {
|
|||||||
@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",
|
||||||
|
description = "Closed post",
|
||||||
|
content = @Content(schema = @Schema(implementation = PostSummaryDto.class))
|
||||||
|
)
|
||||||
public PostSummaryDto close(@PathVariable Long id, Authentication auth) {
|
public PostSummaryDto close(@PathVariable Long id, Authentication auth) {
|
||||||
return postMapper.toSummaryDto(postService.closePost(id, auth.getName()));
|
return postMapper.toSummaryDto(postService.closePost(id, auth.getName()));
|
||||||
}
|
}
|
||||||
@@ -94,16 +129,22 @@ public class PostController {
|
|||||||
@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",
|
||||||
|
description = "Reopened post",
|
||||||
|
content = @Content(schema = @Schema(implementation = PostSummaryDto.class))
|
||||||
|
)
|
||||||
public PostSummaryDto reopen(@PathVariable Long id, Authentication auth) {
|
public PostSummaryDto reopen(@PathVariable Long id, Authentication auth) {
|
||||||
return postMapper.toSummaryDto(postService.reopenPost(id, auth.getName()));
|
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",
|
||||||
|
description = "Post detail",
|
||||||
|
content = @Content(schema = @Schema(implementation = PostDetailDto.class))
|
||||||
|
)
|
||||||
public ResponseEntity<PostDetailDto> getPost(@PathVariable Long id, Authentication auth) {
|
public ResponseEntity<PostDetailDto> getPost(@PathVariable Long id, Authentication auth) {
|
||||||
String viewer = auth != null ? auth.getName() : null;
|
String viewer = auth != null ? auth.getName() : null;
|
||||||
Post post = postService.viewPost(id, viewer);
|
Post post = postService.viewPost(id, viewer);
|
||||||
@@ -121,8 +162,11 @@ public class PostController {
|
|||||||
|
|
||||||
@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",
|
||||||
|
description = "Poll progress",
|
||||||
|
content = @Content(schema = @Schema(implementation = PollDto.class))
|
||||||
|
)
|
||||||
public ResponseEntity<PollDto> pollProgress(@PathVariable Long id) {
|
public ResponseEntity<PollDto> pollProgress(@PathVariable Long id) {
|
||||||
return ResponseEntity.ok(postMapper.toSummaryDto(postService.getPoll(id)).getPoll());
|
return ResponseEntity.ok(postMapper.toSummaryDto(postService.getPoll(id)).getPoll());
|
||||||
}
|
}
|
||||||
@@ -131,131 +175,144 @@ public class PostController {
|
|||||||
@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(
|
||||||
|
@PathVariable Long id,
|
||||||
|
@RequestParam("option") List<Integer> option,
|
||||||
|
Authentication auth
|
||||||
|
) {
|
||||||
postService.votePoll(id, auth.getName(), option);
|
postService.votePoll(id, auth.getName(), option);
|
||||||
return ResponseEntity.ok().build();
|
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",
|
||||||
|
content = @Content(
|
||||||
|
array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@Cacheable(
|
||||||
|
value = CachingConfig.POST_CACHE_NAME,
|
||||||
|
key = "new org.springframework.cache.interceptor.SimpleKey('default', #categoryId, #categoryIds, #tagId, #tagIds, #page, #pageSize)"
|
||||||
|
)
|
||||||
|
public List<PostSummaryDto> listPosts(
|
||||||
|
@RequestParam(value = "categoryId", required = false) Long categoryId,
|
||||||
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
|
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
|
||||||
@RequestParam(value = "tagId", required = false) Long tagId,
|
@RequestParam(value = "tagId", required = false) Long tagId,
|
||||||
@RequestParam(value = "tagIds", required = false) List<Long> tagIds,
|
@RequestParam(value = "tagIds", required = false) List<Long> tagIds,
|
||||||
@RequestParam(value = "page", required = false) Integer page,
|
@RequestParam(value = "page", required = false) Integer page,
|
||||||
@RequestParam(value = "pageSize", required = false) Integer pageSize,
|
@RequestParam(value = "pageSize", required = false) Integer pageSize,
|
||||||
Authentication auth) {
|
Authentication auth
|
||||||
List<Long> ids = categoryIds;
|
) {
|
||||||
if (categoryId != null) {
|
List<Long> ids = categoryService.getSearchCategoryIds(categoryIds, categoryId);
|
||||||
ids = java.util.List.of(categoryId);
|
List<Long> tids = tagService.getSearchTagIds(tagIds, tagId);
|
||||||
}
|
// 只需要在请求的一开始统计一次
|
||||||
List<Long> tids = tagIds;
|
// if (auth != null) {
|
||||||
if (tagId != null) {
|
// userVisitService.recordVisit(auth.getName());
|
||||||
tids = java.util.List.of(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()
|
||||||
if (hasCategories && hasTags) {
|
.map(postMapper::toSummaryDto)
|
||||||
return postService.listPostsByCategoriesAndTags(ids, tids, page, pageSize)
|
.collect(Collectors.toList());
|
||||||
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
if (hasTags) {
|
|
||||||
return postService.listPostsByTags(tids, page, pageSize)
|
|
||||||
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
return postService.listPostsByCategories(ids, page, pageSize)
|
|
||||||
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/ranking")
|
@GetMapping("/ranking")
|
||||||
@Operation(summary = "Ranking posts", description = "List posts by view rankings")
|
@Operation(summary = "Ranking posts", description = "List posts by view rankings")
|
||||||
@ApiResponse(responseCode = "200", description = "Ranked posts",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
|
responseCode = "200",
|
||||||
public List<PostSummaryDto> rankingPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
|
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 = "categoryIds", required = false) List<Long> categoryIds,
|
||||||
@RequestParam(value = "tagId", required = false) Long tagId,
|
@RequestParam(value = "tagId", required = false) Long tagId,
|
||||||
@RequestParam(value = "tagIds", required = false) List<Long> tagIds,
|
@RequestParam(value = "tagIds", required = false) List<Long> tagIds,
|
||||||
@RequestParam(value = "page", required = false) Integer page,
|
@RequestParam(value = "page", required = false) Integer page,
|
||||||
@RequestParam(value = "pageSize", required = false) Integer pageSize,
|
@RequestParam(value = "pageSize", required = false) Integer pageSize,
|
||||||
Authentication auth) {
|
Authentication auth
|
||||||
List<Long> ids = categoryIds;
|
) {
|
||||||
if (categoryId != null) {
|
List<Long> ids = categoryService.getSearchCategoryIds(categoryIds, categoryId);
|
||||||
ids = java.util.List.of(categoryId);
|
List<Long> tids = tagService.getSearchTagIds(tagIds, tagId);
|
||||||
}
|
// 只需要在请求的一开始统计一次
|
||||||
List<Long> tids = tagIds;
|
// if (auth != null) {
|
||||||
if (tagId != null) {
|
// userVisitService.recordVisit(auth.getName());
|
||||||
tids = java.util.List.of(tagId);
|
// }
|
||||||
}
|
|
||||||
// 只需要在请求的一开始统计一次
|
|
||||||
// if (auth != null) {
|
|
||||||
// userVisitService.recordVisit(auth.getName());
|
|
||||||
// }
|
|
||||||
|
|
||||||
return postService.listPostsByViews(ids, tids, page, pageSize)
|
return postService
|
||||||
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
|
.listPostsByViews(ids, tids, page, pageSize)
|
||||||
|
.stream()
|
||||||
|
.map(postMapper::toSummaryDto)
|
||||||
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/latest-reply")
|
@GetMapping("/latest-reply")
|
||||||
@Operation(summary = "Latest reply posts", description = "List posts by latest replies")
|
@Operation(summary = "Latest reply posts", description = "List posts by latest replies")
|
||||||
@ApiResponse(responseCode = "200", description = "Posts sorted by latest reply",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
|
responseCode = "200",
|
||||||
public List<PostSummaryDto> latestReplyPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
|
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 = "categoryIds", required = false) List<Long> categoryIds,
|
||||||
@RequestParam(value = "tagId", required = false) Long tagId,
|
@RequestParam(value = "tagId", required = false) Long tagId,
|
||||||
@RequestParam(value = "tagIds", required = false) List<Long> tagIds,
|
@RequestParam(value = "tagIds", required = false) List<Long> tagIds,
|
||||||
@RequestParam(value = "page", required = false) Integer page,
|
@RequestParam(value = "page", required = false) Integer page,
|
||||||
@RequestParam(value = "pageSize", required = false) Integer pageSize,
|
@RequestParam(value = "pageSize", required = false) Integer pageSize,
|
||||||
Authentication auth) {
|
Authentication auth
|
||||||
List<Long> ids = categoryIds;
|
) {
|
||||||
if (categoryId != null) {
|
List<Long> ids = categoryService.getSearchCategoryIds(categoryIds, categoryId);
|
||||||
ids = java.util.List.of(categoryId);
|
List<Long> tids = tagService.getSearchTagIds(tagIds, tagId);
|
||||||
}
|
// 只需要在请求的一开始统计一次
|
||||||
List<Long> tids = tagIds;
|
// if (auth != null) {
|
||||||
if (tagId != null) {
|
// userVisitService.recordVisit(auth.getName());
|
||||||
tids = java.util.List.of(tagId);
|
// }
|
||||||
}
|
|
||||||
// 只需要在请求的一开始统计一次
|
|
||||||
// if (auth != null) {
|
|
||||||
// userVisitService.recordVisit(auth.getName());
|
|
||||||
// }
|
|
||||||
|
|
||||||
return postService.listPostsByLatestReply(ids, tids, page, pageSize)
|
List<Post> posts = postService.listPostsByLatestReply(ids, tids, page, pageSize);
|
||||||
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
|
return posts.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/featured")
|
@GetMapping("/featured")
|
||||||
@Operation(summary = "Featured posts", description = "List featured posts")
|
@Operation(summary = "Featured posts", description = "List featured posts")
|
||||||
@ApiResponse(responseCode = "200", description = "Featured posts",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
|
responseCode = "200",
|
||||||
public List<PostSummaryDto> featuredPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
|
description = "Featured posts",
|
||||||
|
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,
|
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
|
||||||
@RequestParam(value = "tagId", required = false) Long tagId,
|
@RequestParam(value = "tagId", required = false) Long tagId,
|
||||||
@RequestParam(value = "tagIds", required = false) List<Long> tagIds,
|
@RequestParam(value = "tagIds", required = false) List<Long> tagIds,
|
||||||
@RequestParam(value = "page", required = false) Integer page,
|
@RequestParam(value = "page", required = false) Integer page,
|
||||||
@RequestParam(value = "pageSize", required = false) Integer pageSize,
|
@RequestParam(value = "pageSize", required = false) Integer pageSize,
|
||||||
Authentication auth) {
|
Authentication auth
|
||||||
List<Long> ids = categoryIds;
|
) {
|
||||||
if (categoryId != null) {
|
List<Long> ids = categoryService.getSearchCategoryIds(categoryIds, categoryId);
|
||||||
ids = java.util.List.of(categoryId);
|
List<Long> tids = tagService.getSearchTagIds(tagIds, tagId);
|
||||||
}
|
// 只需要在请求的一开始统计一次
|
||||||
List<Long> tids = tagIds;
|
// if (auth != null) {
|
||||||
if (tagId != null) {
|
// userVisitService.recordVisit(auth.getName());
|
||||||
tids = java.util.List.of(tagId);
|
// }
|
||||||
}
|
return postService
|
||||||
// 只需要在请求的一开始统计一次
|
.listFeaturedPosts(ids, tids, page, pageSize)
|
||||||
// if (auth != null) {
|
.stream()
|
||||||
// userVisitService.recordVisit(auth.getName());
|
.map(postMapper::toSummaryDto)
|
||||||
// }
|
.collect(Collectors.toList());
|
||||||
return postService.listFeaturedPosts(ids, tids, page, pageSize)
|
|
||||||
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,28 +3,33 @@ 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;
|
private final PushSubscriptionService pushSubscriptionService;
|
||||||
|
|
||||||
@Value("${app.webpush.public-key}")
|
@Value("${app.webpush.public-key}")
|
||||||
private String publicKey;
|
private String publicKey;
|
||||||
|
|
||||||
@GetMapping("/public-key")
|
@GetMapping("/public-key")
|
||||||
@Operation(summary = "Get public key", description = "Retrieve web push public key")
|
@Operation(summary = "Get public key", description = "Retrieve web push public key")
|
||||||
@ApiResponse(responseCode = "200", description = "Public key",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = PushPublicKeyDto.class)))
|
responseCode = "200",
|
||||||
|
description = "Public key",
|
||||||
|
content = @Content(schema = @Schema(implementation = PushPublicKeyDto.class))
|
||||||
|
)
|
||||||
public PushPublicKeyDto getPublicKey() {
|
public PushPublicKeyDto getPublicKey() {
|
||||||
PushPublicKeyDto r = new PushPublicKeyDto();
|
PushPublicKeyDto r = new PushPublicKeyDto();
|
||||||
r.setKey(publicKey);
|
r.setKey(publicKey);
|
||||||
@@ -36,6 +41,11 @@ public class PushSubscriptionController {
|
|||||||
@ApiResponse(responseCode = "200", description = "Subscribed")
|
@ApiResponse(responseCode = "200", description = "Subscribed")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public void subscribe(@RequestBody PushSubscriptionRequest req, Authentication auth) {
|
public void subscribe(@RequestBody PushSubscriptionRequest req, Authentication auth) {
|
||||||
pushSubscriptionService.saveSubscription(auth.getName(), req.getEndpoint(), req.getP256dh(), req.getAuth());
|
pushSubscriptionService.saveSubscription(
|
||||||
|
auth.getName(),
|
||||||
|
req.getEndpoint(),
|
||||||
|
req.getP256dh(),
|
||||||
|
req.getAuth()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,20 +8,21 @@ 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 ReactionService reactionService;
|
||||||
private final LevelService levelService;
|
private final LevelService levelService;
|
||||||
private final ReactionMapper reactionMapper;
|
private final ReactionMapper reactionMapper;
|
||||||
@@ -32,20 +33,28 @@ public class ReactionController {
|
|||||||
*/
|
*/
|
||||||
@GetMapping("/reaction-types")
|
@GetMapping("/reaction-types")
|
||||||
@Operation(summary = "List reaction types", description = "Get all available reaction types")
|
@Operation(summary = "List reaction types", description = "Get all available reaction types")
|
||||||
@ApiResponse(responseCode = "200", description = "Reaction types",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = ReactionType[].class)))
|
responseCode = "200",
|
||||||
|
description = "Reaction types",
|
||||||
|
content = @Content(schema = @Schema(implementation = ReactionType[].class))
|
||||||
|
)
|
||||||
public ReactionType[] listReactionTypes() {
|
public ReactionType[] listReactionTypes() {
|
||||||
return ReactionType.values();
|
return ReactionType.values();
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/posts/{postId}/reactions")
|
@PostMapping("/posts/{postId}/reactions")
|
||||||
@Operation(summary = "React to post", description = "React to a post")
|
@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",
|
||||||
|
description = "Reaction result",
|
||||||
|
content = @Content(schema = @Schema(implementation = ReactionDto.class))
|
||||||
|
)
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public ResponseEntity<ReactionDto> reactToPost(@PathVariable Long postId,
|
public ResponseEntity<ReactionDto> reactToPost(
|
||||||
|
@PathVariable Long postId,
|
||||||
@RequestBody ReactionRequest req,
|
@RequestBody ReactionRequest req,
|
||||||
Authentication auth) {
|
Authentication auth
|
||||||
|
) {
|
||||||
Reaction reaction = reactionService.reactToPost(auth.getName(), postId, req.getType());
|
Reaction reaction = reactionService.reactToPost(auth.getName(), postId, req.getType());
|
||||||
if (reaction == null) {
|
if (reaction == null) {
|
||||||
pointService.deductForReactionOfPost(auth.getName(), postId);
|
pointService.deductForReactionOfPost(auth.getName(), postId);
|
||||||
@@ -59,12 +68,17 @@ public class ReactionController {
|
|||||||
|
|
||||||
@PostMapping("/comments/{commentId}/reactions")
|
@PostMapping("/comments/{commentId}/reactions")
|
||||||
@Operation(summary = "React to comment", description = "React to a comment")
|
@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",
|
||||||
|
description = "Reaction result",
|
||||||
|
content = @Content(schema = @Schema(implementation = ReactionDto.class))
|
||||||
|
)
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public ResponseEntity<ReactionDto> reactToComment(@PathVariable Long commentId,
|
public ResponseEntity<ReactionDto> reactToComment(
|
||||||
|
@PathVariable Long commentId,
|
||||||
@RequestBody ReactionRequest req,
|
@RequestBody ReactionRequest req,
|
||||||
Authentication auth) {
|
Authentication auth
|
||||||
|
) {
|
||||||
Reaction reaction = reactionService.reactToComment(auth.getName(), commentId, req.getType());
|
Reaction reaction = reactionService.reactToComment(auth.getName(), commentId, req.getType());
|
||||||
if (reaction == null) {
|
if (reaction == null) {
|
||||||
pointService.deductForReactionOfComment(auth.getName(), commentId);
|
pointService.deductForReactionOfComment(auth.getName(), commentId);
|
||||||
@@ -78,12 +92,17 @@ public class ReactionController {
|
|||||||
|
|
||||||
@PostMapping("/messages/{messageId}/reactions")
|
@PostMapping("/messages/{messageId}/reactions")
|
||||||
@Operation(summary = "React to message", description = "React to a message")
|
@Operation(summary = "React to message", description = "React to a message")
|
||||||
@ApiResponse(responseCode = "200", description = "Reaction result",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = ReactionDto.class)))
|
responseCode = "200",
|
||||||
|
description = "Reaction result",
|
||||||
|
content = @Content(schema = @Schema(implementation = ReactionDto.class))
|
||||||
|
)
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public ResponseEntity<ReactionDto> reactToMessage(@PathVariable Long messageId,
|
public ResponseEntity<ReactionDto> reactToMessage(
|
||||||
|
@PathVariable Long messageId,
|
||||||
@RequestBody ReactionRequest req,
|
@RequestBody ReactionRequest req,
|
||||||
Authentication auth) {
|
Authentication auth
|
||||||
|
) {
|
||||||
Reaction reaction = reactionService.reactToMessage(auth.getName(), messageId, req.getType());
|
Reaction reaction = reactionService.reactToMessage(auth.getName(), messageId, req.getType());
|
||||||
if (reaction == null) {
|
if (reaction == null) {
|
||||||
return ResponseEntity.noContent().build();
|
return ResponseEntity.noContent().build();
|
||||||
|
|||||||
@@ -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,30 +31,11 @@ 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 PostService postService;
|
||||||
private final CommentService commentService;
|
private final CommentService commentService;
|
||||||
|
|
||||||
@@ -45,21 +44,27 @@ public class RssController {
|
|||||||
|
|
||||||
// 兼容 Markdown/HTML 两类图片写法(用于 enclosure)
|
// 兼容 Markdown/HTML 两类图片写法(用于 enclosure)
|
||||||
private static final Pattern MD_IMAGE = Pattern.compile("!\\[[^\\]]*\\]\\(([^)]+)\\)");
|
private static final Pattern MD_IMAGE = Pattern.compile("!\\[[^\\]]*\\]\\(([^)]+)\\)");
|
||||||
private static final Pattern HTML_IMAGE = Pattern.compile("<BaseImage[^>]+src=[\"']?([^\"'>]+)[\"']?[^>]*>");
|
private static final Pattern HTML_IMAGE = Pattern.compile(
|
||||||
|
"<BaseImage[^>]+src=[\"']?([^\"'>]+)[\"']?[^>]*>"
|
||||||
|
);
|
||||||
|
|
||||||
private static final DateTimeFormatter RFC1123 = DateTimeFormatter.RFC_1123_DATE_TIME;
|
private static final DateTimeFormatter RFC1123 = DateTimeFormatter.RFC_1123_DATE_TIME;
|
||||||
|
|
||||||
// flexmark:Markdown -> HTML
|
// flexmark:Markdown -> HTML
|
||||||
private static final Parser MD_PARSER;
|
private static final Parser MD_PARSER;
|
||||||
private static final HtmlRenderer MD_RENDERER;
|
private static final HtmlRenderer MD_RENDERER;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
MutableDataSet opts = new MutableDataSet();
|
MutableDataSet opts = new MutableDataSet();
|
||||||
opts.set(Parser.EXTENSIONS, Arrays.asList(
|
opts.set(
|
||||||
|
Parser.EXTENSIONS,
|
||||||
|
Arrays.asList(
|
||||||
TablesExtension.create(),
|
TablesExtension.create(),
|
||||||
AutolinkExtension.create(),
|
AutolinkExtension.create(),
|
||||||
StrikethroughExtension.create(),
|
StrikethroughExtension.create(),
|
||||||
TaskListExtension.create()
|
TaskListExtension.create()
|
||||||
));
|
)
|
||||||
|
);
|
||||||
// 允许内联 HTML(下游再做 sanitize)
|
// 允许内联 HTML(下游再做 sanitize)
|
||||||
opts.set(Parser.HTML_BLOCK_PARSER, true);
|
opts.set(Parser.HTML_BLOCK_PARSER, true);
|
||||||
MD_PARSER = Parser.builder(opts).build();
|
MD_PARSER = Parser.builder(opts).build();
|
||||||
@@ -68,7 +73,11 @@ public class RssController {
|
|||||||
|
|
||||||
@GetMapping(value = "/api/rss", produces = "application/rss+xml;charset=UTF-8")
|
@GetMapping(value = "/api/rss", produces = "application/rss+xml;charset=UTF-8")
|
||||||
@Operation(summary = "RSS feed", description = "Generate RSS feed for latest posts")
|
@Operation(summary = "RSS feed", description = "Generate RSS feed for latest posts")
|
||||||
@ApiResponse(responseCode = "200", description = "RSS XML", content = @Content(schema = @Schema(implementation = String.class)))
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "RSS XML",
|
||||||
|
content = @Content(schema = @Schema(implementation = String.class))
|
||||||
|
)
|
||||||
public String feed() {
|
public String feed() {
|
||||||
// 建议 20;你现在是 10,这里保留你的 10
|
// 建议 20;你现在是 10,这里保留你的 10
|
||||||
List<Post> posts = postService.listLatestRssPosts(10);
|
List<Post> posts = postService.listLatestRssPosts(10);
|
||||||
@@ -81,7 +90,8 @@ public class RssController {
|
|||||||
elem(sb, "title", cdata("OpenIsle RSS"));
|
elem(sb, "title", cdata("OpenIsle RSS"));
|
||||||
elem(sb, "link", base + "/");
|
elem(sb, "link", base + "/");
|
||||||
elem(sb, "description", cdata("Latest posts"));
|
elem(sb, "description", cdata("Latest posts"));
|
||||||
ZonedDateTime updated = posts.stream()
|
ZonedDateTime updated = posts
|
||||||
|
.stream()
|
||||||
.map(p -> p.getCreatedAt().atZone(ZoneId.systemDefault()))
|
.map(p -> p.getCreatedAt().atZone(ZoneId.systemDefault()))
|
||||||
.max(Comparator.naturalOrder())
|
.max(Comparator.naturalOrder())
|
||||||
.orElse(ZonedDateTime.now());
|
.orElse(ZonedDateTime.now());
|
||||||
@@ -114,8 +124,10 @@ public class RssController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 6) 构造优雅的附加区块(原文链接 + 精选评论),编入 <content:encoded>
|
// 6) 构造优雅的附加区块(原文链接 + 精选评论),编入 <content:encoded>
|
||||||
List<Comment> topComments = commentService
|
List<Comment> topComments = commentService.getCommentsForPost(
|
||||||
.getCommentsForPost(p.getId(), CommentSort.MOST_INTERACTIONS);
|
p.getId(),
|
||||||
|
CommentSort.MOST_INTERACTIONS
|
||||||
|
);
|
||||||
topComments = topComments.subList(0, Math.min(10, topComments.size()));
|
topComments = topComments.subList(0, Math.min(10, topComments.size()));
|
||||||
String footerHtml = buildFooterHtml(base, link, topComments);
|
String footerHtml = buildFooterHtml(base, link, topComments);
|
||||||
|
|
||||||
@@ -127,14 +139,19 @@ public class RssController {
|
|||||||
// 摘要
|
// 摘要
|
||||||
elem(sb, "description", cdata(plain));
|
elem(sb, "description", cdata(plain));
|
||||||
// 全文(HTML):正文 + 优雅的 Markdown 区块(已转 HTML)
|
// 全文(HTML):正文 + 优雅的 Markdown 区块(已转 HTML)
|
||||||
sb.append("<content:encoded><![CDATA[")
|
sb
|
||||||
|
.append("<content:encoded><![CDATA[")
|
||||||
.append(absHtml)
|
.append(absHtml)
|
||||||
.append(footerHtml)
|
.append(footerHtml)
|
||||||
.append("]]></content:encoded>");
|
.append("]]></content:encoded>");
|
||||||
// 首图 enclosure(图片类型)
|
// 首图 enclosure(图片类型)
|
||||||
if (enclosure != null) {
|
if (enclosure != null) {
|
||||||
sb.append("<enclosure url=\"").append(escapeXml(enclosure)).append("\" type=\"")
|
sb
|
||||||
.append(getMimeType(enclosure)).append("\" />");
|
.append("<enclosure url=\"")
|
||||||
|
.append(escapeXml(enclosure))
|
||||||
|
.append("\" type=\"")
|
||||||
|
.append(getMimeType(enclosure))
|
||||||
|
.append("\" />");
|
||||||
}
|
}
|
||||||
sb.append("</item>");
|
sb.append("</item>");
|
||||||
}
|
}
|
||||||
@@ -156,10 +173,26 @@ public class RssController {
|
|||||||
if (html == null) return "";
|
if (html == null) return "";
|
||||||
Safelist wl = Safelist.relaxed()
|
Safelist wl = Safelist.relaxed()
|
||||||
.addTags(
|
.addTags(
|
||||||
"pre","code","figure","figcaption","picture","source",
|
"pre",
|
||||||
"table","thead","tbody","tr","th","td",
|
"code",
|
||||||
"h1","h2","h3","h4","h5","h6",
|
"figure",
|
||||||
"hr","blockquote"
|
"figcaption",
|
||||||
|
"picture",
|
||||||
|
"source",
|
||||||
|
"table",
|
||||||
|
"thead",
|
||||||
|
"tbody",
|
||||||
|
"tr",
|
||||||
|
"th",
|
||||||
|
"td",
|
||||||
|
"h1",
|
||||||
|
"h2",
|
||||||
|
"h3",
|
||||||
|
"h4",
|
||||||
|
"h5",
|
||||||
|
"h6",
|
||||||
|
"hr",
|
||||||
|
"blockquote"
|
||||||
)
|
)
|
||||||
.addAttributes("a", "href", "title", "target", "rel")
|
.addAttributes("a", "href", "title", "target", "rel")
|
||||||
.addAttributes("img", "src", "alt", "title", "width", "height")
|
.addAttributes("img", "src", "alt", "title", "width", "height")
|
||||||
@@ -275,15 +308,24 @@ public class RssController {
|
|||||||
* 将“原文链接 + 精选评论(最多 10 条)”以优雅的 Markdown 形式渲染为 HTML,
|
* 将“原文链接 + 精选评论(最多 10 条)”以优雅的 Markdown 形式渲染为 HTML,
|
||||||
* 并做 sanitize + 绝对化,然后拼入 content:encoded 尾部。
|
* 并做 sanitize + 绝对化,然后拼入 content:encoded 尾部。
|
||||||
*/
|
*/
|
||||||
private static String buildFooterHtml(String baseUrl, String originalLink, List<Comment> topComments) {
|
private static String buildFooterHtml(
|
||||||
|
String baseUrl,
|
||||||
|
String originalLink,
|
||||||
|
List<Comment> topComments
|
||||||
|
) {
|
||||||
StringBuilder md = new StringBuilder(256);
|
StringBuilder md = new StringBuilder(256);
|
||||||
|
|
||||||
// 分割线
|
// 分割线
|
||||||
md.append("\n\n---\n\n");
|
md.append("\n\n---\n\n");
|
||||||
|
|
||||||
// 原文链接(强调 + 可点击)
|
// 原文链接(强调 + 可点击)
|
||||||
md.append("**原文链接:** ")
|
md
|
||||||
.append("[").append(originalLink).append("](").append(originalLink).append(")")
|
.append("**原文链接:** ")
|
||||||
|
.append("[")
|
||||||
|
.append(originalLink)
|
||||||
|
.append("](")
|
||||||
|
.append(originalLink)
|
||||||
|
.append(")")
|
||||||
.append("\n\n");
|
.append("\n\n");
|
||||||
|
|
||||||
// 精选评论(仅当有评论时展示)
|
// 精选评论(仅当有评论时展示)
|
||||||
@@ -340,8 +382,12 @@ public class RssController {
|
|||||||
|
|
||||||
private static String escapeXml(String s) {
|
private static String escapeXml(String s) {
|
||||||
if (s == null) return "";
|
if (s == null) return "";
|
||||||
return s.replace("&", "&").replace("<", "<").replace(">", ">")
|
return s
|
||||||
.replace("\"", """).replace("'", "'");
|
.replace("&", "&")
|
||||||
|
.replace("<", "<")
|
||||||
|
.replace(">", ">")
|
||||||
|
.replace("\"", """)
|
||||||
|
.replace("'", "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String trimTrailingSlash(String s) {
|
private static String trimTrailingSlash(String s) {
|
||||||
@@ -354,5 +400,7 @@ public class RssController {
|
|||||||
return s.endsWith("/") ? s : s + "/";
|
return s.endsWith("/") ? s : s + "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String nullSafe(String s) { return s == null ? "" : s; }
|
private static String nullSafe(String s) {
|
||||||
|
return s == null ? "" : s;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,74 +6,107 @@ 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 SearchService searchService;
|
||||||
private final UserMapper userMapper;
|
private final UserMapper userMapper;
|
||||||
private final PostMapper postMapper;
|
private final PostMapper postMapper;
|
||||||
|
|
||||||
@GetMapping("/users")
|
@GetMapping("/users")
|
||||||
@Operation(summary = "Search users", description = "Search users by keyword")
|
@Operation(summary = "Search users", description = "Search users by keyword")
|
||||||
@ApiResponse(responseCode = "200", description = "List of users",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = UserDto.class))))
|
responseCode = "200",
|
||||||
|
description = "List of users",
|
||||||
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = UserDto.class)))
|
||||||
|
)
|
||||||
public List<UserDto> searchUsers(@RequestParam String keyword) {
|
public List<UserDto> searchUsers(@RequestParam String keyword) {
|
||||||
return searchService.searchUsers(keyword).stream()
|
return searchService
|
||||||
|
.searchUsers(keyword)
|
||||||
|
.stream()
|
||||||
.map(userMapper::toDto)
|
.map(userMapper::toDto)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/posts")
|
@GetMapping("/posts")
|
||||||
@Operation(summary = "Search posts", description = "Search posts by 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",
|
||||||
|
description = "List of posts",
|
||||||
|
content = @Content(
|
||||||
|
array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))
|
||||||
|
)
|
||||||
|
)
|
||||||
public List<PostSummaryDto> searchPosts(@RequestParam String keyword) {
|
public List<PostSummaryDto> searchPosts(@RequestParam String keyword) {
|
||||||
return searchService.searchPosts(keyword).stream()
|
return searchService
|
||||||
|
.searchPosts(keyword)
|
||||||
|
.stream()
|
||||||
.map(postMapper::toSummaryDto)
|
.map(postMapper::toSummaryDto)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/posts/content")
|
@GetMapping("/posts/content")
|
||||||
@Operation(summary = "Search posts by content", description = "Search posts by content 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",
|
||||||
|
description = "List of posts",
|
||||||
|
content = @Content(
|
||||||
|
array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))
|
||||||
|
)
|
||||||
|
)
|
||||||
public List<PostSummaryDto> searchPostsByContent(@RequestParam String keyword) {
|
public List<PostSummaryDto> searchPostsByContent(@RequestParam String keyword) {
|
||||||
return searchService.searchPostsByContent(keyword).stream()
|
return searchService
|
||||||
|
.searchPostsByContent(keyword)
|
||||||
|
.stream()
|
||||||
.map(postMapper::toSummaryDto)
|
.map(postMapper::toSummaryDto)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/posts/title")
|
@GetMapping("/posts/title")
|
||||||
@Operation(summary = "Search posts by title", description = "Search posts by title keyword")
|
@Operation(summary = "Search posts by title", description = "Search posts by title keyword")
|
||||||
@ApiResponse(responseCode = "200", description = "List of posts",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
|
responseCode = "200",
|
||||||
|
description = "List of posts",
|
||||||
|
content = @Content(
|
||||||
|
array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))
|
||||||
|
)
|
||||||
|
)
|
||||||
public List<PostSummaryDto> searchPostsByTitle(@RequestParam String keyword) {
|
public List<PostSummaryDto> searchPostsByTitle(@RequestParam String keyword) {
|
||||||
return searchService.searchPostsByTitle(keyword).stream()
|
return searchService
|
||||||
|
.searchPostsByTitle(keyword)
|
||||||
|
.stream()
|
||||||
.map(postMapper::toSummaryDto)
|
.map(postMapper::toSummaryDto)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/global")
|
@GetMapping("/global")
|
||||||
@Operation(summary = "Global search", description = "Search users and posts globally")
|
@Operation(summary = "Global search", description = "Search users and posts globally")
|
||||||
@ApiResponse(responseCode = "200", description = "Search results",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = SearchResultDto.class))))
|
responseCode = "200",
|
||||||
|
description = "Search results",
|
||||||
|
content = @Content(
|
||||||
|
array = @ArraySchema(schema = @Schema(implementation = SearchResultDto.class))
|
||||||
|
)
|
||||||
|
)
|
||||||
public List<SearchResultDto> global(@RequestParam String keyword) {
|
public List<SearchResultDto> global(@RequestParam String keyword) {
|
||||||
return searchService.globalSearch(keyword).stream()
|
return searchService
|
||||||
|
.globalSearch(keyword)
|
||||||
|
.stream()
|
||||||
.map(r -> {
|
.map(r -> {
|
||||||
SearchResultDto dto = new SearchResultDto();
|
SearchResultDto dto = new SearchResultDto();
|
||||||
dto.setType(r.type());
|
dto.setType(r.type());
|
||||||
|
|||||||
@@ -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,6 +23,7 @@ import java.util.List;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@RequestMapping("/api")
|
@RequestMapping("/api")
|
||||||
public class SitemapController {
|
public class SitemapController {
|
||||||
|
|
||||||
private final PostRepository postRepository;
|
private final PostRepository postRepository;
|
||||||
|
|
||||||
@Value("${app.website-url}")
|
@Value("${app.website-url}")
|
||||||
@@ -31,8 +31,11 @@ public class SitemapController {
|
|||||||
|
|
||||||
@GetMapping(value = "/sitemap.xml", produces = MediaType.APPLICATION_XML_VALUE)
|
@GetMapping(value = "/sitemap.xml", produces = MediaType.APPLICATION_XML_VALUE)
|
||||||
@Operation(summary = "Sitemap", description = "Generate sitemap xml")
|
@Operation(summary = "Sitemap", description = "Generate sitemap xml")
|
||||||
@ApiResponse(responseCode = "200", description = "Sitemap xml",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = String.class)))
|
responseCode = "200",
|
||||||
|
description = "Sitemap xml",
|
||||||
|
content = @Content(schema = @Schema(implementation = String.class))
|
||||||
|
)
|
||||||
public ResponseEntity<String> sitemap() {
|
public ResponseEntity<String> sitemap() {
|
||||||
List<Post> posts = postRepository.findByStatus(PostStatus.PUBLISHED);
|
List<Post> posts = postRepository.findByStatus(PostStatus.PUBLISHED);
|
||||||
|
|
||||||
@@ -40,23 +43,15 @@ public class SitemapController {
|
|||||||
body.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
|
body.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
|
||||||
body.append("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n");
|
body.append("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n");
|
||||||
|
|
||||||
List<String> staticRoutes = List.of(
|
List<String> staticRoutes = List.of("/", "/about", "/activities", "/login", "/signup");
|
||||||
"/",
|
|
||||||
"/about",
|
|
||||||
"/activities",
|
|
||||||
"/login",
|
|
||||||
"/signup"
|
|
||||||
);
|
|
||||||
|
|
||||||
for (String path : staticRoutes) {
|
for (String path : staticRoutes) {
|
||||||
body.append(" <url><loc>")
|
body.append(" <url><loc>").append(websiteUrl).append(path).append("</loc></url>\n");
|
||||||
.append(websiteUrl)
|
|
||||||
.append(path)
|
|
||||||
.append("</loc></url>\n");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Post p : posts) {
|
for (Post p : posts) {
|
||||||
body.append(" <url>\n")
|
body
|
||||||
|
.append(" <url>\n")
|
||||||
.append(" <loc>")
|
.append(" <loc>")
|
||||||
.append(websiteUrl)
|
.append(websiteUrl)
|
||||||
.append("/posts/")
|
.append("/posts/")
|
||||||
@@ -69,8 +64,6 @@ public class SitemapController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body.append("</urlset>");
|
body.append("</urlset>");
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok().contentType(MediaType.APPLICATION_XML).body(body.toString());
|
||||||
.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 UserVisitService userVisitService;
|
||||||
private final StatService statService;
|
private final StatService statService;
|
||||||
|
|
||||||
@GetMapping("/dau")
|
@GetMapping("/dau")
|
||||||
@Operation(summary = "Daily active users", description = "Get daily active user count")
|
@Operation(summary = "Daily active users", description = "Get daily active user count")
|
||||||
@ApiResponse(responseCode = "200", description = "DAU count",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = java.util.Map.class)))
|
responseCode = "200",
|
||||||
public Map<String, Long> dau(@RequestParam(value = "date", required = false)
|
description = "DAU count",
|
||||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) {
|
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);
|
long count = userVisitService.countDau(date);
|
||||||
return Map.of("dau", count);
|
return Map.of("dau", count);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/dau-range")
|
@GetMapping("/dau-range")
|
||||||
@Operation(summary = "DAU range", description = "Get daily active users over range of days")
|
@Operation(summary = "DAU range", description = "Get daily active users over range of days")
|
||||||
@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 data",
|
||||||
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = java.util.Map.class)))
|
||||||
|
)
|
||||||
|
public List<Map<String, Object>> dauRange(
|
||||||
|
@RequestParam(value = "days", defaultValue = "30") int days
|
||||||
|
) {
|
||||||
if (days < 1) days = 1;
|
if (days < 1) days = 1;
|
||||||
LocalDate end = LocalDate.now();
|
LocalDate end = LocalDate.now();
|
||||||
LocalDate start = end.minusDays(days - 1L);
|
LocalDate start = end.minusDays(days - 1L);
|
||||||
var data = userVisitService.countDauRange(start, end);
|
var data = userVisitService.countDauRange(start, end);
|
||||||
return data.entrySet().stream()
|
return data
|
||||||
.map(e -> Map.<String,Object>of(
|
.entrySet()
|
||||||
"date", e.getKey().toString(),
|
.stream()
|
||||||
"value", e.getValue()
|
.map(e -> Map.<String, Object>of("date", e.getKey().toString(), "value", e.getValue()))
|
||||||
))
|
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/new-users-range")
|
@GetMapping("/new-users-range")
|
||||||
@Operation(summary = "New users range", description = "Get new users over range of days")
|
@Operation(summary = "New users range", description = "Get new 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 = "New user data",
|
||||||
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = java.util.Map.class)))
|
||||||
|
)
|
||||||
|
public List<Map<String, Object>> newUsersRange(
|
||||||
|
@RequestParam(value = "days", defaultValue = "30") int days
|
||||||
|
) {
|
||||||
if (days < 1) days = 1;
|
if (days < 1) days = 1;
|
||||||
LocalDate end = LocalDate.now();
|
LocalDate end = LocalDate.now();
|
||||||
LocalDate start = end.minusDays(days - 1L);
|
LocalDate start = end.minusDays(days - 1L);
|
||||||
var data = statService.countNewUsersRange(start, end);
|
var data = statService.countNewUsersRange(start, end);
|
||||||
return data.entrySet().stream()
|
return data
|
||||||
.map(e -> Map.<String,Object>of(
|
.entrySet()
|
||||||
"date", e.getKey().toString(),
|
.stream()
|
||||||
"value", e.getValue()
|
.map(e -> Map.<String, Object>of("date", e.getKey().toString(), "value", e.getValue()))
|
||||||
))
|
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/posts-range")
|
@GetMapping("/posts-range")
|
||||||
@Operation(summary = "Posts range", description = "Get posts count over range of days")
|
@Operation(summary = "Posts range", description = "Get posts count 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 = "Post data",
|
||||||
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = java.util.Map.class)))
|
||||||
|
)
|
||||||
|
public List<Map<String, Object>> postsRange(
|
||||||
|
@RequestParam(value = "days", defaultValue = "30") int days
|
||||||
|
) {
|
||||||
if (days < 1) days = 1;
|
if (days < 1) days = 1;
|
||||||
LocalDate end = LocalDate.now();
|
LocalDate end = LocalDate.now();
|
||||||
LocalDate start = end.minusDays(days - 1L);
|
LocalDate start = end.minusDays(days - 1L);
|
||||||
var data = statService.countPostsRange(start, end);
|
var data = statService.countPostsRange(start, end);
|
||||||
return data.entrySet().stream()
|
return data
|
||||||
.map(e -> Map.<String,Object>of(
|
.entrySet()
|
||||||
"date", e.getKey().toString(),
|
.stream()
|
||||||
"value", e.getValue()
|
.map(e -> Map.<String, Object>of("date", e.getKey().toString(), "value", e.getValue()))
|
||||||
))
|
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/comments-range")
|
@GetMapping("/comments-range")
|
||||||
@Operation(summary = "Comments range", description = "Get comments count over range of days")
|
@Operation(summary = "Comments range", description = "Get comments 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 = "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;
|
if (days < 1) days = 1;
|
||||||
LocalDate end = LocalDate.now();
|
LocalDate end = LocalDate.now();
|
||||||
LocalDate start = end.minusDays(days - 1L);
|
LocalDate start = end.minusDays(days - 1L);
|
||||||
var data = statService.countCommentsRange(start, end);
|
var data = statService.countCommentsRange(start, end);
|
||||||
return data.entrySet().stream()
|
return data
|
||||||
.map(e -> Map.<String,Object>of(
|
.entrySet()
|
||||||
"date", e.getKey().toString(),
|
.stream()
|
||||||
"value", e.getValue()
|
.map(e -> Map.<String, Object>of("date", e.getKey().toString(), "value", e.getValue()))
|
||||||
))
|
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
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;
|
private final SubscriptionService subscriptionService;
|
||||||
|
|
||||||
@PostMapping("/posts/{postId}")
|
@PostMapping("/posts/{postId}")
|
||||||
|
|||||||
@@ -11,23 +11,23 @@ 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 TagService tagService;
|
||||||
private final PostService postService;
|
private final PostService postService;
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
@@ -36,10 +36,16 @@ public class TagController {
|
|||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@Operation(summary = "Create tag", description = "Create a new tag")
|
@Operation(summary = "Create tag", description = "Create a new tag")
|
||||||
@ApiResponse(responseCode = "200", description = "Created tag",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = TagDto.class)))
|
responseCode = "200",
|
||||||
|
description = "Created tag",
|
||||||
|
content = @Content(schema = @Schema(implementation = TagDto.class))
|
||||||
|
)
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
public TagDto create(@RequestBody TagRequest req, org.springframework.security.core.Authentication auth) {
|
public TagDto create(
|
||||||
|
@RequestBody TagRequest req,
|
||||||
|
org.springframework.security.core.Authentication auth
|
||||||
|
) {
|
||||||
boolean approved = true;
|
boolean approved = true;
|
||||||
if (postService.getPublishMode() == PublishMode.REVIEW && auth != null) {
|
if (postService.getPublishMode() == PublishMode.REVIEW && auth != null) {
|
||||||
com.openisle.model.User user = userRepository.findByUsername(auth.getName()).orElseThrow();
|
com.openisle.model.User user = userRepository.findByUsername(auth.getName()).orElseThrow();
|
||||||
@@ -53,17 +59,27 @@ public class TagController {
|
|||||||
req.getIcon(),
|
req.getIcon(),
|
||||||
req.getSmallIcon(),
|
req.getSmallIcon(),
|
||||||
approved,
|
approved,
|
||||||
auth != null ? auth.getName() : null);
|
auth != null ? auth.getName() : null
|
||||||
|
);
|
||||||
long count = postService.countPostsByTag(tag.getId());
|
long count = postService.countPostsByTag(tag.getId());
|
||||||
return tagMapper.toDto(tag, count);
|
return tagMapper.toDto(tag, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/{id}")
|
@PutMapping("/{id}")
|
||||||
@Operation(summary = "Update tag", description = "Update an existing tag")
|
@Operation(summary = "Update tag", description = "Update an existing tag")
|
||||||
@ApiResponse(responseCode = "200", description = "Updated tag",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = TagDto.class)))
|
responseCode = "200",
|
||||||
|
description = "Updated tag",
|
||||||
|
content = @Content(schema = @Schema(implementation = TagDto.class))
|
||||||
|
)
|
||||||
public TagDto update(@PathVariable Long id, @RequestBody TagRequest req) {
|
public TagDto update(@PathVariable Long id, @RequestBody TagRequest req) {
|
||||||
Tag tag = tagService.updateTag(id, req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon());
|
Tag tag = tagService.updateTag(
|
||||||
|
id,
|
||||||
|
req.getName(),
|
||||||
|
req.getDescription(),
|
||||||
|
req.getIcon(),
|
||||||
|
req.getSmallIcon()
|
||||||
|
);
|
||||||
long count = postService.countPostsByTag(tag.getId());
|
long count = postService.countPostsByTag(tag.getId());
|
||||||
return tagMapper.toDto(tag, count);
|
return tagMapper.toDto(tag, count);
|
||||||
}
|
}
|
||||||
@@ -77,14 +93,20 @@ public class TagController {
|
|||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@Operation(summary = "List tags", description = "List tags with optional keyword")
|
@Operation(summary = "List tags", description = "List tags with optional keyword")
|
||||||
@ApiResponse(responseCode = "200", description = "List of tags",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TagDto.class))))
|
responseCode = "200",
|
||||||
public List<TagDto> list(@RequestParam(value = "keyword", required = false) String keyword,
|
description = "List of tags",
|
||||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TagDto.class)))
|
||||||
|
)
|
||||||
|
public List<TagDto> list(
|
||||||
|
@RequestParam(value = "keyword", required = false) String keyword,
|
||||||
|
@RequestParam(value = "limit", required = false) Integer limit
|
||||||
|
) {
|
||||||
List<Tag> tags = tagService.searchTags(keyword);
|
List<Tag> tags = tagService.searchTags(keyword);
|
||||||
List<Long> tagIds = tags.stream().map(Tag::getId).toList();
|
List<Long> tagIds = tags.stream().map(Tag::getId).toList();
|
||||||
Map<Long, Long> postCntByTagIds = postService.countPostsByTagIds(tagIds);
|
Map<Long, Long> postCntByTagIds = postService.countPostsByTagIds(tagIds);
|
||||||
List<TagDto> dtos = tags.stream()
|
List<TagDto> dtos = tags
|
||||||
|
.stream()
|
||||||
.map(t -> tagMapper.toDto(t, postCntByTagIds.getOrDefault(t.getId(), 0L)))
|
.map(t -> tagMapper.toDto(t, postCntByTagIds.getOrDefault(t.getId(), 0L)))
|
||||||
.sorted((a, b) -> Long.compare(b.getCount(), a.getCount()))
|
.sorted((a, b) -> Long.compare(b.getCount(), a.getCount()))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
@@ -96,8 +118,11 @@ public class TagController {
|
|||||||
|
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
@Operation(summary = "Get tag", description = "Get tag by id")
|
@Operation(summary = "Get tag", description = "Get tag by id")
|
||||||
@ApiResponse(responseCode = "200", description = "Tag detail",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = TagDto.class)))
|
responseCode = "200",
|
||||||
|
description = "Tag detail",
|
||||||
|
content = @Content(schema = @Schema(implementation = TagDto.class))
|
||||||
|
)
|
||||||
public TagDto get(@PathVariable Long id) {
|
public TagDto get(@PathVariable Long id) {
|
||||||
Tag tag = tagService.getTag(id);
|
Tag tag = tagService.getTag(id);
|
||||||
long count = postService.countPostsByTag(tag.getId());
|
long count = postService.countPostsByTag(tag.getId());
|
||||||
@@ -106,12 +131,20 @@ public class TagController {
|
|||||||
|
|
||||||
@GetMapping("/{id}/posts")
|
@GetMapping("/{id}/posts")
|
||||||
@Operation(summary = "List posts by tag", description = "Get posts with specific tag")
|
@Operation(summary = "List posts by tag", description = "Get posts with specific tag")
|
||||||
@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 = "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 = "page", required = false) Integer page,
|
||||||
@RequestParam(value = "pageSize", required = false) Integer pageSize) {
|
@RequestParam(value = "pageSize", required = false) Integer pageSize
|
||||||
return postService.listPostsByTags(java.util.List.of(id), page, pageSize)
|
) {
|
||||||
|
return postService
|
||||||
|
.listPostsByTags(java.util.List.of(id), page, pageSize)
|
||||||
.stream()
|
.stream()
|
||||||
.map(postMapper::toSummaryDto)
|
.map(postMapper::toSummaryDto)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
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;
|
private final ImageUploader imageUploader;
|
||||||
|
|
||||||
@Value("${app.upload.check-type:true}")
|
@Value("${app.upload.check-type:true}")
|
||||||
@@ -32,10 +32,16 @@ public class UploadController {
|
|||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@Operation(summary = "Upload file", description = "Upload image file")
|
@Operation(summary = "Upload file", description = "Upload image file")
|
||||||
@ApiResponse(responseCode = "200", description = "Upload result",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = java.util.Map.class)))
|
responseCode = "200",
|
||||||
|
description = "Upload result",
|
||||||
|
content = @Content(schema = @Schema(implementation = java.util.Map.class))
|
||||||
|
)
|
||||||
public ResponseEntity<?> upload(@RequestParam("file") MultipartFile file) {
|
public ResponseEntity<?> upload(@RequestParam("file") MultipartFile file) {
|
||||||
if (checkImageType && (file.getContentType() == null || !file.getContentType().startsWith("image/"))) {
|
if (
|
||||||
|
checkImageType &&
|
||||||
|
(file.getContentType() == null || !file.getContentType().startsWith("image/"))
|
||||||
|
) {
|
||||||
return ResponseEntity.badRequest().body(Map.of("code", 1, "msg", "File is not an image"));
|
return ResponseEntity.badRequest().body(Map.of("code", 1, "msg", "File is not an image"));
|
||||||
}
|
}
|
||||||
if (file.getSize() > maxUploadSize) {
|
if (file.getSize() > maxUploadSize) {
|
||||||
@@ -47,17 +53,16 @@ public class UploadController {
|
|||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
return ResponseEntity.internalServerError().body(Map.of("code", 3, "msg", "Upload failed"));
|
return ResponseEntity.internalServerError().body(Map.of("code", 3, "msg", "Upload failed"));
|
||||||
}
|
}
|
||||||
return ResponseEntity.ok(Map.of(
|
return ResponseEntity.ok(Map.of("code", 0, "msg", "ok", "data", Map.of("url", url)));
|
||||||
"code", 0,
|
|
||||||
"msg", "ok",
|
|
||||||
"data", Map.of("url", url)
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/url")
|
@PostMapping("/url")
|
||||||
@Operation(summary = "Upload from URL", description = "Upload image from remote URL")
|
@Operation(summary = "Upload from URL", description = "Upload image from remote URL")
|
||||||
@ApiResponse(responseCode = "200", description = "Upload result",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = java.util.Map.class)))
|
responseCode = "200",
|
||||||
|
description = "Upload result",
|
||||||
|
content = @Content(schema = @Schema(implementation = java.util.Map.class))
|
||||||
|
)
|
||||||
public ResponseEntity<?> uploadUrl(@RequestBody Map<String, String> body) {
|
public ResponseEntity<?> uploadUrl(@RequestBody Map<String, String> body) {
|
||||||
String link = body.get("url");
|
String link = body.get("url");
|
||||||
if (link == null || link.isBlank()) {
|
if (link == null || link.isBlank()) {
|
||||||
@@ -75,11 +80,7 @@ public class UploadController {
|
|||||||
return ResponseEntity.badRequest().body(Map.of("code", 1, "msg", "File is not an image"));
|
return ResponseEntity.badRequest().body(Map.of("code", 1, "msg", "File is not an image"));
|
||||||
}
|
}
|
||||||
String url = imageUploader.upload(data, filename).join();
|
String url = imageUploader.upload(data, filename).join();
|
||||||
return ResponseEntity.ok(Map.of(
|
return ResponseEntity.ok(Map.of("code", 0, "msg", "ok", "data", Map.of("url", url)));
|
||||||
"code", 0,
|
|
||||||
"msg", "ok",
|
|
||||||
"data", Map.of("url", url)
|
|
||||||
));
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return ResponseEntity.internalServerError().body(Map.of("code", 3, "msg", "Upload failed"));
|
return ResponseEntity.internalServerError().body(Map.of("code", 3, "msg", "Upload failed"));
|
||||||
}
|
}
|
||||||
@@ -87,8 +88,11 @@ public class UploadController {
|
|||||||
|
|
||||||
@GetMapping("/presign")
|
@GetMapping("/presign")
|
||||||
@Operation(summary = "Presign upload", description = "Get presigned upload URL")
|
@Operation(summary = "Presign upload", description = "Get presigned upload URL")
|
||||||
@ApiResponse(responseCode = "200", description = "Presigned URL",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = java.util.Map.class)))
|
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) {
|
public java.util.Map<String, String> presign(@RequestParam("filename") String filename) {
|
||||||
return imageUploader.presignUpload(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,13 +21,11 @@ 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 UserService userService;
|
||||||
private final ImageUploader imageUploader;
|
private final ImageUploader imageUploader;
|
||||||
private final PostService postService;
|
private final PostService postService;
|
||||||
@@ -56,8 +56,11 @@ public class UserController {
|
|||||||
@GetMapping("/me")
|
@GetMapping("/me")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "Current user", description = "Get current authenticated user information")
|
@Operation(summary = "Current user", description = "Get current authenticated user information")
|
||||||
@ApiResponse(responseCode = "200", description = "User detail",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = UserDto.class)))
|
responseCode = "200",
|
||||||
|
description = "User detail",
|
||||||
|
content = @Content(schema = @Schema(implementation = UserDto.class))
|
||||||
|
)
|
||||||
public ResponseEntity<UserDto> me(Authentication auth) {
|
public ResponseEntity<UserDto> me(Authentication auth) {
|
||||||
User user = userService.findByUsername(auth.getName()).orElseThrow();
|
User user = userService.findByUsername(auth.getName()).orElseThrow();
|
||||||
return ResponseEntity.ok(userMapper.toDto(user, auth));
|
return ResponseEntity.ok(userMapper.toDto(user, auth));
|
||||||
@@ -66,11 +69,19 @@ public class UserController {
|
|||||||
@PostMapping("/me/avatar")
|
@PostMapping("/me/avatar")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "Upload avatar", description = "Upload avatar for current user")
|
@Operation(summary = "Upload avatar", description = "Upload avatar for current user")
|
||||||
@ApiResponse(responseCode = "200", description = "Upload result",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = Map.class)))
|
responseCode = "200",
|
||||||
public ResponseEntity<?> uploadAvatar(@RequestParam("file") MultipartFile file,
|
description = "Upload result",
|
||||||
Authentication auth) {
|
content = @Content(schema = @Schema(implementation = Map.class))
|
||||||
if (checkImageType && (file.getContentType() == null || !file.getContentType().startsWith("image/"))) {
|
)
|
||||||
|
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"));
|
return ResponseEntity.badRequest().body(Map.of("error", "File is not an image"));
|
||||||
}
|
}
|
||||||
if (file.getSize() > maxUploadSize) {
|
if (file.getSize() > maxUploadSize) {
|
||||||
@@ -89,23 +100,32 @@ public class UserController {
|
|||||||
@PutMapping("/me")
|
@PutMapping("/me")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "Update profile", description = "Update current user's profile")
|
@Operation(summary = "Update profile", description = "Update current user's profile")
|
||||||
@ApiResponse(responseCode = "200", description = "Updated profile",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = Map.class)))
|
responseCode = "200",
|
||||||
public ResponseEntity<?> updateProfile(@RequestBody UpdateProfileDto dto,
|
description = "Updated profile",
|
||||||
Authentication auth) {
|
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());
|
User user = userService.updateProfile(auth.getName(), dto.getUsername(), dto.getIntroduction());
|
||||||
return ResponseEntity.ok(Map.of(
|
return ResponseEntity.ok(
|
||||||
"token", jwtService.generateToken(user.getUsername()),
|
Map.of(
|
||||||
"user", userMapper.toDto(user, auth)
|
"token",
|
||||||
));
|
jwtService.generateToken(user.getUsername()),
|
||||||
|
"user",
|
||||||
|
userMapper.toDto(user, auth)
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 这个方法似乎没有使用?
|
// 这个方法似乎没有使用?
|
||||||
@PostMapping("/me/signin")
|
@PostMapping("/me/signin")
|
||||||
@SecurityRequirement(name = "JWT")
|
@SecurityRequirement(name = "JWT")
|
||||||
@Operation(summary = "Daily sign in", description = "Sign in to receive rewards")
|
@Operation(summary = "Daily sign in", description = "Sign in to receive rewards")
|
||||||
@ApiResponse(responseCode = "200", description = "Sign in reward",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = Map.class)))
|
responseCode = "200",
|
||||||
|
description = "Sign in reward",
|
||||||
|
content = @Content(schema = @Schema(implementation = Map.class))
|
||||||
|
)
|
||||||
public Map<String, Integer> signIn(Authentication auth) {
|
public Map<String, Integer> signIn(Authentication auth) {
|
||||||
int reward = levelService.awardForSignin(auth.getName());
|
int reward = levelService.awardForSignin(auth.getName());
|
||||||
return Map.of("reward", reward);
|
return Map.of("reward", reward);
|
||||||
@@ -113,36 +133,57 @@ public class UserController {
|
|||||||
|
|
||||||
@GetMapping("/{identifier}")
|
@GetMapping("/{identifier}")
|
||||||
@Operation(summary = "Get user", description = "Get user by identifier")
|
@Operation(summary = "Get user", description = "Get user by identifier")
|
||||||
@ApiResponse(responseCode = "200", description = "User detail",
|
@ApiResponse(
|
||||||
content = @Content(schema = @Schema(implementation = UserDto.class)))
|
responseCode = "200",
|
||||||
public ResponseEntity<UserDto> getUser(@PathVariable("identifier") String identifier,
|
description = "User detail",
|
||||||
Authentication auth) {
|
content = @Content(schema = @Schema(implementation = UserDto.class))
|
||||||
User user = userService.findByIdentifier(identifier).orElseThrow(() -> new NotFoundException("User not found"));
|
)
|
||||||
|
public ResponseEntity<UserDto> getUser(
|
||||||
|
@PathVariable("identifier") String identifier,
|
||||||
|
Authentication auth
|
||||||
|
) {
|
||||||
|
User user = userService
|
||||||
|
.findByIdentifier(identifier)
|
||||||
|
.orElseThrow(() -> new NotFoundException("User not found"));
|
||||||
return ResponseEntity.ok(userMapper.toDto(user, auth));
|
return ResponseEntity.ok(userMapper.toDto(user, auth));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{identifier}/posts")
|
@GetMapping("/{identifier}/posts")
|
||||||
@Operation(summary = "User posts", description = "Get recent posts by user")
|
@Operation(summary = "User posts", description = "Get recent posts by user")
|
||||||
@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 posts",
|
||||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostMetaDto.class)))
|
||||||
|
)
|
||||||
|
public java.util.List<PostMetaDto> userPosts(
|
||||||
|
@PathVariable("identifier") String identifier,
|
||||||
|
@RequestParam(value = "limit", required = false) Integer limit
|
||||||
|
) {
|
||||||
int l = limit != null ? limit : defaultPostsLimit;
|
int l = limit != null ? limit : defaultPostsLimit;
|
||||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||||
return postService.getRecentPostsByUser(user.getUsername(), l).stream()
|
return postService
|
||||||
|
.getRecentPostsByUser(user.getUsername(), l)
|
||||||
|
.stream()
|
||||||
.map(userMapper::toMetaDto)
|
.map(userMapper::toMetaDto)
|
||||||
.collect(java.util.stream.Collectors.toList());
|
.collect(java.util.stream.Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{identifier}/subscribed-posts")
|
@GetMapping("/{identifier}/subscribed-posts")
|
||||||
@Operation(summary = "Subscribed posts", description = "Get posts the user subscribed to")
|
@Operation(summary = "Subscribed posts", description = "Get posts the user subscribed to")
|
||||||
@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 = "Subscribed posts",
|
||||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostMetaDto.class)))
|
||||||
|
)
|
||||||
|
public java.util.List<PostMetaDto> subscribedPosts(
|
||||||
|
@PathVariable("identifier") String identifier,
|
||||||
|
@RequestParam(value = "limit", required = false) Integer limit
|
||||||
|
) {
|
||||||
int l = limit != null ? limit : defaultPostsLimit;
|
int l = limit != null ? limit : defaultPostsLimit;
|
||||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||||
return subscriptionService.getSubscribedPosts(user.getUsername()).stream()
|
return subscriptionService
|
||||||
|
.getSubscribedPosts(user.getUsername())
|
||||||
|
.stream()
|
||||||
.limit(l)
|
.limit(l)
|
||||||
.map(userMapper::toMetaDto)
|
.map(userMapper::toMetaDto)
|
||||||
.collect(java.util.stream.Collectors.toList());
|
.collect(java.util.stream.Collectors.toList());
|
||||||
@@ -150,54 +191,86 @@ public class UserController {
|
|||||||
|
|
||||||
@GetMapping("/{identifier}/replies")
|
@GetMapping("/{identifier}/replies")
|
||||||
@Operation(summary = "User replies", description = "Get recent replies by user")
|
@Operation(summary = "User replies", description = "Get recent replies by user")
|
||||||
@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 = "User replies",
|
||||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
content = @Content(
|
||||||
|
array = @ArraySchema(schema = @Schema(implementation = CommentInfoDto.class))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
public java.util.List<CommentInfoDto> userReplies(
|
||||||
|
@PathVariable("identifier") String identifier,
|
||||||
|
@RequestParam(value = "limit", required = false) Integer limit
|
||||||
|
) {
|
||||||
int l = limit != null ? limit : defaultRepliesLimit;
|
int l = limit != null ? limit : defaultRepliesLimit;
|
||||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||||
return commentService.getRecentCommentsByUser(user.getUsername(), l).stream()
|
return commentService
|
||||||
|
.getRecentCommentsByUser(user.getUsername(), l)
|
||||||
|
.stream()
|
||||||
.map(userMapper::toCommentInfoDto)
|
.map(userMapper::toCommentInfoDto)
|
||||||
.collect(java.util.stream.Collectors.toList());
|
.collect(java.util.stream.Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{identifier}/hot-posts")
|
@GetMapping("/{identifier}/hot-posts")
|
||||||
@Operation(summary = "User hot posts", description = "Get most reacted posts by user")
|
@Operation(summary = "User hot posts", description = "Get most reacted posts 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 = "Hot posts",
|
||||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostMetaDto.class)))
|
||||||
|
)
|
||||||
|
public java.util.List<PostMetaDto> hotPosts(
|
||||||
|
@PathVariable("identifier") String identifier,
|
||||||
|
@RequestParam(value = "limit", required = false) Integer limit
|
||||||
|
) {
|
||||||
int l = limit != null ? limit : 10;
|
int l = limit != null ? limit : 10;
|
||||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||||
java.util.List<Long> ids = reactionService.topPostIds(user.getUsername(), l);
|
java.util.List<Long> ids = reactionService.topPostIds(user.getUsername(), l);
|
||||||
return postService.getPostsByIds(ids).stream()
|
return postService
|
||||||
|
.getPostsByIds(ids)
|
||||||
|
.stream()
|
||||||
.map(userMapper::toMetaDto)
|
.map(userMapper::toMetaDto)
|
||||||
.collect(java.util.stream.Collectors.toList());
|
.collect(java.util.stream.Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{identifier}/hot-replies")
|
@GetMapping("/{identifier}/hot-replies")
|
||||||
@Operation(summary = "User hot replies", description = "Get most reacted replies by user")
|
@Operation(summary = "User hot replies", description = "Get most reacted replies 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 replies",
|
||||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
content = @Content(
|
||||||
|
array = @ArraySchema(schema = @Schema(implementation = CommentInfoDto.class))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
public java.util.List<CommentInfoDto> hotReplies(
|
||||||
|
@PathVariable("identifier") String identifier,
|
||||||
|
@RequestParam(value = "limit", required = false) Integer limit
|
||||||
|
) {
|
||||||
int l = limit != null ? limit : 10;
|
int l = limit != null ? limit : 10;
|
||||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||||
java.util.List<Long> ids = reactionService.topCommentIds(user.getUsername(), l);
|
java.util.List<Long> ids = reactionService.topCommentIds(user.getUsername(), l);
|
||||||
return commentService.getCommentsByIds(ids).stream()
|
return commentService
|
||||||
|
.getCommentsByIds(ids)
|
||||||
|
.stream()
|
||||||
.map(userMapper::toCommentInfoDto)
|
.map(userMapper::toCommentInfoDto)
|
||||||
.collect(java.util.stream.Collectors.toList());
|
.collect(java.util.stream.Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{identifier}/hot-tags")
|
@GetMapping("/{identifier}/hot-tags")
|
||||||
@Operation(summary = "User hot tags", description = "Get tags frequently used by user")
|
@Operation(summary = "User hot tags", description = "Get tags frequently used 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 tags",
|
||||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TagDto.class)))
|
||||||
|
)
|
||||||
|
public java.util.List<TagDto> hotTags(
|
||||||
|
@PathVariable("identifier") String identifier,
|
||||||
|
@RequestParam(value = "limit", required = false) Integer limit
|
||||||
|
) {
|
||||||
int l = limit != null ? limit : 10;
|
int l = limit != null ? limit : 10;
|
||||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||||
return tagService.getTagsByUser(user.getUsername()).stream()
|
return tagService
|
||||||
|
.getTagsByUser(user.getUsername())
|
||||||
|
.stream()
|
||||||
.map(t -> tagMapper.toDto(t, postService.countPostsByTag(t.getId())))
|
.map(t -> tagMapper.toDto(t, postService.countPostsByTag(t.getId())))
|
||||||
.sorted((a, b) -> Long.compare(b.getCount(), a.getCount()))
|
.sorted((a, b) -> Long.compare(b.getCount(), a.getCount()))
|
||||||
.limit(l)
|
.limit(l)
|
||||||
@@ -206,64 +279,95 @@ public class UserController {
|
|||||||
|
|
||||||
@GetMapping("/{identifier}/tags")
|
@GetMapping("/{identifier}/tags")
|
||||||
@Operation(summary = "User tags", description = "Get recent tags used by user")
|
@Operation(summary = "User tags", description = "Get recent tags 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 = "User tags",
|
||||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TagDto.class)))
|
||||||
|
)
|
||||||
|
public java.util.List<TagDto> userTags(
|
||||||
|
@PathVariable("identifier") String identifier,
|
||||||
|
@RequestParam(value = "limit", required = false) Integer limit
|
||||||
|
) {
|
||||||
int l = limit != null ? limit : defaultTagsLimit;
|
int l = limit != null ? limit : defaultTagsLimit;
|
||||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||||
return tagService.getRecentTagsByUser(user.getUsername(), l).stream()
|
return tagService
|
||||||
|
.getRecentTagsByUser(user.getUsername(), l)
|
||||||
|
.stream()
|
||||||
.map(t -> tagMapper.toDto(t, postService.countPostsByTag(t.getId())))
|
.map(t -> tagMapper.toDto(t, postService.countPostsByTag(t.getId())))
|
||||||
.collect(java.util.stream.Collectors.toList());
|
.collect(java.util.stream.Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{identifier}/following")
|
@GetMapping("/{identifier}/following")
|
||||||
@Operation(summary = "Following users", description = "Get users that this user is following")
|
@Operation(summary = "Following users", description = "Get users that this user is following")
|
||||||
@ApiResponse(responseCode = "200", description = "Following list",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = UserDto.class))))
|
responseCode = "200",
|
||||||
|
description = "Following list",
|
||||||
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = UserDto.class)))
|
||||||
|
)
|
||||||
public java.util.List<UserDto> following(@PathVariable("identifier") String identifier) {
|
public java.util.List<UserDto> following(@PathVariable("identifier") String identifier) {
|
||||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||||
return subscriptionService.getSubscribedUsers(user.getUsername()).stream()
|
return subscriptionService
|
||||||
|
.getSubscribedUsers(user.getUsername())
|
||||||
|
.stream()
|
||||||
.map(userMapper::toDto)
|
.map(userMapper::toDto)
|
||||||
.collect(java.util.stream.Collectors.toList());
|
.collect(java.util.stream.Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{identifier}/followers")
|
@GetMapping("/{identifier}/followers")
|
||||||
@Operation(summary = "Followers", description = "Get followers of this user")
|
@Operation(summary = "Followers", description = "Get followers of this user")
|
||||||
@ApiResponse(responseCode = "200", description = "Followers list",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = UserDto.class))))
|
responseCode = "200",
|
||||||
|
description = "Followers list",
|
||||||
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = UserDto.class)))
|
||||||
|
)
|
||||||
public java.util.List<UserDto> followers(@PathVariable("identifier") String identifier) {
|
public java.util.List<UserDto> followers(@PathVariable("identifier") String identifier) {
|
||||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||||
return subscriptionService.getSubscribers(user.getUsername()).stream()
|
return subscriptionService
|
||||||
|
.getSubscribers(user.getUsername())
|
||||||
|
.stream()
|
||||||
.map(userMapper::toDto)
|
.map(userMapper::toDto)
|
||||||
.collect(java.util.stream.Collectors.toList());
|
.collect(java.util.stream.Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/admins")
|
@GetMapping("/admins")
|
||||||
@Operation(summary = "Admin users", description = "List administrator users")
|
@Operation(summary = "Admin users", description = "List administrator users")
|
||||||
@ApiResponse(responseCode = "200", description = "Admin users",
|
@ApiResponse(
|
||||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = UserDto.class))))
|
responseCode = "200",
|
||||||
|
description = "Admin users",
|
||||||
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = UserDto.class)))
|
||||||
|
)
|
||||||
public java.util.List<UserDto> admins() {
|
public java.util.List<UserDto> admins() {
|
||||||
return userService.getAdmins().stream()
|
return userService
|
||||||
|
.getAdmins()
|
||||||
|
.stream()
|
||||||
.map(userMapper::toDto)
|
.map(userMapper::toDto)
|
||||||
.collect(java.util.stream.Collectors.toList());
|
.collect(java.util.stream.Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{identifier}/all")
|
@GetMapping("/{identifier}/all")
|
||||||
@Operation(summary = "User aggregate", description = "Get aggregate information for user")
|
@Operation(summary = "User aggregate", description = "Get aggregate information for user")
|
||||||
@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 = "User aggregate",
|
||||||
|
content = @Content(schema = @Schema(implementation = UserAggregateDto.class))
|
||||||
|
)
|
||||||
|
public ResponseEntity<UserAggregateDto> userAggregate(
|
||||||
|
@PathVariable("identifier") String identifier,
|
||||||
@RequestParam(value = "postsLimit", required = false) Integer postsLimit,
|
@RequestParam(value = "postsLimit", required = false) Integer postsLimit,
|
||||||
@RequestParam(value = "repliesLimit", required = false) Integer repliesLimit,
|
@RequestParam(value = "repliesLimit", required = false) Integer repliesLimit,
|
||||||
Authentication auth) {
|
Authentication auth
|
||||||
|
) {
|
||||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||||
int pLimit = postsLimit != null ? postsLimit : defaultPostsLimit;
|
int pLimit = postsLimit != null ? postsLimit : defaultPostsLimit;
|
||||||
int rLimit = repliesLimit != null ? repliesLimit : defaultRepliesLimit;
|
int rLimit = repliesLimit != null ? repliesLimit : defaultRepliesLimit;
|
||||||
java.util.List<PostMetaDto> posts = postService.getRecentPostsByUser(user.getUsername(), pLimit).stream()
|
java.util.List<PostMetaDto> posts = postService
|
||||||
|
.getRecentPostsByUser(user.getUsername(), pLimit)
|
||||||
|
.stream()
|
||||||
.map(userMapper::toMetaDto)
|
.map(userMapper::toMetaDto)
|
||||||
.collect(java.util.stream.Collectors.toList());
|
.collect(java.util.stream.Collectors.toList());
|
||||||
java.util.List<CommentInfoDto> replies = commentService.getRecentCommentsByUser(user.getUsername(), rLimit).stream()
|
java.util.List<CommentInfoDto> replies = commentService
|
||||||
|
.getRecentCommentsByUser(user.getUsername(), rLimit)
|
||||||
|
.stream()
|
||||||
.map(userMapper::toCommentInfoDto)
|
.map(userMapper::toCommentInfoDto)
|
||||||
.collect(java.util.stream.Collectors.toList());
|
.collect(java.util.stream.Collectors.toList());
|
||||||
UserAggregateDto dto = new UserAggregateDto();
|
UserAggregateDto dto = new UserAggregateDto();
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
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 Long id;
|
||||||
private String title;
|
private String title;
|
||||||
private String icon;
|
private String icon;
|
||||||
|
|||||||
@@ -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 Long id;
|
||||||
private String username;
|
private String username;
|
||||||
private String avatar;
|
private String avatar;
|
||||||
private MedalType displayMedal;
|
private MedalType displayMedal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import lombok.Data;
|
|||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
public class CategoryDto {
|
public class CategoryDto {
|
||||||
|
|
||||||
private Long id;
|
private Long id;
|
||||||
private String name;
|
private String name;
|
||||||
private String description;
|
private String description;
|
||||||
@@ -14,4 +15,3 @@ public class CategoryDto {
|
|||||||
private String smallIcon;
|
private String smallIcon;
|
||||||
private Long count;
|
private Long count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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 name;
|
||||||
private String description;
|
private String description;
|
||||||
private String icon;
|
private String icon;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import lombok.Setter;
|
|||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
public class ChannelDto {
|
public class ChannelDto {
|
||||||
|
|
||||||
private Long id;
|
private Long id;
|
||||||
private String name;
|
private String name;
|
||||||
private String description;
|
private String description;
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
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 Long id;
|
||||||
private String content;
|
private String content;
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
@@ -20,4 +20,3 @@ public class CommentDto {
|
|||||||
private int reward;
|
private int reward;
|
||||||
private int pointReward;
|
private int pointReward;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
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 Long id;
|
||||||
private String content;
|
private String content;
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|||||||
@@ -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 currentCommentCount;
|
||||||
private long targetCommentCount;
|
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 content;
|
||||||
private String captcha;
|
private String captcha;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import lombok.Data;
|
|||||||
/** DTO for site configuration. */
|
/** DTO for site configuration. */
|
||||||
@Data
|
@Data
|
||||||
public class ConfigDto {
|
public class ConfigDto {
|
||||||
|
|
||||||
private PublishMode publishMode;
|
private PublishMode publishMode;
|
||||||
private PasswordStrength passwordStrength;
|
private PasswordStrength passwordStrength;
|
||||||
private Integer aiFormatLimit;
|
private Integer aiFormatLimit;
|
||||||
|
|||||||
@@ -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 currentContributionLines;
|
||||||
private long targetContributionLines;
|
private long targetContributionLines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
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 Long id;
|
||||||
private String name;
|
private String name;
|
||||||
private boolean channel;
|
private boolean channel;
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
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 Long id;
|
||||||
private String name;
|
private String name;
|
||||||
private boolean channel;
|
private boolean channel;
|
||||||
|
|||||||
@@ -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,6 +5,7 @@ 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 code;
|
||||||
private String redirectUri;
|
private String redirectUri;
|
||||||
private String inviteToken;
|
private String inviteToken;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
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 Long id;
|
||||||
private String title;
|
private String title;
|
||||||
private String content;
|
private String content;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
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 title;
|
||||||
private String content;
|
private String content;
|
||||||
private Long categoryId;
|
private Long categoryId;
|
||||||
|
|||||||
@@ -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 currentFeaturedCount;
|
||||||
private long targetFeaturedCount;
|
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,6 +5,7 @@ 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 code;
|
||||||
private String redirectUri;
|
private String redirectUri;
|
||||||
private String inviteToken;
|
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 idToken;
|
||||||
private String inviteToken;
|
private String inviteToken;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import lombok.Data;
|
|||||||
/** Request to login. */
|
/** Request to login. */
|
||||||
@Data
|
@Data
|
||||||
public class LoginRequest {
|
public class LoginRequest {
|
||||||
|
|
||||||
private String username;
|
private String username;
|
||||||
private String password;
|
private String password;
|
||||||
private String captcha;
|
private String captcha;
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
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 prizeDescription;
|
||||||
private String prizeIcon;
|
private String prizeIcon;
|
||||||
private int prizeCount;
|
private int prizeCount;
|
||||||
|
|||||||
@@ -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 token;
|
||||||
private String reason;
|
private String reason;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import lombok.Data;
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class MedalDto {
|
public class MedalDto {
|
||||||
|
|
||||||
private String icon;
|
private String icon;
|
||||||
private String title;
|
private String title;
|
||||||
private String description;
|
private String description;
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ import lombok.Data;
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class MedalSelectRequest {
|
public class MedalSelectRequest {
|
||||||
|
|
||||||
private MedalType type;
|
private MedalType type;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
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 Long id;
|
||||||
private String content;
|
private String content;
|
||||||
private UserSummaryDto sender;
|
private UserSummaryDto sender;
|
||||||
|
|||||||
@@ -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 String targetUsername;
|
||||||
private Object payload;
|
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 long redeemCount;
|
||||||
private boolean ended;
|
private boolean ended;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ import lombok.Data;
|
|||||||
/** Request to redeem the milk tea activity. */
|
/** Request to redeem the milk tea activity. */
|
||||||
@Data
|
@Data
|
||||||
public class MilkTeaRedeemRequest {
|
public class MilkTeaRedeemRequest {
|
||||||
|
|
||||||
private String contact;
|
private String contact;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ package com.openisle.dto;
|
|||||||
|
|
||||||
import com.openisle.model.NotificationType;
|
import com.openisle.model.NotificationType;
|
||||||
import com.openisle.model.ReactionType;
|
import com.openisle.model.ReactionType;
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
/** DTO representing a user notification. */
|
/** DTO representing a user notification. */
|
||||||
@Data
|
@Data
|
||||||
public class NotificationDto {
|
public class NotificationDto {
|
||||||
|
|
||||||
private Long id;
|
private Long id;
|
||||||
private NotificationType type;
|
private NotificationType type;
|
||||||
private PostSummaryDto post;
|
private PostSummaryDto post;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package com.openisle.dto;
|
package com.openisle.dto;
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
/** Request to mark notifications as read. */
|
/** Request to mark notifications as read. */
|
||||||
@Data
|
@Data
|
||||||
public class NotificationMarkReadRequest {
|
public class NotificationMarkReadRequest {
|
||||||
|
|
||||||
private List<Long> ids;
|
private List<Long> ids;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import lombok.Data;
|
|||||||
/** User notification preference DTO. */
|
/** User notification preference DTO. */
|
||||||
@Data
|
@Data
|
||||||
public class NotificationPreferenceDto {
|
public class NotificationPreferenceDto {
|
||||||
|
|
||||||
private NotificationType type;
|
private NotificationType type;
|
||||||
private boolean enabled;
|
private boolean enabled;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import lombok.Data;
|
|||||||
/** Request to update a single notification preference. */
|
/** Request to update a single notification preference. */
|
||||||
@Data
|
@Data
|
||||||
public class NotificationPreferenceUpdateRequest {
|
public class NotificationPreferenceUpdateRequest {
|
||||||
|
|
||||||
private NotificationType type;
|
private NotificationType type;
|
||||||
private boolean enabled;
|
private boolean enabled;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ import lombok.Data;
|
|||||||
/** DTO representing unread notification count. */
|
/** DTO representing unread notification count. */
|
||||||
@Data
|
@Data
|
||||||
public class NotificationUnreadCountDto {
|
public class NotificationUnreadCountDto {
|
||||||
|
|
||||||
private long count;
|
private long count;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import lombok.Data;
|
|||||||
/** DTO representing a parent comment. */
|
/** DTO representing a parent comment. */
|
||||||
@Data
|
@Data
|
||||||
public class ParentCommentDto {
|
public class ParentCommentDto {
|
||||||
|
|
||||||
private Long id;
|
private Long id;
|
||||||
private String author;
|
private String author;
|
||||||
private String content;
|
private String content;
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ import lombok.EqualsAndHashCode;
|
|||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
public class PioneerMedalDto extends MedalDto {
|
public class PioneerMedalDto extends MedalDto {
|
||||||
|
|
||||||
private long rank;
|
private long rank;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import lombok.Data;
|
|||||||
/** Point mall good info. */
|
/** Point mall good info. */
|
||||||
@Data
|
@Data
|
||||||
public class PointGoodDto {
|
public class PointGoodDto {
|
||||||
|
|
||||||
private Long id;
|
private Long id;
|
||||||
private String name;
|
private String name;
|
||||||
private int cost;
|
private int cost;
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
package com.openisle.dto;
|
package com.openisle.dto;
|
||||||
|
|
||||||
import com.openisle.model.PointHistoryType;
|
import com.openisle.model.PointHistoryType;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
public class PointHistoryDto {
|
public class PointHistoryDto {
|
||||||
|
|
||||||
private Long id;
|
private Long id;
|
||||||
private PointHistoryType type;
|
private PointHistoryType type;
|
||||||
private int amount;
|
private int amount;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import lombok.Data;
|
|||||||
/** Request to redeem a point mall good. */
|
/** Request to redeem a point mall good. */
|
||||||
@Data
|
@Data
|
||||||
public class PointRedeemRequest {
|
public class PointRedeemRequest {
|
||||||
|
|
||||||
private Long goodId;
|
private Long goodId;
|
||||||
private String contact;
|
private String contact;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
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 java.util.Map;
|
import java.util.Map;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class PollDto {
|
public class PollDto {
|
||||||
|
|
||||||
private List<String> options;
|
private List<String> options;
|
||||||
private Map<Integer, Integer> votes;
|
private Map<Integer, Integer> votes;
|
||||||
private LocalDateTime endTime;
|
private LocalDateTime endTime;
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
package com.openisle.dto;
|
package com.openisle.dto;
|
||||||
|
|
||||||
import com.openisle.model.PostChangeType;
|
import com.openisle.model.PostChangeType;
|
||||||
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 PostChangeLogDto {
|
public class PostChangeLogDto {
|
||||||
|
|
||||||
private Long id;
|
private Long id;
|
||||||
private String username;
|
private String username;
|
||||||
private String userAvatar;
|
private String userAvatar;
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
package com.openisle.dto;
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detailed DTO for a post, including comments.
|
* Detailed DTO for a post, including comments.
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
public class PostDetailDto extends PostSummaryDto {
|
public class PostDetailDto extends PostSummaryDto {
|
||||||
|
|
||||||
private List<CommentDto> comments;
|
private List<CommentDto> comments;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user